关于 clean-architecture-php
面向 PHP 8.3+ 和 Symfony 7.x 项目的架构设计技能,涵盖整洁架构、六边形架构与领域驱动设计。指导开发者分层组织应用,使领域逻辑与框架及基础设施分离,并支持独立测试。
主要功能
- 01遵循依赖向内的分层设计
- 02应用领域驱动设计模式
- 03使用端口与适配器隔离基础设施
- 04通过内存仓储支持测试
- 05指导 Symfony 依赖注入配置
适用场景
- 01设计新的 Symfony 企业应用
- 02将遗留 PHP 代码重构为可测试结构
- 03在 PHP 项目中实现领域驱动设计
- 04构建可替换基础设施的模块化系统
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill clean-architecture-php安装后在 Claude Code 中输入/clean-architecture-php确认已加载。
附带文件references/php-clean-architecture.mdreferences/symfony-implementation.md
详细介绍
PHP/Symfony 的整洁架构、六边形架构与 DDD
概述
本技能为使用 Symfony 7.x 的 PHP 8.3+ 应用提供整洁架构、六边形架构(端口与适配器)和领域驱动设计模式的实施指导。它通过依赖仅向内的分层架构,确保职责清晰分离、业务逻辑不依赖框架,并让代码易于测试。
适用场景
- 使用 Symfony 7.x 设计新的企业级 PHP 应用
- 将旧版 PHP 代码重构为现代且易于测试的模式
- 在 PHP 项目中实施领域驱动设计
- 构建职责分离清晰、易于维护的应用
- 构建独立于框架且易于测试的业务逻辑
- 设计基础设施可替换的模块化 PHP 系统
指南
1. 了解架构层
整洁架构遵循依赖规则:依赖只能指向内层。
+-------------------------------------+
| Infrastructure (Frameworks) | Symfony, Doctrine, External APIs
+-------------------------------------+
| Adapter (Interface Adapters) | Controllers, Repositories, Presenters
+-------------------------------------+
| Application (Use Cases) | Commands, Handlers, DTOs
+-------------------------------------+
| Domain (Entities & Business Rules) | Entities, Value Objects, Domain Events
+-------------------------------------+
六边形架构(端口与适配器):
- 领域核心:业务逻辑,不依赖框架
- 端口:接口(例如
UserRepositoryInterface) - 适配器:具体实现(Doctrine、用于测试的 InMemory)
DDD 战术模式:
- 实体:具有标识的对象(例如
User、Order) - 值对象:不可变,由属性定义(例如
Email、Money) - 聚合:以根实体为界的一致性边界
- 领域事件:记录业务事件
- 仓储:持久化/检索聚合
2. 组织目录结构
创建以下目录结构,以确保各层分离:
src/
+-- Domain/ # Innermost layer - no dependencies
| +-- Entity/
| | +-- User.php
| | +-- Order.php
| +-- ValueObject/
| | +-- Email.php
| | +-- Money.php
| | +-- OrderId.php
| +-- Repository/
| | +-- UserRepositoryInterface.php
| +-- Event/
| | +-- UserCreatedEvent.php
| +-- Exception/
| +-- DomainException.php
+-- Application/ # Use cases - depends on Domain
| +-- Command/
| | +-- CreateUserCommand.php
| | +-- UpdateOrderCommand.php
| +-- Handler/
| | +-- CreateUserHandler.php
| | +-- UpdateOrderHandler.php
| +-- Query/
| | +-- GetUserQuery.php
| +-- Dto/
| | +-- UserDto.php
| +-- Service/
| +-- NotificationServiceInterface.php
+-- Adapter/ # Interface adapters
| +-- Http/
| | +-- Controller/
| | | +-- UserController.php
| | +-- Request/
| | +-- CreateUserRequest.php
| +-- Persistence/
| +-- Doctrine/
| +-- Repository/
| | +-- DoctrineUserRepository.php
| +-- Mapping/
| +-- User.orm.xml
+-- Infrastructure/ # Framework & external concerns
+-- Config/
| +-- services.yaml
+-- Event/
| +-- SymfonyEventDispatcher.php
+-- Service/
+-- SendgridEmailService.php
3. 实现领域层
从最内层(领域层)开始,逐层向外构建:
- 创建值对象,并在构造时进行验证——使用 PHP 8.1+ 的
readonly确保其不可变 - 创建实体,并加入领域逻辑和业务规则——实体应封装行为,而不只是数据容器
- 定义仓储接口(端口)——保持接口精简且专注
- 定义领域事件,将副作用与核心业务逻辑解耦
4. 实现应用层
构建用于协调领域对象的用例:
- 创建命令,使用 readonly DTO 表示写入操作
- 创建查询,用于读取操作(CQRS 模式)
- 实现处理器,接收命令/查询并协调领域对象
- 定义服务接口,用于外部依赖(通知等)
5. 实现适配器层
创建连接应用层与基础设施层的接口适配器:
- 创建控制器,接收 HTTP 请求并调用处理器
- 创建请求 DTO,并添加 Symfony 验证属性
- 实现仓储适配器,将领域接口连接到持久化层
6. 配置基础设施
设置特定于框架的配置:
- 配置 Symfony DI,将接口绑定到实现
- 创建测试替身(内存仓储),以便在不连接数据库的情况下进行单元测试
- 配置 Doctrine 映射,用于持久化
7. 不依赖框架进行测试
确保领域层和应用层可以在不依赖 Symfony、Doctrine 或数据库的情况下进行测试。使用内存仓储执行快速单元测试。
示例
示例 1:带验证的值对象
<?php
// src/Domain/ValueObject/Email.php
namespace App\Domain\ValueObject;
use InvalidArgumentException;
final readonly class Email
{
public function __construct(
private string $value
) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
sprintf('"%s" is not a valid email address', $value)
);
}
}
public function value(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function domain(): string
{
return substr($this->value, strrpos($this->value, '@') + 1);
}
}
示例 2:包含领域逻辑的实体
<?php
// src/Domain/Entity/User.php
namespace App\Domain\Entity;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
use App\Domain\Event\UserCreatedEvent;
use DateTimeImmutable;
class User
{
private array $domainEvents = [];
public function __construct(
private UserId $id,
private Email $email,
private string $name,
private DateTimeImmutable $createdAt,
private bool $isActive = true
) {
$this->recordEvent(new UserCreatedEvent($id->value()));
}
public static function create(
UserId $id,
Email $email,
string $name
): self {
return new self(
$id,
$email,
$name,
new DateTimeImmutable()
);
}
public function deactivate(): void
{
$this->isActive = false;
}
public function canPlaceOrder(): bool
{
return $this->isActive;
}
public function id(): UserId
{
return $this->id;
}
public function email(): Email
{
return $this->email;
}
public function domainEvents(): array
{
return $this->domainEvents;
}
public function clearDomainEvents(): void
{
$this->domainEvents = [];
}
private function recordEvent(object $event): void
{
$this->domainEvents[] = $event;
}
}
示例 3:仓储端口(接口)
<?php
// src/Domain/Repository/UserRepositoryInterface.php
namespace App\Domain\Repository;
use App\Domain\Entity\User;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function save(User $user): void;
public function delete(UserId $id): void;
}
示例 4:命令与处理器
<?php
// src/Application/Command/CreateUserCommand.php
namespace App\Application\Command;
final readonly class CreateUserCommand
{
public function __construct(
public string $id,
public string $email,
public string $name
) {
}
}
<?php
// src/Application/Handler/CreateUserHandler.php
namespace App\Application\Handler;
use App\Application\Command\CreateUserCommand;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
use InvalidArgumentException;
readonly class CreateUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepository
) {
}
public function __invoke(CreateUserCommand $command): void
{
$email = new Email($command->email);
if ($this->userRepository->findByEmail($email) !== null) {
throw new InvalidArgumentException(
'User with this email already exists'
);
}
$user = User::create(
new UserId($command->id),
$email,
$command->name
);
$this->userRepository->save($user);
}
}
示例 5:Symfony 控制器
<?php
// src/Adapter/Http/Controller/UserController.php
namespace App\Adapter\Http\Controller;
use App\Adapter\Http\Request\CreateUserRequest;
use App\Application\Command\CreateUserCommand;
use App\Application\Handler\CreateUserHandler;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\Uuid;
#[AsController]
class UserController
{
public function __construct(
private CreateUserHandler $createUserHandler
) {
}
#[Route('/api/users', methods: ['POST'])]
public function create(CreateUserRequest $request): JsonResponse
{
$command = new CreateUserCommand(
id: Uuid::v4()->toRfc4122(),
email: $request->email,
name: $request->name
);
($this->createUserHandler)($command);
return new JsonResponse(['id' => $command->id], 201);
}
}
示例 6:带验证的请求 DTO
<?php
// src/Adapter/Http/Request/CreateUserRequest.php
namespace App\Adapter\Http\Request;
use Symfony\Component\Validator\Constraints as Assert;
class CreateUserRequest
{
#[Assert\NotBlank]
#[Assert\Email]
public string $email;
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 100)]
public string $name;
}
示例 7:Doctrine 仓储适配器
<?php
// src/Adapter/Persistence/Doctrine/Repository/DoctrineUserRepository.php
namespace App\Adapter\Persistence\Doctrine\Repository;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
use Doctrine\ORM\EntityManagerInterface;
readonly class DoctrineUserRepository implements UserRepositoryInterface
{
public function __construct(
private EntityManagerInterface $entityManager
) {
}
public function findById(UserId $id): ?User
{
return $this->entityManager
->getRepository(User::class)
->find($id->value());
}
public function findByEmail(Email $email): ?User
{
return $this->entityManager
->getRepository(User::class)
->findOneBy(['email.value' => $email->value()]);
}
public function save(User $user): void
{
$this->entityManager->persist($user);
$this->entityManager->flush();
}
public function delete(UserId $id): void
{
$user = $this->findById($id);
if ($user !== null) {
$this->entityManager->remove($user);
$this->entityManager->flush();
}
}
}
示例 8:Symfony DI 配置
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/Domain/Entity/'
- '../src/Kernel.php'
# Repository binding - Port to Adapter
App\Domain\Repository\UserRepositoryInterface:
class: App\Adapter\Persistence\Doctrine\Repository\DoctrineUserRepository
# In-memory repository for tests
App\Domain\Repository\UserRepositoryInterface $inMemoryUserRepository:
class: App\Tests\Infrastructure\Repository\InMemoryUserRepository
示例 9:用于测试的内存仓储
<?php
// tests/Infrastructure/Repository/InMemoryUserRepository.php
namespace App\Tests\Infrastructure\Repository;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function findById(UserId $id): ?User
{
return $this->users[$id->value()] ?? null;
}
public function findByEmail(Email $email): ?User
{
foreach ($this->users as $user) {
if ($user->email()->equals($email)) {
return $user;
}
}
return null;
}
public function save(User $user): void
{
$this->users[$user->id()->value()] = $user;
}
public function delete(UserId $id): void
{
unset($this->users[$id->value()]);
}
}
最佳实践
- 依赖规则:依赖只指向内部——领域层不了解应用层或基础设施层
- 不可变性:值对象必须使用 PHP 8.1+ 中的
readonly保持不可变——绝不允许可变状态 - 丰富的领域模型:将业务逻辑放入实体中,并使用
create()之类的工厂方法——避免贫血模型 - 接口隔离:保持仓储接口精简且专注——不要创建万能接口
- 框架独立性:领域层和应用层必须能够在不依赖 Symfony 或 Doctrine 的情况下进行测试
- 构造时验证:在值对象构造时进行验证——绝不允许无效状态
- Symfony 属性:使用 PHP 8 属性配置路由(
#[Route])、验证(#[Assert\])和依赖注入 - 测试替身:始终为仓储提供内存实现,以便快速进行单元测试
- 领域事件:分发领域事件以解耦副作用——不要从实体中调用外部服务
- XML/YAML 映射:使用 XML 或 YAML 配置 Doctrine 映射,而不要在领域实体中使用注解
约束和警告
架构约束
- 依赖规则:依赖只指向内部。领域层不了解应用层,应用层不了解基础设施层。违反此规则会破坏架构。
- 避免贫血领域模型:实体应封装行为,而不只是数据容器。避免使用没有业务逻辑的 getter/setter。
- 接口隔离:保持仓储接口精简且专注。不要创建万能接口。
PHP 实现约束
- 不可变性:值对象必须使用 PHP 8.1+ 中的
readonly保持不可变。绝不允许值对象中存在可变状态。 - 验证:在值对象构造时进行验证。绝不允许无效状态存在。
- Symfony 属性:使用 PHP 8 属性配置路由、验证和依赖注入(
#[Route]、#[Assert\Email]、#[Autowire])。
测试约束
- 框架独立性:领域层和应用层必须能够在不依赖 Symfony、Doctrine 或数据库的情况下进行测试。
- 测试替身:始终为仓储接口提供内存实现,以便快速进行单元测试。
警告
- 避免在控制器中实现丰富的领域模型:控制器只应负责协调,不应包含业务逻辑。
- 谨防抽象泄漏:基础设施相关内容(如 Doctrine 注解)不应泄漏到领域实体中。请改用 XML/YAML 映射。
- 考虑命令总线:对于复杂应用,可使用 Symfony Messenger 进行异步处理。不要在处理器中内联复杂的编排逻辑。
- 领域事件:分发领域事件,以将副作用与核心业务逻辑解耦。不要直接从实体中调用外部服务。
参考资料
评论
支持 Markdown