mcpField
C
clean-architecture-php

clean-architecture-php

github.com/giuseppe-trisciuoglio开发36595 次安装7 周前更新
代码仓库

关于 clean-architecture-php

面向 PHP 8.3+ 和 Symfony 7.x 项目的架构设计技能,涵盖整洁架构、六边形架构与领域驱动设计。指导开发者分层组织应用,使领域逻辑与框架及基础设施分离,并支持独立测试。

主要功能

  1. 01遵循依赖向内的分层设计
  2. 02应用领域驱动设计模式
  3. 03使用端口与适配器隔离基础设施
  4. 04通过内存仓储支持测试
  5. 05指导 Symfony 依赖注入配置

适用场景

  1. 01设计新的 Symfony 企业应用
  2. 02将遗留 PHP 代码重构为可测试结构
  3. 03在 PHP 项目中实现领域驱动设计
  4. 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. 实现领域层

从最内层(领域层)开始,逐层向外构建:

  1. 创建值对象,并在构造时进行验证——使用 PHP 8.1+ 的 readonly 确保其不可变
  2. 创建实体,并加入领域逻辑和业务规则——实体应封装行为,而不只是数据容器
  3. 定义仓储接口(端口)——保持接口精简且专注
  4. 定义领域事件,将副作用与核心业务逻辑解耦
4. 实现应用层

构建用于协调领域对象的用例:

  1. 创建命令,使用 readonly DTO 表示写入操作
  2. 创建查询,用于读取操作(CQRS 模式)
  3. 实现处理器,接收命令/查询并协调领域对象
  4. 定义服务接口,用于外部依赖(通知等)
5. 实现适配器层

创建连接应用层与基础设施层的接口适配器:

  1. 创建控制器,接收 HTTP 请求并调用处理器
  2. 创建请求 DTO,并添加 Symfony 验证属性
  3. 实现仓储适配器,将领域接口连接到持久化层
6. 配置基础设施

设置特定于框架的配置:

  1. 配置 Symfony DI,将接口绑定到实现
  2. 创建测试替身(内存仓储),以便在不连接数据库的情况下进行单元测试
  3. 配置 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()]);
    }
}

最佳实践

  1. 依赖规则:依赖只指向内部——领域层不了解应用层或基础设施层
  2. 不可变性:值对象必须使用 PHP 8.1+ 中的 readonly 保持不可变——绝不允许可变状态
  3. 丰富的领域模型:将业务逻辑放入实体中,并使用 create() 之类的工厂方法——避免贫血模型
  4. 接口隔离:保持仓储接口精简且专注——不要创建万能接口
  5. 框架独立性:领域层和应用层必须能够在不依赖 Symfony 或 Doctrine 的情况下进行测试
  6. 构造时验证:在值对象构造时进行验证——绝不允许无效状态
  7. Symfony 属性:使用 PHP 8 属性配置路由(#[Route])、验证(#[Assert\])和依赖注入
  8. 测试替身:始终为仓储提供内存实现,以便快速进行单元测试
  9. 领域事件:分发领域事件以解耦副作用——不要从实体中调用外部服务
  10. 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