PHP项目六边形架构与端口适配器

wen PHP项目 1

本文目录导读:

PHP项目六边形架构与端口适配器

  1. 核心概念回顾
  2. PHP项目结构示例
  3. 关键代码实现
  4. 优势与PHP最佳实践
  5. 常见陷阱与建议
  6. 与端口适配器的关系说明

这是一个非常专业且前瞻性的问题,在PHP项目中应用六边形架构(又称端口适配器模式),是为了解决传统MVC架构中业务逻辑与框架、数据库、外部服务强耦合的问题,从而打造高可维护、可测试、可独立演进的业务核心。

下面我将从核心概念PHP项目结构示例关键实现细节以及最佳实践四个维度,为你系统性地拆解。


核心概念回顾

六边形架构的核心思想是:业务逻辑是应用程序的中心(六边形内部),它通过“端口”与外部世界通信,“适配器”则负责将外部输入转化为端口能理解的形式。

  • 内部(领域层):纯PHP对象,无框架依赖,无数据库依赖,包含实体、值对象、领域服务、仓库接口。
  • 端口:定义在内部层的接口,如 UserRepositoryInterfaceSendNotificationInterface
  • 外部(基础设施层):适配器实现,如 MysqlUserRepositoryTwilioSmsAdapter

PHP项目结构示例

假设一个用户注册的场景,项目目录结构如下:

project-root/
├── src/
│   ├── Application/          # 应用层(编排用例)
│   │   ├── UseCase/
│   │   │   └── RegisterUserUseCase.php
│   │   └── DTO/
│   │       └── RegisterUserRequest.php
│   │
│   ├── Domain/               # 六边形内部 - 核心
│   │   ├── Entity/
│   │   │   └── User.php
│   │   ├── ValueObject/
│   │   │   └── Email.php
│   │   ├── Repository/
│   │   │   └── UserRepositoryInterface.php    # 输出端口
│   │   └── Service/
│   │       └── NotificationServiceInterface.php # 输入端口
│   │
│   └── Infrastructure/      # 六边形外部 - 适配器
│       ├── Persistence/
│       │   └── DoctrineUserRepository.php     # 输出适配器
│       ├── Notification/
│       │   └── SendGridNotificationAdapter.php # 输入适配器
│       └── Controller/
│           └── ApiUserController.php          # 输入适配器(HTTP端)
│
├── config/
│   └── services.yaml        # 依赖注入配置
├── public/
│   └── index.php
└── tests/
    └── Unit/
        └── Domain/
            └── UserTest.php  # 纯单元测试,无需数据库

关键代码实现

领域层(六边形核心,不依赖任何外部)

// src/Domain/Entity/User.php
class User {
    public function __construct(
        private readonly UserId $id,
        private readonly Email $email,
        private string $name
    ) {}
    public function changeName(string $newName): void {
        // 业务规则逻辑
        if (strlen($newName) < 2) {
            throw new \InvalidArgumentException('Name too short');
        }
        $this->name = $newName;
    }
}
// src/Domain/Repository/UserRepositoryInterface.php (端口)
interface UserRepositoryInterface {
    public function save(User $user): void;
    public function findByEmail(Email $email): ?User;
}

应用层(用例编排)

// src/Application/UseCase/RegisterUserUseCase.php
class RegisterUserUseCase {
    public function __construct(
        private UserRepositoryInterface $userRepo,
        private NotificationServiceInterface $notifier
    ) {}
    public function execute(RegisterUserRequest $request): void {
        // 1. 检查用户是否存在
        if ($this->userRepo->findByEmail($request->email)) {
            throw new \RuntimeException('User already exists');
        }
        // 2. 创建领域实体
        $user = new User(
            UserId::generate(),
            $request->email,
            $request->name
        );
        // 3. 持久化
        $this->userRepo->save($user);
        // 4. 发送通知
        $this->notifier->sendWelcomeEmail($user);
    }
}

基础设施层(适配器实现)

// src/Infrastructure/Persistence/DoctrineUserRepository.php
class DoctrineUserRepository implements UserRepositoryInterface {
    public function __construct(
        private EntityManager $em // Doctrine ORM
    ) {}
    public function save(User $user): void {
        $this->em->persist($user);
        $this->em->flush();
    }
    public function findByEmail(Email $email): ?User {
        return $this->em->getRepository(User::class)
            ->findOneBy(['email' => (string)$email]);
    }
}
// src/Infrastructure/Controller/ApiUserController.php
class ApiUserController {
    public function __construct(
        private RegisterUserUseCase $useCase
    ) {}
    public function register(Request $request): Response {
        $dto = new RegisterUserRequest(
            new Email($request->get('email')),
            $request->get('name')
        );
        try {
            $this->useCase->execute($dto);
            return new Response('OK', 201);
        } catch (\Throwable $e) {
            return new Response($e->getMessage(), 400);
        }
    }
}

优势与PHP最佳实践

  1. 框架无关性:业务逻辑不依赖Laravel/Symfony,你可以先写Domain层,最后再绑定到框架。

  2. 极致可测试性

    • 测试 RegisterUserUseCase 时,只需Mock UserRepositoryInterfaceNotificationServiceInterface

    • 无需启动数据库、无需HTTP内核。

      // tests/Unit/Application/RegisterUserUseCaseTest.php
      public function test_duplicate_email_throws_exception(): void {
      $mockRepo = $this->createMock(UserRepositoryInterface::class);
      $mockRepo->method('findByEmail')->willReturn($this->createMock(User::class));
      $useCase = new RegisterUserUseCase($mockRepo, ...);
      $this->expectException(\RuntimeException::class);
      $useCase->execute(new RegisterUserRequest(...));
      }
  3. 技术栈切换低成本

    • 从MySQL切到PostgreSQL?只需写一个新的 PostgresUserRepository 实现 UserRepositoryInterface
    • 从SendGrid切到Mailgun?同样只需一个新的适配器。

常见陷阱与建议

  1. 避免“贫血领域模型”:不要只把Entity变成简单的getter/setter,业务逻辑应该放在Entity内部,而不是放在Service里。
  2. 防止端口爆炸:如果一个接口只有一个适配器,且未来几乎不可能变化,过早抽象是浪费,建议“第一次用接口分离,第二次再抽象”。
  3. 依赖注入是核心:必须使用容器(如PHP-DI、Symfony DI)来注入适配器实现到用例中。避免在Domain层使用Service Locator
  4. DTO设计:用例的输入输出尽量使用简单的DTO(Data Transfer Object),不要直接传递HTTP Request对象到应用层,这会破坏六边形架构。

与端口适配器的关系说明

  • “端口”:是Domain层定义的接口,如 UserRepositoryInterface,表达“我需要什么”。
  • “适配器”:是Infrastructure层的实现,如 DoctrineUserRepository,表达“我用什么技术来满足这个需求”。

在PHP中,端口适配器的本质是“依赖倒置”——高层模块(Domain)不依赖低层模块(Infrastructure),两者都依赖抽象接口。

在PHP项目中实现六边形架构,本质是将业务复杂度与基础设施复杂度解耦,对于中大型团队、长期维护的PHP项目,或者需要高测试覆盖率、多端点(API+CLI+队列)的Web应用,这套架构的投资回报率非常高,但对于简单的CRUD项目,建议谨慎采用,避免过度工程化。

如果你有具体的“如何将现有MVC项目重构为六边形架构”的场景,欢迎进一步交流。

抱歉,评论功能暂时关闭!