每次讨论到 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);
}
}
类似的,你还可以定义 InvalidEmailException、WeakPasswordException 等异常,让业务错误的语义更加清晰。
第三步: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 字符串。性能指标上去了,可维护性却跌入了深渊。切勿让对“快”的追求,成为编写“乱”代码的借口。
希望这个完整的示例能为你带来启发。如果你在实践分层架构或单元测试中遇到其他问题,欢迎到云栈社区与更多开发者交流讨论。