Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Hexagonal Maker Bundle Laravel Package

ahmed-bhs/hexagonal-maker-bundle

View on GitHub
Deep Wiki
Context7

layout: default

Architecture Hexagonale - Guide Complet

Table des matières

  1. Vue d'ensemble
  2. Les 3 Couches
  3. Ports vs Adapters
  4. CQRS Pattern
  5. Testabilité
  6. Structure de répertoires
  7. Bonnes pratiques
  8. Design Patterns Favorisés
  9. Migration progressive
  10. Ressources

1. Vue d'ensemble

L'architecture hexagonale (aussi appelée Ports and Adapters) est un pattern architectural qui vise à isoler la logique métier des préoccupations techniques (framework, base de données, API externes, etc.).

1.1 Principe fondamental

Les dépendances pointent toujours vers l'intérieur (vers le domaine)

%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'16px'}}}%%
graph LR
    Infra["🔌 Infrastructure<br/><small>Adapters</small>"]
    App["⚙️ Application<br/><small>Use Cases</small>"]
    Domain["💎 Domain<br/><small>Business Logic</small>"]

    Infra ==>|"depends on"| App
    App ==>|"depends on"| Domain

    style Domain fill:#C8E6C9,stroke:#2E7D32,stroke-width:4px,color:#000
    style App fill:#B3E5FC,stroke:#0277BD,stroke-width:3px,color:#000
    style Infra fill:#F8BBD0,stroke:#C2185B,stroke-width:3px,color:#000

2. Les 3 Couches

2.1 Domain (Cœur - Hexagone)

Responsabilité: Logique métier pure, règles de gestion, invariants

Contient:

  • Model/ - Entités avec identité et cycle de vie
  • ValueObject/ - Objets immuables définis par leurs valeurs
  • Port/In/ - Interfaces des ports primaires (driving) - ce que l'application offre
  • Port/Out/ - Interfaces des ports secondaires (driven) - ce dont l'application a besoin

Règles strictes:

  • AUCUNE dépendance vers les couches externes
  • AUCUNE annotation/attribut Symfony/Doctrine
  • PHP pur uniquement
  • Indépendant du framework

Exemple Entity:

namespace App\User\Account\Domain\Model;

final class User
{
    public function __construct(
        private UserId $id,
        private Email $email,
        private bool $isActive = false,
    ) {
    }

    // Business logic
    public function activate(): void
    {
        if ($this->isActive) {
            throw new UserAlreadyActiveException();
        }
        $this->isActive = true;
    }
}

Exemple Value Object:

namespace App\User\Account\Domain\ValueObject;

final readonly class Email
{
    public function __construct(public string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException($value);
        }
    }
}

Exemple Port Out (Interface - Driven/Secondary):

namespace App\User\Account\Domain\Port\Out;

/**
 * Output Port - Defines what the application NEEDS from infrastructure
 * Implemented by adapters in Infrastructure layer (e.g., DoctrineUserRepository)
 */
interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(UserId $id): ?User;
}

Exemple Port In (Interface - Driving/Primary):

namespace App\User\Account\Domain\Port\In;

/**
 * Input Port - Defines what the application OFFERS to the outside world
 * Implemented by use cases in Application layer
 */
interface CreateUserUseCaseInterface
{
    public function execute(CreateUserCommand $command): void;

2.2 Application (Orchestration)

Responsabilité: Cas d'utilisation, orchestration des opérations métier

Contient:

  • Command/ - Commandes CQRS (écritures)
  • Query/ - Requêtes CQRS (lectures)
  • Handlers - Logique d'orchestration

Règles:

  • Dépend du Domain uniquement
  • Utilise les Ports (interfaces)
  • Coordonne les opérations
  • Ne contient PAS de logique métier

Command:

namespace App\User\Account\Application\Register;

final readonly class RegisterCommand
{
    public function __construct(
        public string $email,
        public string $password,
    ) {
    }
}

Command Handler:

namespace App\User\Account\Application\Register;

use App\User\Account\Domain\Port\UserRepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final readonly class RegisterCommandHandler
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private PasswordHasherInterface $hasher,
    ) {
    }

    public function __invoke(RegisterCommand $command): void
    {
        // Orchestration only, no business logic
        $user = new User(
            id: UserId::generate(),
            email: new Email($command->email),
            password: $this->hasher->hash($command->password),
        );

        $this->repository->save($user);
    }
}

