Agent skill
symfony-ddd
Symfony 7 with Hexagonal Architecture, Domain-Driven Design (DDD), and CQRS patterns. Use this skill when implementing backend features, creating entities, commands, queries, repositories, or working with bounded contexts.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/symfony-ddd
SKILL.md
Symfony DDD - Hexagonal Architecture Skill
This skill provides guidance for implementing features in the Family Plan backend using Hexagonal Architecture, DDD, and CQRS patterns.
Architecture Overview
The backend follows a strict layered architecture within each Bounded Context:
BoundedContext/
├── Domain/ # Core business logic (no dependencies)
│ ├── Entity/ # Domain entities
│ ├── ValueObject/ # Value objects
│ ├── Event/ # Domain events
│ ├── Repository/ # Repository interfaces
│ └── Exception/ # Domain exceptions
├── Application/ # Use cases (depends only on Domain)
│ ├── Command/ # Commands and handlers
│ ├── Query/ # Queries and handlers
│ └── Service/ # Application services
└── Infrastructure/ # External adapters (implements Domain interfaces)
├── Doctrine/ # Database repositories
├── Http/ # External API clients
└── Adapter/ # Other infrastructure adapters
Bounded Contexts in This Project
UserManagement- Authentication, user accounts, rolesTaskManagement- Tasks, templates, executions, approvalsPointsManagement- Points wallets, rewards systemTeamManagement- Team organization, member invitationsUserSettings- User preferencesNotifications- Email/SMS notificationsShared- Shared kernel (common value objects, events)
Implementation Patterns
1. Creating an Entity
declare(strict_types=1);
namespace App\TaskManagement\Domain\Entity;
use App\Shared\Domain\ValueObject\Uuid;
use App\TaskManagement\Domain\Event\TaskCreated;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'tasks')]
final class Task
{
private array $domainEvents = [];
private function __construct(
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private Uuid $id,
#[ORM\Column(type: 'string', length: 255)]
private string $name,
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt
) {}
public static function create(Uuid $id, string $name): self
{
$task = new self($id, $name, new \DateTimeImmutable());
$task->recordEvent(new TaskCreated($id));
return $task;
}
public function id(): Uuid
{
return $this->id;
}
public function name(): string
{
return $this->name;
}
private function recordEvent(object $event): void
{
$this->domainEvents[] = $event;
}
public function pullDomainEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
2. Creating a Command and Handler
// Command
declare(strict_types=1);
namespace App\TaskManagement\Application\Command;
final readonly class CreateTaskCommand
{
public function __construct(
public string $id,
public string $name,
public string $teamId
) {}
}
// Handler
declare(strict_types=1);
namespace App\TaskManagement\Application\Command;
use App\Shared\Domain\ValueObject\Uuid;
use App\TaskManagement\Domain\Entity\Task;
use App\TaskManagement\Domain\Repository\TaskRepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final readonly class CreateTaskHandler
{
public function __construct(
private TaskRepositoryInterface $taskRepository
) {}
public function __invoke(CreateTaskCommand $command): void
{
$task = Task::create(
Uuid::fromString($command->id),
$command->name
);
$this->taskRepository->save($task);
}
}
3. Creating a Query and Handler
// Query
declare(strict_types=1);
namespace App\TaskManagement\Application\Query;
final readonly class GetTaskQuery
{
public function __construct(
public string $taskId
) {}
}
// Handler
declare(strict_types=1);
namespace App\TaskManagement\Application\Query;
use App\TaskManagement\Domain\Repository\TaskRepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final readonly class GetTaskHandler
{
public function __construct(
private TaskRepositoryInterface $taskRepository
) {}
public function __invoke(GetTaskQuery $query): ?TaskDTO
{
$task = $this->taskRepository->findById(
Uuid::fromString($query->taskId)
);
return $task ? TaskDTO::fromEntity($task) : null;
}
}
4. Repository Interface and Implementation
// Interface (Domain layer)
declare(strict_types=1);
namespace App\TaskManagement\Domain\Repository;
use App\Shared\Domain\ValueObject\Uuid;
use App\TaskManagement\Domain\Entity\Task;
interface TaskRepositoryInterface
{
public function save(Task $task): void;
public function findById(Uuid $id): ?Task;
public function findByTeamId(Uuid $teamId): array;
}
// Implementation (Infrastructure layer)
declare(strict_types=1);
namespace App\TaskManagement\Infrastructure\Doctrine;
use App\Shared\Domain\ValueObject\Uuid;
use App\TaskManagement\Domain\Entity\Task;
use App\TaskManagement\Domain\Repository\TaskRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
final class DoctrineTaskRepository extends ServiceEntityRepository implements TaskRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Task::class);
}
public function save(Task $task): void
{
$this->getEntityManager()->persist($task);
$this->getEntityManager()->flush();
}
public function findById(Uuid $id): ?Task
{
return $this->find($id->toString());
}
public function findByTeamId(Uuid $teamId): array
{
return $this->findBy(['teamId' => $teamId->toString()]);
}
}
5. Value Object
declare(strict_types=1);
namespace App\Shared\Domain\ValueObject;
use Symfony\Component\Uid\Uuid as SymfonyUuid;
final readonly class Uuid
{
private function __construct(
private string $value
) {}
public static function generate(): self
{
return new self(SymfonyUuid::v4()->toString());
}
public static function fromString(string $value): self
{
if (!SymfonyUuid::isValid($value)) {
throw new \InvalidArgumentException('Invalid UUID format');
}
return new self($value);
}
public function toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
Dependency Injection Configuration
Register repository bindings in config/services.yaml:
services:
App\TaskManagement\Domain\Repository\TaskRepositoryInterface:
class: App\TaskManagement\Infrastructure\Doctrine\DoctrineTaskRepository
CQRS Bus Usage
// In Controller
use Symfony\Component\Messenger\MessageBusInterface;
#[Route('/api/tasks', methods: ['POST'])]
public function create(
Request $request,
MessageBusInterface $commandBus
): JsonResponse {
$data = json_decode($request->getContent(), true);
$commandBus->dispatch(new CreateTaskCommand(
id: Uuid::generate()->toString(),
name: $data['name'],
teamId: $data['teamId']
));
return new JsonResponse(['status' => 'created'], 201);
}
Key Principles
- Domain Layer is Framework-Agnostic - No Symfony dependencies in Domain
- Always Use Interfaces - Repository interfaces in Domain, implementations in Infrastructure
- Named Constructors - Use static factory methods instead of public constructors
- Immutable Value Objects - Use
readonlyfor value objects - Rich Domain Models - Business logic belongs in entities, not services
- Domain Events - Record events in entities for side effects
Common Mistakes to Avoid
- Putting business logic in controllers or handlers
- Using Doctrine annotations/attributes in Domain layer
- Creating anemic domain models (entities with only getters/setters)
- Skipping the interface for repositories
- Mixing bounded contexts directly (use domain events instead)
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?