clean-architecture-php
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClean Architecture, Hexagonal Architecture & DDD for PHP/Symfony
PHP/Symfony 中的 Clean Architecture、Hexagonal Architecture 与 DDD 实践
Overview
概述
This skill provides guidance for implementing Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design patterns in PHP 8.3+ applications using Symfony 7.x. It ensures clear separation of concerns, framework-independent business logic, and highly testable code through layered architecture with inward-only dependencies.
本技能为使用Symfony 7.x的PHP 8.3+应用提供Clean Architecture、Hexagonal Architecture(端口与适配器模式)以及领域驱动设计(DDD)模式的实现指导。通过依赖仅向内的分层架构,确保关注点清晰分离、业务逻辑独立于框架,同时代码具备高度可测试性。
When to Use
适用场景
- Architecting new enterprise PHP applications with Symfony 7.x
- Refactoring legacy PHP code to modern, testable patterns
- Implementing Domain-Driven Design in PHP projects
- Creating maintainable applications with clear separation of concerns
- Building testable business logic independent of frameworks
- Designing modular PHP systems with swappable infrastructure
- 基于Symfony 7.x构建全新的企业级PHP应用
- 将遗留PHP代码重构为现代化、可测试的架构模式
- 在PHP项目中实现领域驱动设计(DDD)
- 创建关注点清晰、易于维护的应用
- 构建独立于框架的可测试业务逻辑
- 设计具备可替换基础设施的模块化PHP系统
Instructions
操作步骤
1. Understand the Architecture Layers
1. 理解架构分层
Clean Architecture follows the dependency rule: dependencies only point inward.
+-------------------------------------+
| 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
+-------------------------------------+Hexagonal Architecture (Ports & Adapters):
- Domain Core: Business logic, framework-agnostic
- Ports: Interfaces (e.g., )
UserRepositoryInterface - Adapters: Concrete implementations (Doctrine, InMemory for tests)
DDD Tactical Patterns:
- Entities: Objects with identity (e.g., ,
User)Order - Value Objects: Immutable, defined by attributes (e.g., ,
Email)Money - Aggregates: Consistency boundaries with root entity
- Domain Events: Capture business occurrences
- Repositories: Persist/retrieve aggregates
Clean Architecture遵循依赖规则:所有依赖仅指向内部。
+-------------------------------------+
| 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
+-------------------------------------+Hexagonal Architecture(端口与适配器模式):
- 领域核心:业务逻辑,与框架无关
- 端口:接口(例如 )
UserRepositoryInterface - 适配器:具体实现(Doctrine、用于测试的内存实现)
DDD战术模式:
- 实体:具备唯一标识的对象(例如 、
User)Order - 值对象:不可变,由属性定义(例如 、
Email)Money - 聚合:包含根实体的一致性边界
- 领域事件:捕获业务发生的事件
- 仓库:持久化/检索聚合对象
2. Organize Directory Structure
2. 组织目录结构
Create the following directory structure to enforce layer separation:
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创建以下目录结构以强制实现分层隔离:
src/
+-- Domain/ # 最内层 - 无外部依赖
| +-- Entity/
| | +-- User.php
| | +-- Order.php
| +-- ValueObject/
| | +-- Email.php
| | +-- Money.php
| | +-- OrderId.php
| +-- Repository/
| | +-- UserRepositoryInterface.php
| +-- Event/
| | +-- UserCreatedEvent.php
| +-- Exception/
| +-- DomainException.php
+-- Application/ # 用例层 - 依赖领域层
| +-- Command/
| | +-- CreateUserCommand.php
| | +-- UpdateOrderCommand.php
| +-- Handler/
| | +-- CreateUserHandler.php
| | +-- UpdateOrderHandler.php
| +-- Query/
| | +-- GetUserQuery.php
| +-- Dto/
| | +-- UserDto.php
| +-- Service/
| +-- NotificationServiceInterface.php
+-- Adapter/ # 接口适配器层
| +-- Http/
| | +-- Controller/
| | | +-- UserController.php
| | +-- Request/
| | +-- CreateUserRequest.php
| +-- Persistence/
| +-- Doctrine/
| +-- Repository/
| | +-- DoctrineUserRepository.php
| +-- Mapping/
| +-- User.orm.xml
+-- Infrastructure/ # 框架与外部依赖层
+-- Config/
| +-- services.yaml
+-- Event/
| +-- SymfonyEventDispatcher.php
+-- Service/
+-- SendgridEmailService.php3. Implement Domain Layer
3. 实现领域层
Start from the innermost layer (Domain) and work outward:
- Create Value Objects with validation at construction time - they must be immutable using PHP 8.1+
readonly - Create Entities with domain logic and business rules - entities should encapsulate behavior, not just be data bags
- Define Repository Interfaces (Ports) - keep them small and focused
- Define Domain Events to decouple side effects from core business logic
从最内层(领域层)开始向外构建:
- 创建值对象:在构造时进行验证,使用PHP 8.1+的关键字确保不可变
readonly - 创建实体:包含领域逻辑与业务规则,实体应封装行为,而非仅作为数据容器
- 定义仓库接口(端口):保持接口小巧且聚焦
- 定义领域事件:将副作用与核心业务逻辑解耦
4. Implement Application Layer
4. 实现应用层
Build use cases that orchestrate domain objects:
- Create Commands as readonly DTOs representing write operations
- Create Queries for read operations (CQRS pattern)
- Implement Handlers that receive commands/queries and coordinate domain objects
- Define Service Interfaces for external dependencies (notifications, etc.)
构建编排领域对象的用例:
- 创建命令:作为只读DTO表示写操作
- 创建查询:用于读操作(CQRS模式)
- 实现处理器:接收命令/查询并协调领域对象
- 定义服务接口:用于外部依赖(如通知服务等)
5. Implement Adapter Layer
5. 实现适配器层
Create interface adapters that connect Application to Infrastructure:
- Create Controllers that receive HTTP requests and invoke handlers
- Create Request DTOs with Symfony validation attributes
- Implement Repository Adapters that bridge domain interfaces to persistence layer
创建连接应用层与基础设施层的接口适配器:
- 创建控制器:接收HTTP请求并调用处理器
- 创建请求DTO:使用Symfony验证属性
- 实现仓库适配器:桥接领域接口与持久化层
6. Configure Infrastructure
6. 配置基础设施
Set up framework-specific configuration:
- Configure Symfony DI to bind interfaces to implementations
- Create test doubles (In-Memory repositories) for unit testing without database
- Configure Doctrine mappings for persistence
设置框架相关配置:
- 配置Symfony依赖注入:将接口绑定到具体实现
- 创建测试替身:(内存仓库)用于无需数据库的单元测试
- 配置Doctrine映射:用于持久化
7. Test Without Framework
7. 脱离框架测试
Ensure Domain and Application layers are testable without Symfony, Doctrine, or database. Use In-Memory repositories for fast unit tests.
确保领域层与应用层无需Symfony、Doctrine或数据库即可测试。使用内存仓库实现快速单元测试。
Examples
示例代码
Example 1: Value Object with Validation
示例1:带验证的 value object
php
<?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);
}
}php
<?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);
}
}Example 2: Entity with Domain Logic
示例2:包含领域逻辑的实体
php
<?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;
}
}php
<?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;
}
}Example 3: Repository Port (Interface)
示例3:仓库端口(接口)
php
<?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;
}php
<?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;
}Example 4: Command and Handler
示例4:命令与处理器
php
<?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
<?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);
}
}php
<?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
<?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);
}
}Example 5: Symfony Controller
示例5:Symfony控制器
php
<?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);
}
}php
<?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);
}
}Example 6: Request DTO with Validation
示例6:带验证的请求DTO
php
<?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;
}php
<?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;
}Example 7: Doctrine Repository Adapter
示例7:Doctrine仓库适配器
php
<?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();
}
}
}php
<?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();
}
}
}Example 8: Symfony DI Configuration
示例8:Symfony依赖注入配置
yaml
undefinedyaml
undefinedconfig/services.yaml
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\InMemoryUserRepositoryundefinedservices:
_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\InMemoryUserRepositoryundefinedExample 9: In-Memory Repository for Testing
示例9:用于测试的内存仓库
php
<?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
<?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()]);
}
}Best Practices
最佳实践
- Dependency Rule: Dependencies only point inward - domain knows nothing of application or infrastructure
- Immutability: Value Objects MUST be immutable using in PHP 8.1+ - never allow mutable state
readonly - Rich Domain Models: Put business logic in entities with factory methods like - avoid anemic models
create() - Interface Segregation: Keep repository interfaces small and focused - do not create god interfaces
- Framework Independence: Domain and application layers MUST be testable without Symfony or Doctrine
- Validation at Construction: Validate in Value Objects at construction time - never allow invalid state
- Symfony Attributes: Use PHP 8 attributes for routing (), validation (
#[Route]), and DI#[Assert\] - Test Doubles: Always provide In-Memory implementations for repositories to enable fast unit tests
- Domain Events: Dispatch domain events to decouple side effects - do not call external services from entities
- XML/YAML Mappings: Use XML or YAML for Doctrine mappings instead of annotations in domain entities
- 依赖规则:依赖仅指向内部,领域层对应用层和基础设施层一无所知
- 不可变性:值对象必须使用PHP 8.1+的确保不可变,绝不允许可变状态
readonly - 富领域模型:在实体中通过等工厂方法封装业务逻辑,避免贫血模型
create() - 接口隔离:保持仓库接口小巧聚焦,不要创建全能接口
- 框架独立:领域层和应用层必须能够脱离Symfony或Doctrine进行测试
- 构造时验证:在值对象构造时进行验证,绝不允许无效状态存在
- Symfony属性:使用PHP 8属性进行路由()、验证(
#[Route])和依赖注入配置#[Assert\] - 测试替身:始终为仓库提供内存实现,以支持快速单元测试
- 领域事件:通过领域事件解耦副作用,不要在实体中直接调用外部服务
- XML/YAML映射:使用XML或YAML进行Doctrine映射,而非领域实体中的注解
Constraints and Warnings
约束与警告
Architecture Constraints
架构约束
- Dependency Rule: Dependencies only point inward. Domain knows nothing of Application, Application knows nothing of Infrastructure. Violating this breaks the architecture.
- No Anemic Domain: Entities should encapsulate behavior, not just be data bags. Avoid getters/setters without business logic.
- Interface Segregation: Keep repository interfaces small and focused. Do not create god interfaces.
- 依赖规则:依赖仅指向内部,领域层不知道应用层,应用层不知道基础设施层。违反此规则会破坏架构。
- 禁止贫血领域模型:实体应封装行为,而非仅作为数据容器。避免无业务逻辑的getter/setter。
- 接口隔离:保持仓库接口小巧聚焦,不要创建全能接口。
PHP Implementation Constraints
PHP实现约束
- Immutability: Value Objects MUST be immutable using in PHP 8.1+. Never allow mutable state in Value Objects.
readonly - Validation: Validate in Value Objects at construction time. Never allow invalid state to exist.
- Symfony Attributes: Use PHP 8 attributes for routing, validation, and DI (,
#[Route],#[Assert\Email]).#[Autowire]
- 不可变性:值对象必须使用PHP 8.1+的确保不可变,绝不允许值对象存在可变状态。
readonly - 验证规则:在值对象构造时进行验证,绝不允许无效状态存在。
- Symfony属性:使用PHP 8属性进行路由、验证和依赖注入(、
#[Route]、#[Assert\Email])。#[Autowire]
Testing Constraints
测试约束
- Framework Independence: Domain and Application layers MUST be testable without Symfony, Doctrine, or database.
- Test Doubles: Always provide In-Memory implementations for repository interfaces to enable fast unit tests.
- 框架独立:领域层和应用层必须能够脱离Symfony、Doctrine或数据库进行测试。
- 测试替身:始终为仓库接口提供内存实现,以支持快速单元测试。
Warnings
警告
- Avoid Rich Domain Models in Controllers: Controllers should only coordinate, not contain business logic.
- Beware of Leaky Abstractions: Infrastructure concerns (like Doctrine annotations) should not leak into Domain entities. Use XML/YAML mappings instead.
- Command Bus Consideration: For complex applications, use Symfony Messenger for async processing. Do not inline complex orchestrations in handlers.
- Domain Events: Dispatch domain events to decouple side effects from core business logic. Do not call external services directly from entities.
- 避免在控制器中实现富领域逻辑:控制器仅应负责协调,不应包含业务逻辑。
- 警惕抽象泄漏:基础设施相关内容(如Doctrine注解)不应泄漏到领域实体中,应使用XML/YAML映射替代。
- 命令总线考量:对于复杂应用,使用Symfony Messenger进行异步处理,不要在处理器中内联复杂编排逻辑。
- 领域事件:通过领域事件解耦副作用与核心业务逻辑,不要在实体中直接调用外部服务。
References
参考资料
- PHP Clean Architecture Patterns
- Symfony Implementation Guide
- PHP Clean Architecture Patterns
- Symfony Implementation Guide