找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1419

积分

0

好友

179

主题
发表于 昨天 03:20 | 查看: 2| 回复: 0

每次讨论到 PHP,似乎总有人摆出一副了如指掌的姿态,断言:“这语言本身就不行。”

请先等等。

你真正在批评的,是 PHP 这门语言,还是那些无人维护、未经测试、上线即抛的混乱代码,正在让 PHP 背锅?

一个示例:用 Hyperf 的方式重构“用户注册”

许多开发者误以为 Hyperf 仅仅是“Swoole 的包装器”,于是顺手就把控制器写成了这样:

// 典型的反模式:Hyperf 控制器中塞满业务逻辑
class AuthController extends AbstractController
{
    public function register()
    {
        $email = $this->request->input('email');
        $password = $this->request->input('password');

        // 验证、查库、哈希、发邮件、分配角色……全挤在这里
        // 甚至可能直接使用 DB::query() 拼接 SQL
        // 异常直接 throw,前端收到的是笼统的 500 错误
    }
}

这段代码能运行吗?当然可以。
但它可维护、可测试吗?答案是不能。

然而,Hyperf 的设计哲学恰恰是鼓励分层、依赖注入和可测试性。问题往往不在于框架本身,而在于开发者如何使用它。

遵循 Hyperf 风格的整洁架构实践

第一步:定义 DTO(使用 Hyperf 的 @Data 注解)

<?php
// app/Dto/RegisterUserDto.php
declare(strict_types=1);

namespace App\Dto;

use Hyperf\Contract\Arrayable;
use Hyperf\Utils\Arr;

#[\Hyperf\Data\Attributes\Data]
class RegisterUserDto implements Arrayable
{
    public function __construct(
        public string $email,
        public string $password
    ) {}

    public static function fromRequest(array $data): self
    {
        return new self(
            email: trim(Arr::get($data, 'email', '')),
            password: (string) Arr::get($data, 'password', '')
        );
    }

    public function toArray(): array
    {
        return [
            'email' => $this->email,
            'password' => $this->password,
        ];
    }
}

提示:在 Hyperf 3.1+ 版本中,推荐使用 hyperf/data 组件来处理 DTO,它能提供类型安全并支持自动验证(可配合 @Validation 注解使用)。

第二步:定义领域异常(继承自 Hyperf 的 HttpException

<?php
// app/Exception/Domain/EmailAlreadyTakenException.php
declare(strict_types=1);

namespace App\Exception\Domain;

use Hyperf\Server\Exception\ServerException;
use Throwable;

class EmailAlreadyTakenException extends ServerException
{
    public function __construct(string $message = 'Email already registered', int $code = 400, ?Throwable $previous = null)
    {
        parent::__construct($message, $code, $previous);
    }
}

类似的,你还可以定义 InvalidEmailExceptionWeakPasswordException 等异常,让业务错误的语义更加清晰。

第三步:Repository 接口与实现(利用 Hyperf 的 DB 组件)

首先定义接口:

<?php
// app/Repository/User/UserRepositoryInterface.php
namespace App\Repository\User;

use App\Model\User;

interface UserRepositoryInterface
{
    public function existsByEmail(string $email): bool;
    public function create(array $attributes): User;
}

然后是实现类:

<?php
// app/Repository/User/UserRepository.php
declare(strict_types=1);

namespace App\Repository\User;

use App\Model\User;
use Hyperf\DbConnection\Db;

class UserRepository implements UserRepositoryInterface
{
    public function existsByEmail(string $email): bool
    {
        return User::where('email', $email)->exists();
    }

    public function create(array $attributes): User
    {
        return User::create($attributes);
    }
}

注意:这里虽然使用了 Eloquent 模型,但其职责仅限于数据存取映射,不包含任何业务逻辑。这种对分层架构的遵守,是保证代码清晰度的关键。

第四步:服务层 —— 核心业务逻辑的归宿

<?php
// app/Service/User/RegisterUserService.php
declare(strict_types=1);

namespace App\Service\User;

use App\Dto\RegisterUserDto;
use App\Exception\Domain\EmailAlreadyTakenException;
use App\Exception\Domain\InvalidEmailException;
use App\Exception\Domain\WeakPasswordException;
use App\Repository\User\UserRepositoryInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Utils\ApplicationContext;

class RegisterUserService
{
    #[Inject]
    protected UserRepositoryInterface $userRepository;

    public function handle(RegisterUserDto $dto): array
    {
        $email = $dto->email;
        $password = $dto->password;

        // 1. 验证邮箱格式
        if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException();
        }

        // 2. 验证密码强度
        if (strlen($password) < 8) {
            throw new WeakPasswordException();
        }

        // 3. 检查邮箱是否已存在
        if ($this->userRepository->existsByEmail($email)) {
            throw new EmailAlreadyTakenException();
        }

        // 4. 创建用户
        $user = $this->userRepository->create([
            'email' => $email,
            'password' => password_hash($password, PASSWORD_ARGON2ID),
            'is_active' => false,
        ]);

        // 5. 触发事件:发送激活邮件(实现业务解耦)
        $event = ApplicationContext::getContainer()->get(\App\Event\UserRegistered::class);
        $event->handle($user->id, $email);

        return [
            'user_id' => $user->id,
            'message' => 'Registration successful. Please check your email.',
        ];
    }
}

