别着急,坐和放宽
每次聊到 PHP,总有人一脸“懂行”地说:
“这语言本来就不行。”
慢着。
你骂的,真的是 PHP 吗?
还是说,你只是在为那些没人管、没人测、上线就扔的代码找替罪羊?
很多人以为 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 的设计哲学恰恰是鼓励分层、依赖注入、可测试。
问题不在框架,而在你怎么用它。
@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)
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 同理。
<?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,但只用于数据映射,不放业务逻辑
关键点:
- 业务规则集中
- 依赖通过
#[Inject]注入- 邮件发送通过事件解耦(可在
Listener中处理)
Hyperf 官方提供 Hyperf\Testing,支持在协程外运行测试,并 Mock 依赖。
优势:
- 不启动 Swoole,纯 PHP 运行
- 依赖可 Mock,无需真实数据库
- 测试速度快,反馈及时
Hyperf 提供了 DI、注解、协程、事件系统……
但它不会自动阻止你在控制器里写 500 行业务逻辑。
框架给你自由,也考验你的纪律。
PHP 早就不是“脚本语言”了。
Hyperf 更不是“玩具框架”。
问题从来不在技术栈——
而在我们是否愿意为可维护性付出那一点点额外成本。
下次再听到“PHP 太乱”,
不妨反问:
“是你写的乱,还是你根本没用现代 PHP 的方式写?”
毕竟,代码不是框架自己生成的。
是你写的。
P.S. 我见过太多团队,一边用 Hyperf 追求高性能,一边在 Controller 里
DB::select()拼字符串。
性能上去了,可维护性掉进了沟里。
别让“快”成了“乱”的借口。
<?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;
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
if (strlen($password) < 8) {
throw new WeakPasswordException();
}
if ($this->userRepository->existsByEmail($email)) {
throw new EmailAlreadyTakenException();
}
$user = $this->userRepository->create([
'email' => $email,
'password' => password_hash($password, PASSWORD_ARGON2ID),
'is_active' => false,
]);
// 触发事件:发送激活邮件(解耦)
$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.',
];
}
}
<?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 {
$dto = RegisterUserDto::fromRequest($this->request->all());
$result = $this->registerService->handle($dto);
return $this->response->json(['ok' => true, ...$result]);
} catch (InvalidEmailException|WeakPasswordException|EmailAlreadyTakenException $e) {
return $this->response->json(['ok' => false, 'error' => $e->getMessage()], 400);
} catch (\Throwable $e) {
// 记录日志(略)
return $this->response->json(['ok' => false, 'error' => 'Registration failed'], 500);
}
}
}
<?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()
{
$userRepository = m::mock(UserRepositoryInterface::class);
$userRepository->shouldReceive('existsByEmail')->with('test@example.com')->andReturnFalse();
$userRepository->shouldReceive('create')->once()->andReturn((object)['id' => 42]);
$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);
}
}