2.3 Infrastructure (Détails techniques)

Responsabilité: Implémentations concrètes, détails techniques

Contient:

  • Persistence/ - Adapters pour la persistance (Doctrine, etc.)
  • Messaging/ - Adapters pour la messagerie
  • ExternalAPI/ - Adapters pour les APIs externes

Règles:

  • Implémente les Ports (interfaces du Domain)
  • Contient les détails techniques
  • Peut dépendre de Domain et Application
  • Utilise Doctrine, HTTP clients, etc.

Adapter Doctrine:

namespace App\User\Account\Infrastructure\Persistence\Doctrine;

use App\User\Account\Domain\Model\User;
use App\User\Account\Domain\Port\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;

final class DoctrineUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private readonly EntityManagerInterface $em,
    ) {
    }

    public function save(User $user): void
    {
        $this->em->persist($user);
        $this->em->flush();
    }

    public function findById(UserId $id): ?User
    {
        return $this->em->find(User::class, $id->value);
    }
}

3. Ports vs Adapters

%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'15px'}}}%%
graph LR
    subgraph Primary["🎮 Primary Adapters (Driving)"]
        HTTP["🌐 HTTP Controller"]
        CLI["⌨️ CLI Command"]
        GraphQL["📊 GraphQL Resolver"]
    end

    subgraph Core["💎 Core - Domain + Application"]
        UseCases["⚙️ Use Cases<br/><small>Handlers</small>"]
        PortsIn["🔗 Port/In<br/><small>Use Case Interfaces</small>"]
        PortsOut["🔗 Port/Out<br/><small>Repository Interfaces</small>"]
    end

    subgraph Secondary["🔌 Secondary Adapters (Driven)"]
        Doctrine["🗄️ Doctrine<br/><small>Repository</small>"]
        Redis["⚡ Redis<br/><small>Cache</small>"]
        SMTP["📧 SMTP<br/><small>Mailer</small>"]
    end

    Primary ==>|"calls"| PortsIn
    PortsIn -.->|"implemented by"| UseCases
    UseCases ==>|"uses"| PortsOut
    Secondary -.->|"🎯 implements"| PortsOut

    style Core fill:#C8E6C9,stroke:#2E7D32,stroke-width:4px,color:#000
    style Primary fill:#E1BEE7,stroke:#6A1B9A,stroke-width:3px,color:#000
    style Secondary fill:#F8BBD0,stroke:#C2185B,stroke-width:3px,color:#000

3.1 Port (Interface)

Un Port est une interface définie dans le Domain qui représente un contrat.

Types de Ports:

  1. Ports In (Driving/Primary) - Ce que l'application offre (Domain/Port/In/)

    • Exemple : CreateUserUseCaseInterface, RegisterUserUseCaseInterface
    • Implémentés par les Use Cases dans la couche Application
    • Appelés par les adapters primaires (Controllers, CLI)
  2. Ports Out (Driven/Secondary) - Ce dont l'application a besoin (Domain/Port/Out/)

    • Exemple : UserRepositoryInterface, EmailSenderInterface
    • Implémentés par les adapters secondaires (Infrastructure)

3.2 Adapter (Implémentation)

Un Adapter est une implémentation concrète d'un Port dans l'Infrastructure.

3.3 Exemples Port/Out (Besoins métier)

Port Out Besoin métier Implémentations
PricingServiceInterface Calculer prix final (promos, B2B) StripePricing, CustomEngine
TaxCalculatorInterface Calculer TVA selon pays/produit TaxJarAPI, InternalTaxRules
InventoryCheckerInterface Vérifier disponibilité stock WarehouseAPI, ERPConnector
FraudDetectionInterface Détecter commandes suspectes SiftScience, InternalRules
ShippingCostCalculatorInterface Calculer frais de port Colissimo, UPS, FedEx
LoyaltyPointsServiceInterface Gérer points fidélité ZendeskLoyalty, InternalSystem

3.4 Exemples Port/In (Capacités métier)