这段代码的关键点

  • 业务规则集中:所有注册相关的校验和逻辑都聚集在此。
  • 依赖注入:Repository 通过 #[Inject] 自动注入,便于替换和测试。
  • 事件解耦:发送邮件等副作用操作通过事件系统处理,可在 Listener 中异步执行,保持服务层的纯粹性。

第五步:控制器 —— 薄到只剩“胶水”代码

<?php
// app/Controller/AuthController.php
declare(strict_types=1);

namespace App\Controller;

use App\Dto\RegisterUserDto;
use App\Exception\Domain\EmailAlreadyTakenException;
use App\Exception\Domain\InvalidEmailException;
use App\Exception\Domain\WeakPasswordException;
use App\Service\User\RegisterUserService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Post;
use Psr\Http\Message\ResponseInterface;

#[Controller(prefix: 'auth')]
class AuthController extends AbstractController
{
    #[Inject]
    protected RegisterUserService $registerService;

    #[Post('/register')]
    public function register(): ResponseInterface
    {
        try {
            // 1. 组装DTO
            $dto = RegisterUserDto::fromRequest($this->request->all());
            // 2. 调用服务
            $result = $this->registerService->handle($dto);
            // 3. 返回成功响应
            return $this->response->json(['ok' => true, ...$result]);
        } catch (InvalidEmailException|WeakPasswordException|EmailAlreadyTakenException $e) {
            // 捕获已知业务异常,返回友好的 400 错误
            return $this->response->json(['ok' => false, 'error' => $e->getMessage()], 400);
        } catch (\Throwable $e) {
            // 记录未知异常日志(此处略)
            // 返回通用的 500 错误,避免泄露系统内部信息
            return $this->response->json(['ok' => false, 'error' => 'Registration failed'], 500);
        }
    }
}

测试:为 Hyperf 服务编写单元测试

Hyperf 官方提供了 Hyperf\Testing 组件,支持在协程环境外运行测试,并能方便地 Mock 依赖。

<?php
// test/Cases/User/RegisterUserServiceTest.php
declare(strict_types=1);

namespace Test\Cases\User;

use App\Dto\RegisterUserDto;
use App\Exception\Domain\EmailAlreadyTakenException;
use App\Repository\User\UserRepositoryInterface;
use App\Service\User\RegisterUserService;
use Hyperf\Testing\TestCase;
use Mockery as m;

/**
 * @internal
 * @coversNothing
 */
class RegisterUserServiceTest extends TestCase
{
    public function testRegisterSuccess()
    {
        // Mock 依赖的 Repository
        $userRepository = m::mock(UserRepositoryInterface::class);
        $userRepository->shouldReceive('existsByEmail')->with('test@example.com')->andReturnFalse();
        $userRepository->shouldReceive('create')->once()->andReturn((object)['id' => 42]);

        // 将 Mock 对象注入容器
        $container = $this->getContainer();
        $container->set(UserRepositoryInterface::class, $userRepository);

        // 获取服务并执行
        $service = $container->get(RegisterUserService::class);
        $dto = new RegisterUserDto('test@example.com', 'secure123');
        $result = $service->handle($dto);

        // 断言结果
        $this->assertSame(42, $result['user_id']);
        $this->assertStringContainsString('email', $result['message']);
    }

    public function testThrowsIfEmailExists()
    {
        $userRepository = m::mock(UserRepositoryInterface::class);
        $userRepository->shouldReceive('existsByEmail')->with('taken@example.com')->andReturnTrue();

        $container = $this->getContainer();
        $container->set(UserRepositoryInterface::class, $userRepository);

        $service = $container->get(RegisterUserService::class);
        $dto = new RegisterUserDto('taken@example.com', 'pass123');

        // 断言会抛出特定异常
        $this->expectException(EmailAlreadyTakenException::class);
        $service->handle($dto);
    }
}

这种单元测试方式的优势

  • 无需启动 Swoole:在纯 PHP 环境下运行,速度快。
  • 依赖完全可 Mock无需连接真实数据库,测试环境隔离且稳定。
  • 反馈及时:执行速度快,能快速验证业务逻辑的正确性。

结论:Hyperf 是工具,而非魔法

Hyperf 提供了强大的依赖注入、注解、协程、事件系统等现代化工具。
但它无法自动阻止你将 500 行业务逻辑塞进控制器。

框架赋予你自由,同时也考验你的开发纪律。

如今的 PHP 早已不是那个简单的“脚本语言”。Hyperf 更不是一个“玩具框架”。问题的根源很少在于技术栈本身——而在于我们是否愿意为了长远的可维护性,付出那一点点额外的设计成本。

下次再听到有人抱怨“PHP 项目太乱”,或许可以反问一句:“究竟是语言或框架的错,还是代码没有按照现代 PHP 的最佳实践来书写?”

毕竟,代码不是框架自动生成的。
是你写的。

附言:我见过不少团队,一方面采用 Hyperf 追求极致性能,另一方面却在 Controller 里使用 DB::select() 拼接 SQL 字符串。性能指标上去了,可维护性却跌入了深渊。切勿让对“快”的追求,成为编写“乱”代码的借口。

希望这个完整的示例能为你带来启发。如果你在实践分层架构单元测试中遇到其他问题,欢迎到云栈社区与更多开发者交流讨论。




上一篇:提升Claude Code效率:8个覆盖完整开发工作流的必备插件
下一篇:跨境SaaS收款的第一性原理:个人开发者如何掌握交易主动权
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 09:01 , Processed in 0.478225 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表