Provides implementation patterns for Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design in PHP 8.3+ with Symfony 7.x. Use when architecting enterprise PHP applications with entities/value objects/aggregates, refactoring legacy code to modern patterns, implementing domain-driven design with Symfony, or creating testable backends with clear separation of concerns.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill clean-architecture
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.
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):
UserRepositoryInterface)DDD Tactical Patterns:
User, Order)Email, Money)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
Start from the innermost layer (Domain) and work outward:
readonlyBuild use cases that orchestrate domain objects:
Create interface adapters that connect Application to Infrastructure:
Set up framework-specific configuration:
Ensure Domain and Application layers are testable without Symfony, Doctrine, or database. Use In-Memory repositories for fast unit tests.
<?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
// 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
// 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
// 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);
}
}
<?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
// 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
// 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();
}
}
}
# 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
<?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()]);
}
}
readonly in PHP 8.1+ - never allow mutable statecreate() - avoid anemic models#[Route]), validation (#[Assert\]), and DI10. XML/YAML Mappings: Use XML or YAML for Doctrine mappings instead of annotations in domain entities
readonly in PHP 8.1+. Never allow mutable state in Value Objects.#[Route], #[Assert\Email], #[Autowire]).Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping
Comprehensive GitHub code review with AI-powered swarm coordination
Create high-quality git commits: review/stage intended changes, split into logical commits, and write clear commit messages (including Conventional Commits). Use when the user asks to commit, craft a commit message, stage changes, or split work into multiple commits.
Comprehensive truth scoring, code quality verification, and automatic rollback system with 0.95 accuracy threshold for ensuring high-quality agent outputs and codebase reliability.
GitHub CLI (gh) comprehensive reference for repositories, issues, pull requests, Actions, projects, releases, gists, codespaces, organizations, extensions, and all GitHub operations from the command line.
GitHub CLI - manage repositories, issues, pull requests, actions, releases, and more from the command line.
You are a code refactoring expert specializing in clean code principles, SOLID design patterns, and modern software engineering best practices. Analyze and refactor the provided code to improve its quality, maintainability, and performance.
You are a technical debt expert specializing in identifying, quantifying, and prioritizing technical debt in software projects. Analyze the codebase to uncover debt, assess its impact, and create acti
Take giuseppe-trisciuoglio/developer-kit-clean-architecture from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.