Port In Cas d'usage métier Appelé par
PlaceOrderUseCaseInterface Passer une commande Web, Mobile, API
ApplyDiscountUseCaseInterface Appliquer code promo Checkout, Support
RequestRefundUseCaseInterface Demander remboursement Client, Support
CancelSubscriptionUseCaseInterface Résilier abonnement Espace client

3.5 Exemples d'Adapters

// Port Out (Domain/Port/Out/)
namespace App\User\Account\Domain\Port\Out;

interface UserRepositoryInterface
{
    public function save(User $user): void;
}

// Adapter 1 - Doctrine (Infrastructure)
namespace App\User\Account\Infrastructure\Persistence\Doctrine;

class DoctrineUserRepository implements UserRepositoryInterface
{
    public function save(User $user): void
    {
        $this->em->persist($user);
        $this->em->flush();
    }
}

// Adapter 2 - In Memory (Infrastructure/Tests)
class InMemoryUserRepository implements UserRepositoryInterface
{
    private array $users = [];

    public function save(User $user): void
    {
        $this->users[$user->getId()->value] = $user;
    }
}

3.6 Quiz rapide : Port In ou Port Out ?

Interface Réponse
RegisterUserUseCaseInterface In - Ce que l'app offre
UserRepositoryInterface Out - L'app a besoin de persistence
PaymentGatewayInterface Out - L'app a besoin du paiement
PlaceOrderUseCaseInterface In - Ce que l'app offre
TaxCalculatorInterface Out - L'app a besoin du calcul TVA
EmailSenderInterface Out - L'app a besoin d'envoyer des emails

Règle simple :

  • In = *UseCaseInterface → Implémenté par Application
  • Out = *RepositoryInterface, *ServiceInterface → Implémenté par Infrastructure

---

## 4. CQRS Pattern

