clean-architecture-php

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean 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.php

3. Implement Domain Layer

3. 实现领域层

Start from the innermost layer (Domain) and work outward:
  1. Create Value Objects with validation at construction time - they must be immutable using PHP 8.1+
    readonly
  2. Create Entities with domain logic and business rules - entities should encapsulate behavior, not just be data bags
  3. Define Repository Interfaces (Ports) - keep them small and focused
  4. Define Domain Events to decouple side effects from core business logic
从最内层(领域层)开始向外构建:
  1. 创建值对象:在构造时进行验证,使用PHP 8.1+的
    readonly
    关键字确保不可变
  2. 创建实体:包含领域逻辑与业务规则,实体应封装行为,而非仅作为数据容器
  3. 定义仓库接口(端口):保持接口小巧且聚焦
  4. 定义领域事件:将副作用与核心业务逻辑解耦

4. Implement Application Layer

4. 实现应用层

Build use cases that orchestrate domain objects:
  1. Create Commands as readonly DTOs representing write operations
  2. Create Queries for read operations (CQRS pattern)
  3. Implement Handlers that receive commands/queries and coordinate domain objects
  4. Define Service Interfaces for external dependencies (notifications, etc.)
构建编排领域对象的用例:
  1. 创建命令:作为只读DTO表示写操作
  2. 创建查询:用于读操作(CQRS模式)
  3. 实现处理器:接收命令/查询并协调领域对象
  4. 定义服务接口:用于外部依赖(如通知服务等)

5. Implement Adapter Layer

5. 实现适配器层

Create interface adapters that connect Application to Infrastructure:
  1. Create Controllers that receive HTTP requests and invoke handlers
  2. Create Request DTOs with Symfony validation attributes
  3. Implement Repository Adapters that bridge domain interfaces to persistence layer
创建连接应用层与基础设施层的接口适配器:
  1. 创建控制器:接收HTTP请求并调用处理器
  2. 创建请求DTO:使用Symfony验证属性
  3. 实现仓库适配器:桥接领域接口与持久化层

6. Configure Infrastructure

6. 配置基础设施

Set up framework-specific configuration:
  1. Configure Symfony DI to bind interfaces to implementations
  2. Create test doubles (In-Memory repositories) for unit testing without database
  3. Configure Doctrine mappings for persistence
设置框架相关配置:
  1. 配置Symfony依赖注入:将接口绑定到具体实现
  2. 创建测试替身:(内存仓库)用于无需数据库的单元测试
  3. 配置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
undefined
yaml
undefined

config/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\InMemoryUserRepository
undefined
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
undefined

Example 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

最佳实践

  1. Dependency Rule: Dependencies only point inward - domain knows nothing of application or infrastructure
  2. Immutability: Value Objects MUST be immutable using
    readonly
    in PHP 8.1+ - never allow mutable state
  3. Rich Domain Models: Put business logic in entities with factory methods like
    create()
    - avoid anemic models
  4. Interface Segregation: Keep repository interfaces small and focused - do not create god interfaces
  5. Framework Independence: Domain and application layers MUST be testable without Symfony or Doctrine
  6. Validation at Construction: Validate in Value Objects at construction time - never allow invalid state
  7. Symfony Attributes: Use PHP 8 attributes for routing (
    #[Route]
    ), validation (
    #[Assert\]
    ), and DI
  8. Test Doubles: Always provide In-Memory implementations for repositories to enable fast unit tests
  9. Domain Events: Dispatch domain events to decouple side effects - do not call external services from entities
  10. XML/YAML Mappings: Use XML or YAML for Doctrine mappings instead of annotations in domain entities
  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映射,而非领域实体中的注解

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
    readonly
    in PHP 8.1+. Never allow mutable state in Value Objects.
  • 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