```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
graph TB
    UI["🎮 UI Layer<br/><small>Controller/CLI</small>"]

    subgraph Write["✍️ Write Side - Commands"]
        CMD["📝 Command<br/><small>RegisterUserCommand</small>"]
        CMDH["⚙️ Command Handler<br/><small>RegisterUserCommandHandler</small>"]
        WriteRepo["💾 Write Repository<br/><small>Save/Update/Delete</small>"]
    end

    subgraph Read["📖 Read Side - Queries"]
        QRY["🔍 Query<br/><small>FindUserQuery</small>"]
        QRYH["⚙️ Query Handler<br/><small>FindUserQueryHandler</small>"]
        ReadRepo["📚 Read Repository<br/><small>Find/List/Search</small>"]
        RESP["📋 Response<br/><small>FindUserResponse</small>"]
    end

    UI ==>|"dispatch"| CMD
    UI ==>|"dispatch"| QRY

    CMD ==> CMDH
    CMDH ==>|"uses"| WriteRepo

    QRY ==> QRYH
    QRYH ==>|"uses"| ReadRepo
    QRYH ==>|"returns"| RESP

    style Write fill:#FFCDD2,stroke:#C62828,stroke-width:3px,color:#000
    style Read fill:#B3E5FC,stroke:#0277BD,stroke-width:3px,color:#000
    style UI fill:#E1BEE7,stroke:#6A1B9A,stroke-width:3px,color:#000

4.1 Command (Écriture)

Caractéristiques:

  • Intention de modifier l'état
  • Retourne void
  • Nom au présent : RegisterUser, PublishArticle
final readonly class PublishArticleCommand
{
    public function __construct(
        public string $articleId,
        public \DateTimeImmutable $publishedAt,
    ) {
    }
}

4.2 Query (Lecture)

Caractéristiques:

  • Intention de lire des données
  • Retourne un Response
  • Nom descriptif : FindUserById, ListArticles
final readonly class FindUserByIdQuery
{
    public function __construct(
        public string $userId,
    ) {
    }
}

final readonly class FindUserByIdResponse
{
    public function __construct(
        public string $id,
        public string $email,
        public bool $isActive,
    ) {
    }
}

4.3 Séparation stricte

// MAUVAIS - Retourne une valeur
class RegisterUserCommand
{
    public function __invoke(RegisterCommand $cmd): User { ... }
}

// BON - Void uniquement
class RegisterUserCommandHandler
{
    public function __invoke(RegisterCommand $cmd): void { ... }
}

// BON - Query retourne les données
class FindUserQueryHandler
{
    public function __invoke(FindUserQuery $q): FindUserResponse { ... }
}

5. Testabilité

L'architecture hexagonale facilite grandement les tests :

graph TB
    subgraph Pyramid["Pyramide des Tests"]
        E2E[Tests E2E<br/>Lents - Peu nombreux<br/>Full stack avec DB]
        Integration[Tests d'Intégration<br/>Moyens - Modérés<br/>Avec Symfony Kernel]
        Unit[Tests Unitaires<br/>Rapides - Nombreux<br/>InMemory - Mocks]
    end

    subgraph Layers["Couches Testées"]
        UnitTests[Domain + Application<br/>Tests unitaires<br/>InMemoryRepository]
        IntTests[Handlers + Adapters<br/>Tests d'intégration<br/>DoctrineRepository]
        E2ETests[UI → DB<br/>Tests E2E<br/>Parcours complet]
    end

    Unit -.->|teste| UnitTests
    Integration -.->|teste| IntTests
    E2E -.->|teste| E2ETests

    style Unit fill:#90EE90,stroke:#333,stroke-width:2px
    style Integration fill:#87CEEB,stroke:#333,stroke-width:2px
    style E2E fill:#FFB6C1,stroke:#333,stroke-width:2px

5.1 Test du Domain (ultra rapide)

class UserTest extends TestCase
{
    public function testUserCanBeActivated(): void
    {
        $user = new User(
            id: UserId::generate(),
            email: new Email('test@example.com'),
        );

        $user->activate();

        $this->assertTrue($user->isActive());
    }
}

5.2 Test de l'Application avec InMemory

class RegisterCommandHandlerTest extends TestCase
{
    public function testUserIsRegistered(): void
    {
        $repository = new InMemoryUserRepository();
        $handler = new RegisterCommandHandler($repository);

        $command = new RegisterCommand(
            email: 'test@example.com',
            password: 'secret',
        );

        $handler($command);

        $this->assertCount(1, $repository->all());
    }
}

6. Structure de répertoires

src/
└── User/                          # Bounded Context
    └── Account/                   # Module
        ├── Application/           # Couche Application
        │   ├── Register/
        │   │   ├── RegisterCommand.php
        │   │   ├── RegisterCommandHandler.php
        │   │   └── AccountFactory.php
        │   └── Find/
        │       ├── FindQuery.php
        │       ├── FindQueryHandler.php
        │       └── FindResponse.php
        │
        ├── Domain/                # Couche Domain (Cœur)
        │   ├── Model/
        │   │   └── User.php
        │   ├── ValueObject/
        │   │   ├── Email.php
        │   │   └── UserId.php
        │   └── Port/
        │       ├── In/            # Input/Driving Ports (Primary)
        │       │   └── RegisterUserUseCaseInterface.php
        │       └── Out/           # Output/Driven Ports (Secondary)
        │           └── UserRepositoryInterface.php
        │
        └── Infrastructure/        # Couche Infrastructure
            ├── Persistence/
            │   ├── Doctrine/
            │   │   ├── DoctrineUserRepository.php
            │   │   └── Mapping/
            │   │       └── User.orm.xml
            │   └── InMemory/
            │       └── InMemoryUserRepository.php
            └── Messaging/
                └── SymfonyMessengerAdapter.php

7. Bonnes pratiques

7.1 Domain pur

// MAUVAIS - Dépendance à Doctrine
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User { }

// BON - PHP pur
class User
{
    public function __construct(
        private UserId $id,
        private Email $email,
    ) {
    }
}

7.2 Value Objects immuables

// MAUVAIS - Mutable
class Email
{
    public string $value;

    public function setValue(string $value): void
    {
        $this->value = $value;
    }
}

// BON - Immutable avec readonly
final readonly class Email
{
    public function __construct(
        public string $value,
    ) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException();
        }
    }
}

7.3 Ports dans le Domain

// MAUVAIS - Adapter dans le Domain
namespace App\Domain;

use Doctrine\ORM\EntityManagerInterface;

class UserService
{
    public function __construct(
        private EntityManagerInterface $em
    ) {}
}

// BON - Port (interface) dans le Domain
namespace App\Domain\Port;

interface UserRepositoryInterface
{
    public function save(User $user): void;
}

namespace App\Application;

class RegisterHandler
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}
}

7.4 Factories pour création complexe

final readonly class OrderFactory
{
    public function __construct(
        private IdGeneratorInterface $idGenerator,
        private ClockInterface $clock,
    ) {
    }

    public function create(CreateOrderCommand $command): Order
    {
        return new Order(
            id: new OrderId($this->idGenerator->generate()),
            customerId: new CustomerId($command->customerId),
            items: $this->createOrderItems($command->items),
            createdAt: $this->clock->now(),
        );
    }
}

8. Design Patterns Favorisés

L'architecture hexagonale encourage naturellement l'utilisation de nombreux design patterns éprouvés. Cette section explore comment l'hexagonal facilite et favorise ces patterns.

graph TB
    subgraph Creational["Patterns de Création (Creational)"]
        Factory[Factory Pattern]
        Builder[Builder Pattern]
        Singleton[Singleton Pattern]
    end

    subgraph Structural["Patterns Structuraux (Structural)"]
        Adapter[Adapter Pattern]
        Repository[Repository Pattern]
        DTO[DTO Pattern]
    end

    subgraph Behavioral["Patterns Comportementaux (Behavioral)"]
        Strategy[Strategy Pattern]
        Observer[Observer Pattern]
        Command[Command Pattern]
    end

    subgraph Hexagonal["Architecture Hexagonale"]
        Domain[Domain Layer]
        App[Application Layer]
        Infra[Infrastructure Layer]
    end

    Factory -.->|crée| Domain
    Builder -.->|construit| Domain
    Adapter -.->|implémente ports| Infra
    Repository -.->|abstrait persistance| Domain
    DTO -.->|transfère données| App
    Strategy -.->|sélection implémentation| Infra
    Command -.->|encapsule intention| App

    style Creational fill:#FFD700,stroke:#333,stroke-width:2px
    style Structural fill:#87CEEB,stroke:#333,stroke-width:2px
    style Behavioral fill:#FFB6C1,stroke:#333,stroke-width:2px
    style Hexagonal fill:#90EE90,stroke:#333,stroke-width:2px

8.1 Patterns de Création (Creational Patterns)

8.1.1 Factory Pattern

Pourquoi l'hexagonal le favorise:

  • La création d'entités complexes nécessite souvent plusieurs Value Objects
  • Validation et logique métier doivent être centralisées
  • Le Domain ne doit pas dépendre de l'Infrastructure

Exemple:

<?php

declare(strict_types=1);

namespace App\User\Account\Application\Register;

use App\User\Account\Domain\Model\User;
use App\User\Account\Domain\ValueObject\Email;
use App\User\Account\Domain\ValueObject\UserId;
use App\User\Account\Domain\ValueObject\HashedPassword;
use App\Shared\Domain\Service\IdGeneratorInterface;
use App\Shared\Domain\Service\PasswordHasherInterface;

/**
 * Factory Pattern - Crée des entités complexes du Domain
 */
final readonly class UserFactory
{
    public function __construct(
        private IdGeneratorInterface $idGenerator,
        private PasswordHasherInterface $passwordHasher,
    ) {
    }

    public function createFromCommand(RegisterCommand $command): User
    {
        return new User(
            id: new UserId($this->idGenerator->generate()),
            email: new Email($command->email),
            password: new HashedPassword(
                $this->passwordHasher->hash($command->password)
            ),
            createdAt: new \DateTimeImmutable(),
        );
    }
}

Avantages dans l'hexagonal:

  • Encapsule la logique de création complexe
  • Isole les dépendances (ID generator, hasher) de l'entité
  • Facilite les tests (mock de la factory)

8.1.2 Builder Pattern

Utilisation:

  • Construction progressive d'objets complexes
  • Configurations avec nombreuses options

Exemple:

<?php

declare(strict_types=1);

namespace App\Order\Domain\Builder;

use App\Order\Domain\Model\Order;
use App\Order\Domain\ValueObject\OrderId;
use App\Order\Domain\ValueObject\OrderItem;

/**
 * Builder Pattern - Construction progressive d'une commande
 */
final class OrderBuilder
{
    private ?OrderId $id = null;
    private ?string $customerId = null;
    private array $items = [];
    private...
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky