https://github.com/ahmed-bhs/hexagonal-maker-bundle
A complete Symfony Maker bundle for generating Hexagonal Architecture (Ports & Adapters) components with CQRS, pure domain entities, and YAML mapping
https://github.com/ahmed-bhs/hexagonal-maker-bundle
clean-archit code-generator command cqrs hexa hexagonal-architecture maker-bundle patterns php ports-adapters pure-domain query solid symfony symfony-bundle
Last synced: 6 months ago
JSON representation
A complete Symfony Maker bundle for generating Hexagonal Architecture (Ports & Adapters) components with CQRS, pure domain entities, and YAML mapping
- Host: GitHub
- URL: https://github.com/ahmed-bhs/hexagonal-maker-bundle
- Owner: ahmed-bhs
- License: mit
- Created: 2026-01-05T19:42:46.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-01-09T14:05:13.000Z (6 months ago)
- Last Synced: 2026-01-11T17:44:52.404Z (6 months ago)
- Topics: clean-archit, code-generator, command, cqrs, hexa, hexagonal-architecture, maker-bundle, patterns, php, ports-adapters, pure-domain, query, solid, symfony, symfony-bundle
- Language: PHP
- Homepage:
- Size: 1.1 MB
- Stars: 6
- Watchers: 3
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG-AUTONOMIE.md
- Contributing: docs/contributing/development.md
- License: LICENSE
- Roadmap: ROADMAP-AUTONOMIE.md
Awesome Lists containing this project
README
# Hexagonal Architecture Maker Bundle for Symfony
A complete Symfony Maker bundle for generating Hexagonal Architecture (Ports & Adapters) components
### โ Support This Project
If this project helped you or saved you time, consider buying me a coffee!
[](https://buymeacoffee.com/w6ZhBSGX2)
*Your support helps maintain this project and create more learning resources!* โค๏ธ
โจ 19 maker commands | ๐ Pure Domain | ๐ฏ CQRS Pattern | ๐๏ธ Full Layer Coverage | ๐ Async/Queue Support
---
## Table of Contents
- [Quick Start](#quick-start)
- [1. Features](#1-features)
- [2. Why Hexagonal Architecture](#2-why-hexagonal-architecture) โ [๐ Complete Guide](WHY-HEXAGONAL.md)
- [3. Installation](#3-installation)
- [4. Complete Architecture Generation](#4-complete-architecture-generation)
- [5. Available Makers (18 Commands)](#5-available-makers)
- [6. Configuration](#6-configuration)
- [7. Best Practices](#7-best-practices)
- [8. Additional Resources](#8-additional-resources)
- [9. License](#9-license)
---
## Quick Start
```bash
# 1. Install
composer require ahmedbhs/hexagonal-maker-bundle --dev
# 2. Generate a complete module (User Registration example)
bin/console make:hexagonal:entity user/account User
bin/console make:hexagonal:exception user/account InvalidEmailException
bin/console make:hexagonal:value-object user/account Email
bin/console make:hexagonal:repository user/account User
bin/console make:hexagonal:command user/account register --factory
bin/console make:hexagonal:controller user/account CreateUser /users/register
bin/console make:hexagonal:form user/account User
# 3. Configure Doctrine ORM mapping (see section 7.3)
# 4. Start coding your business logic!
```
**Result:** Complete hexagonal architecture with pure domain, separated layers, and ready-to-use components! ๐
---
## 1. Features
### 1.1 Core CQRS Components
- **Commands** - Write operations that modify state (e.g., `CreateUserCommand`) with their handlers (e.g., `CreateUserCommandHandler`) decorated with `#[AsMessageHandler]` for business logic execution
- **Queries** - Read operations that retrieve data (e.g., `FindUserQuery`) with their handlers (e.g., `FindUserQueryHandler`) decorated with `#[AsMessageHandler]` and response DTOs (e.g., `FindUserResponse`)
### 1.2 Complete Maker Commands Summary
**18 makers covering all hexagonal layers + tests + events + rapid CRUD:**
| Layer | Maker Command | What it generates |
|-------|--------------|-------------------|
| **Domain** | `make:hexagonal:entity` | Domain entities + YAML mapping |
| **Domain** | `make:hexagonal:value-object` | Immutable value objects |
| **Domain** | `make:hexagonal:exception` | Business rule exceptions |
| **Domain** | `make:hexagonal:domain-event` | Domain events |
| **Application** | `make:hexagonal:command` | CQRS commands + handlers |
| **Application** | `make:hexagonal:query` | CQRS queries + handlers + responses |
| **Application** | `make:hexagonal:repository` | Repository port + Doctrine adapter |
| **Application** | `make:hexagonal:input` | Input DTOs with validation |
| **Application** | `make:hexagonal:use-case` | Use cases |
| **Application/Infrastructure** | `make:hexagonal:event-subscriber` | Event subscribers |
| **Infrastructure** | `make:hexagonal:message-handler` | Async message handlers |
| **UI** | `make:hexagonal:controller` | Web controllers |
| **UI** | `make:hexagonal:form` | Symfony forms |
| **UI** | `make:hexagonal:cli-command` | Console commands |
| **Tests** | `make:hexagonal:use-case-test` | Use case tests (KernelTestCase) |
| **Tests** | `make:hexagonal:controller-test` | Controller tests (WebTestCase) |
| **Tests** | `make:hexagonal:cli-command-test` | CLI tests (CommandTester) |
| **Config** | `make:hexagonal:test-config` | Test configuration setup |
| **Rapid Dev** | `make:hexagonal:crud` | Complete CRUD (Entity + 5 UseCases + Controllers + Forms + Tests) |
---
## 2. Why Hexagonal Architecture
> **๐ [Read the complete guide: WHY-HEXAGONAL.md](WHY-HEXAGONAL.md)**
### 2.1 What the Founders Say
#### Alistair Cockburn - Creator of Hexagonal Architecture (2005)
> *"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases."*
>
> โ Alistair Cockburn, [Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/)
**On the core principle:**
> *"The hexagon is intended to visually highlight the following:*
> - *(a) There is an inside and an outside to the application*
> - *(b) The number of ports is not two, but many (and variable)*
> - *(c) The number of adapters for any particular port is not one, but many (and variable)*"*
**On dependencies:**
> *"Create your application to work without either a UI or a database so you can run automated regression-tests against the application, work when the database becomes unavailable, and link applications together without any user involvement."*
#### Robert C. Martin (Uncle Bob) - Creator of Clean Architecture (2012)
**On the business logic:**
> *"The business rules are the heart of the software. They carry the code that makes, or saves, money. They are the family jewels. We want to protect them from all forms of complexity and change."*
>
> โ Robert C. Martin, [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
**On frameworks:**
> *"Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases."*
**On the dependency rule:**
> *"Source code dependencies must point only inward, toward higher-level policies. Nothing in an inner circle can know anything at all about something in an outer circle."*
**On volatility:**
> *"The less volatile things are, the more they should be depended upon. Business rules change less frequently than technical details, so technical details should depend on business rules, not the other way around."*
#### Eric Evans - Domain-Driven Design (2003)
**On isolating the domain:**
> *"The heart of software is its ability to solve domain-related problems for its user. All other features, vital though they may be, support this basic purpose."*
>
> โ Eric Evans, [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.domainlanguage.com/ddd/)
**On the domain model:**
> *"When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as a standalone interface declared as a SERVICE."*
#### Jeffrey Palermo - Onion Architecture (2008)
**On dependency direction:**
> *"The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core. In other words, all coupling is toward the center."*
>
> โ Jeffrey Palermo, [The Onion Architecture](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/)
**On persistence ignorance:**
> *"The application core doesn't know anything about how data is persisted or where data comes from. It defines interfaces for these concerns, and the outer layers implement these interfaces."*
### 2.2 Key Principles from the Masters
| Principle | Author | Meaning |
|-----------|--------|---------|
| **Dependency Inversion** | Uncle Bob | High-level modules should not depend on low-level modules. Both should depend on abstractions. |
| **Ports & Adapters** | Alistair Cockburn | The core defines ports (interfaces), the outside world provides adapters (implementations). |
| **Screaming Architecture** | Uncle Bob | Your architecture should scream what the application does, not what framework it uses. |
| **Ubiquitous Language** | Eric Evans | The code should speak the language of the domain experts, not technical jargon. |
| **Isolation** | All | Business logic must be isolated from technical concerns (UI, DB, frameworks). |
### Quick Summary
**Everything is coupled anyway, so why bother?**
Hexagonal architecture isn't about eliminating couplingโthat's impossible. It's about **controlling the direction** of coupling.
### The Core Problem with Traditional Architecture
**Traditional layered architecture problems:**
- โ๏ธ Framework Prison: Business logic tightly coupled to Doctrine/Symfony
- ๐ข Testing Complexity: Every test requires database, 10 min vs 10 sec
- ๐ช๏ธ Lost Business Rules: Rules scattered across 10+ files
- ๐งฑ Cannot Evolve: Adding GraphQL/CLI requires code duplication
- ๐ Cost Predictability: Simple features take 3x longer after 2 years
**Hexagonal architecture solution:**
- **๐ Pure Domain Isolation:** Your business logic lives in pure PHP, zero framework dependencies. Why? Because frameworks become obsolete, but your business rules don't. Isolated domain = no technical debt accumulation, easier to understand (speaks business language, not technical jargon), and survives all technology changes. The secret: Dependency Inversion - the domain defines interfaces (Ports), infrastructure adapts to them
- **๐ฏ Direction Control:** Business logic depends on abstractions, infrastructure depends on business
- **โก Testing Speed:** 1000x faster (in-memory vs database I/O) - 10 min โ 10 sec
- **๐ Technology Freedom:** Swap MySQL to MongoDB in days not months (10-20x effort saved)
- **๐ฐ Cost Predictability (The "5-Day Rule"):** Features cost consistent time, no technical debt tax
- **๐ Reusability:** Same business logic for REST, GraphQL, CLI, gRPC
- **๐๏ธ Craftsmanship Practices:** Promotes SOLID principles, DRY (Don't Repeat Yourself), YAGNI (You Aren't Gonna Need It), KISS (Keep It Simple, Stupid), Separation of Concerns (SoC), and design patterns like DTO, Strategy, Factory, Dependency Injection
**The Investment Analogy:**
- Traditional = Consumer credit: easy at start, debt strangles you later
- Hexagonal = Investment: pay upfront, every feature costs its real price forever
> **๐ Want to learn more?** [Read the complete guide with examples, analogies, and decision trees โ](WHY-HEXAGONAL.md)
---
## 3. Installation
```bash
composer require ahmedbhs/hexagonal-maker-bundle
```
The bundle will auto-register if you use Symfony Flex. Otherwise, add it to `config/bundles.php`:
```php
return [
// ...
AhmedBhs\HexagonalMakerBundle\HexagonalMakerBundle::class => ['dev' => true],
];
```
---
## 4. Complete Architecture Generation
This section shows exactly how to build a complete hexagonal architecture module step by step, with the exact commands to run for each component.
### 4.1 Scenario: User Account Management Module
Let's build a complete **User Account** module with all layers of hexagonal architecture.
#### 4.1.1 Step-by-Step Architecture Generation
```bash
# LAYER 1: DOMAIN (Core Business Logic - Pure PHP)
# ============================================
# 1.1 Create Domain Entity (User aggregate root - PURE, no Doctrine)
bin/console make:hexagonal:entity user/account User
# 1.2 Create Domain Exceptions (business rule violations)
bin/console make:hexagonal:exception user/account InvalidEmailException
bin/console make:hexagonal:exception user/account UserAlreadyExistsException
# 1.3 Create Value Objects (domain concepts)
bin/console make:hexagonal:value-object user/account UserId
bin/console make:hexagonal:value-object user/account Email
bin/console make:hexagonal:value-object user/account Password
# 1.4 Create Repository Port (interface in domain)
bin/console make:hexagonal:repository user/account User
# LAYER 2: APPLICATION (Use Cases & DTOs)
# ============================================
# 2.1 Create Input DTOs (with validation)
bin/console make:hexagonal:input user/account RegisterUserInput
# 2.2 Create Registration Use Case (Command)
bin/console make:hexagonal:command user/account register --factory
# 2.3 Create Activation Use Case (Command)
bin/console make:hexagonal:command user/account activate
# 2.4 Create Find User Use Case (Query)
bin/console make:hexagonal:query user/account find-by-id
# 2.5 Create List Users Use Case (Query)
bin/console make:hexagonal:query user/account list-all
# 2.6 Alternative: Create Use Case (instead of Command/Query)
bin/console make:hexagonal:use-case user/account RegisterUser
# LAYER 3: UI (Primary Adapters - Driving)
# ============================================
# 3.1 Create Web Controller
bin/console make:hexagonal:controller user/account RegisterUser /users/register
# 3.2 Create Symfony Form
bin/console make:hexagonal:form user/account User
# 3.3 Create CLI Command
bin/console make:hexagonal:cli-command user/account RegisterUser app:user:register
# LAYER 4: INFRASTRUCTURE (Secondary Adapters - Already generated!)
# ============================================
# The Repository adapter was auto-generated in step 1.4
# Located at: Infrastructure/Persistence/Doctrine/DoctrineUserRepository.php
# Doctrine YAML mapping auto-generated with entity in step 1.1
# Located at: Infrastructure/Persistence/Doctrine/Orm/Mapping/User.orm.yml
```
#### 4.1.2 Generated Architecture Structure
After running the commands above, here's your **complete hexagonal architecture**:
```
src/User/Account/
โ
โโโ Domain/ # ๐ CORE BUSINESS LOGIC (Pure PHP, ZERO framework deps)
โ โโโ Model/
โ โ โโโ User.php โ make:hexagonal:entity
โ โ
โ โโโ Exception/ โ NEW!
โ โ โโโ InvalidEmailException.php โ make:hexagonal:exception
โ โ โโโ UserAlreadyExistsException.php โ make:hexagonal:exception
โ โ
โ โโโ ValueObject/
โ โ โโโ UserId.php โ make:hexagonal:value-object
โ โ โโโ Email.php โ make:hexagonal:value-object
โ โ โโโ Password.php โ make:hexagonal:value-object
โ โ
โ โโโ Port/ # Interfaces (Ports)
โ โโโ UserRepositoryInterface.php โ make:hexagonal:repository
โ
โโโ Application/ # โ๏ธ USE CASES & DTOs
โ โโโ Input/ โ NEW!
โ โ โโโ RegisterUserInput.php โ make:hexagonal:input
โ โ
โ โโโ UseCase/ โ NEW!
โ โ โโโ RegisterUserUseCase.php โ make:hexagonal:use-case
โ โ
โ โโโ Register/ # CQRS Command
โ โ โโโ RegisterCommand.php โ make:hexagonal:command
โ โ โโโ RegisterCommandHandler.php โ (auto-generated)
โ โ โโโ AccountFactory.php โ (auto-generated with --factory)
โ โ
โ โโโ Activate/
โ โ โโโ ActivateCommand.php โ make:hexagonal:command
โ โ โโโ ActivateCommandHandler.php โ (auto-generated)
โ โ
โ โโโ FindById/ # CQRS Query
โ โ โโโ FindByIdQuery.php โ make:hexagonal:query
โ โ โโโ FindByIdQueryHandler.php โ (auto-generated)
โ โ โโโ FindByIdResponse.php โ (auto-generated)
โ โ
โ โโโ ListAll/
โ โโโ ListAllQuery.php โ make:hexagonal:query
โ โโโ ListAllQueryHandler.php โ (auto-generated)
โ โโโ ListAllResponse.php โ (auto-generated)
โ
โโโ UI/ # ๐ฎ PRIMARY ADAPTERS (Driving) - NEW!
โ โโโ Http/
โ โ โโโ Web/
โ โ โโโ Controller/
โ โ โ โโโ RegisterUserController.php โ make:hexagonal:controller
โ โ โ
โ โ โโโ Form/
โ โ โโโ UserType.php โ make:hexagonal:form
โ โ
โ โโโ Cli/
โ โโโ RegisterUserCommand.php โ make:hexagonal:cli-command
โ
โโโ Infrastructure/ # ๐ SECONDARY ADAPTERS (Driven)
โโโ Persistence/
โโโ Doctrine/
โโโ Orm/
โ โโโ Mapping/
โ โโโ User.orm.yml โ Auto-generated with entity (YAML mapping)
โ
โโโ DoctrineUserRepository.php โ make:hexagonal:repository (Adapter)
```
#### 4.1.3 Understanding the Architecture
| Layer | Responsibility | Dependencies | Makers Available |
|-------|---------------|--------------|------------------|
| **๐ Domain** | Business logic, rules, invariants | **ZERO** (Pure PHP) | `make:hexagonal:entity`
`make:hexagonal:value-object`
`make:hexagonal:exception`
`make:hexagonal:repository` (Port) |
| **โ๏ธ Application** | Use cases, orchestration, DTOs | Domain only | `make:hexagonal:command`
`make:hexagonal:query`
`make:hexagonal:use-case`
`make:hexagonal:input` |
| **๐ฎ UI** | HTTP/CLI interfaces (Primary Adapters) | Application + Domain | `make:hexagonal:controller`
`make:hexagonal:form`
`make:hexagonal:cli-command` |
| **๐ Infrastructure** | DB/API implementation (Secondary Adapters) | Domain (implements Ports) | `make:hexagonal:repository` (Adapter)
Auto: Doctrine YAML mapping |
### 4.2 Dependency Flow (Hexagonal Rule)
```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
graph TB
subgraph UI["๐ฎ UI / Controllers"]
HTTP["๐ HTTP Controllers"]
CLI["โจ๏ธ bin/console Commands"]
end
subgraph APP["โ๏ธ APPLICATION LAYER"]
Commands["๐จ Commands & Queries
Use Cases"]
Reg["โข RegisterCommand"]
Find["โข FindByIdQuery"]
Commands --- Reg
Commands --- Find
end
subgraph DOMAIN["๐ DOMAIN LAYER - CORE"]
Entities["๐ฆ Entities & Value Objects"]
EntList["โข User
โข Email, UserId"]
Ports["๐ Ports
Interfaces"]
PortList["โข UserRepositoryInterface"]
Entities --- EntList
Ports --- PortList
end
subgraph INFRA["๐ INFRASTRUCTURE LAYER"]
Adapters["๐ง Adapters
Implementations"]
AdList["โข DoctrineUserRepository"]
Adapters --- AdList
end
UI ==>|"uses"| APP
APP ==>|"depends on"| DOMAIN
INFRA -.->|"๐ฏ implements"| Ports
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
style UI fill:#E1BEE7,stroke:#6A1B9A,stroke-width:3px,color:#000
style Commands fill:#E1F5FE,stroke:#01579B,stroke-width:2px,color:#000
style Entities fill:#E8F5E9,stroke:#1B5E20,stroke-width:2px,color:#000
style Ports fill:#FFF9C4,stroke:#F57F17,stroke-width:2px,color:#000
style Adapters fill:#FCE4EC,stroke:#880E4F,stroke-width:2px,color:#000
```
**Key Points:**
- `make:hexagonal:command` / `make:hexagonal:query` โ Application Layer
- `make:hexagonal:entity` / `make:hexagonal:value-object` โ Domain Layer
- `make:hexagonal:repository` โ Port (Domain) + Adapter (Infrastructure)
### 4.3 Quick Start: 5-Command Complete Module
Want to generate a complete module in just 5 commands? Here's a copy-paste ready script:
```bash
# Context: Product Catalog Module
bin/console make:hexagonal:entity product/catalog Product
bin/console make:hexagonal:value-object product/catalog ProductId
bin/console make:hexagonal:repository product/catalog Product
bin/console make:hexagonal:command product/catalog create-product --factory
bin/console make:hexagonal:query product/catalog find-product
```
**Result:** Complete Product module with Domain, Application, and Infrastructure layers.
---
## 5. Available Makers
**Quick reference:** 19 makers covering Domain, Application, Infrastructure, UI, and Tests layers.
๐ Click to expand: Detailed maker commands documentation
### 5.1 Create a Command (Write Operation)
Generate a CQRS Command for state-changing operations:
```bash
bin/console make:hexagonal:command user/account register
```
**Generated files:**
```
src/User/Account/Application/Register/
โโโ RegisterCommand.php # The command (DTO)
โโโ RegisterCommandHandler.php # The handler (business logic)
```
**With Factory pattern:**
```bash
bin/console make:hexagonal:command user/account register --factory
```
**Generated files:**
```
src/User/Account/Application/Register/
โโโ RegisterCommand.php
โโโ RegisterCommandHandler.php # Uses factory
โโโ AccountFactory.php # Domain entity factory
```
**With Tests:**
```bash
bin/console make:hexagonal:command user/account register --with-tests
```
**Generated files:**
```
src/User/Account/Application/Register/
โโโ RegisterCommand.php
โโโ RegisterCommandHandler.php
tests/Unit/User/Account/Application/Register/
โโโ RegisterCommandHandlerTest.php # Unit test (with mocks)
tests/Integration/User/Account/Application/Register/
โโโ RegisterCommandHandlerTest.php # Integration test (full stack)
```
**With Factory and Tests:**
```bash
bin/console make:hexagonal:command user/account register --factory --with-tests
```
### 5.2 Create a Query (Read Operation)
Generate a CQRS Query for data retrieval:
```bash
bin/console make:hexagonal:query user/account find
```
**Generated files:**
```
src/User/Account/Application/Find/
โโโ FindQuery.php # The query (request DTO)
โโโ FindQueryHandler.php # The handler (read logic)
โโโ FindResponse.php # The response (response DTO)
```
### 5.3 Create a Repository (Port + Adapter)
Generate a repository interface (Port) and its infrastructure implementation (Adapter):
```bash
bin/console make:hexagonal:repository user/account User
```
**Generated files:**
```
src/User/Account/
โโโ Domain/Port/
โ โโโ UserRepositoryInterface.php # Port (interface)
โโโ Infrastructure/Persistence/Doctrine/
โโโ DoctrineUserRepository.php # Adapter (implementation)
```
### 5.4 Create a Domain Entity
Generate a domain entity in the core layer:
```bash
bin/console make:hexagonal:entity user/account User
```
**Generated files:**
```
src/User/Account/Domain/Model/
โโโ User.php # Domain entity with business logic
```
### 5.5 Create a Value Object
Generate an immutable value object:
```bash
bin/console make:hexagonal:value-object user/account Email
```
**Generated files:**
```
src/User/Account/Domain/ValueObject/
โโโ Email.php # Immutable value object with validation
```
### 5.6 Create a Domain Exception
Generate a business exception in the domain layer:
```bash
bin/console make:hexagonal:exception user/account InvalidEmailException
```
**Generated files:**
```
src/User/Account/Domain/Exception/
โโโ InvalidEmailException.php # Domain exception for business rule violations
```
### 5.7 Create an Input DTO
Generate an input DTO with validation constraints:
```bash
bin/console make:hexagonal:input user/account CreateUserInput
```
**Generated files:**
```
src/User/Account/Application/Input/
โโโ CreateUserInput.php # Input DTO with Symfony Validator constraints
```
### 5.8 Create a Use Case
Generate a use case (application service):
```bash
bin/console make:hexagonal:use-case user/account CreateUser
```
**Generated files:**
```
src/User/Account/Application/UseCase/
โโโ CreateUserUseCase.php # Use case orchestrating domain logic
```
### 5.9 Create a Web Controller (UI Layer)
Generate a web controller for HTTP requests:
```bash
bin/console make:hexagonal:controller user/account CreateUser /users/create
```
**Generated files:**
```
src/User/Account/UI/Http/Web/Controller/
โโโ CreateUserController.php # Web controller with routing
```
### 5.10 Create a Symfony Form
Generate a Symfony form type:
```bash
bin/console make:hexagonal:form user/account User
```
**Generated files:**
```
src/User/Account/UI/Http/Web/Form/
โโโ UserType.php # Symfony form type for web UI
```
### 5.11 Create a CLI Command (UI Layer)
Generate a console command:
```bash
bin/console make:hexagonal:cli-command user/account CreateUser app:user:create
```
**Generated files:**
```
src/User/Account/UI/Cli/
โโโ CreateUserCommand.php # CLI command for console operations
```
**With UseCase workflow:**
```bash
bin/console make:hexagonal:cli-command user/account CreateUser app:user:create --with-use-case
```
**Generated files:**
```
src/User/Account/UI/Cli/
โโโ CreateUserCommand.php
src/User/Account/Application/
โโโ UseCase/
โ โโโ CreateUserUseCase.php
โโโ Command/
โ โโโ CreateUserCommand.php
โ โโโ CreateUserCommandHandler.php
โโโ Input/
โโโ CreateUserInput.php
```
**Benefits:**
- Avoids duplication between web and CLI interfaces
- Both interfaces use the same UseCase
- Consistent business logic across all entry points
### 5.12 Create a Use Case Test (Tests)
Generate a test for your use case (Application layer):
```bash
bin/console make:hexagonal:use-case-test blog/post CreatePost
```
**Generated files:**
```
tests/Blog/Post/Application/CreatePost/
โโโ CreatePostTest.php # KernelTestCase with repository switching
```
**Key features:**
- Extends `KernelTestCase` for full container access
- Includes success and validation test methods
- Data providers for parameterized testing
- Helper method to switch between repository implementations (Memory/Doctrine/File)
### 5.13 Create a Controller Test (Tests)
Generate a test for your web controller (UI layer):
```bash
bin/console make:hexagonal:controller-test blog/post CreatePost /posts/create
```
**Generated files:**
```
tests/Blog/Post/UI/Http/Web/Controller/
โโโ CreatePostControllerTest.php # WebTestCase with HTTP client
```
**Key features:**
- Extends `WebTestCase` for HTTP testing
- Tests page loading and redirects
- Form submission testing with field mapping
- Database state verification
- Automatic cleanup in `setUp()`
### 5.14 Create a CLI Command Test (Tests)
Generate a test for your console command (UI layer):
```bash
bin/console make:hexagonal:cli-command-test blog/post CreatePost app:post:create
```
**Generated files:**
```
tests/Blog/Post/UI/Cli/
โโโ CreatePostCommandTest.php # CommandTester for CLI testing
```
**Key features:**
- Extends `KernelTestCase` with `CommandTester`
- Tests command execution and exit codes
- Tests arguments and options
- Output verification
- Error handling tests
### 5.15 Create a Domain Event (Domain Layer)
Generate an immutable domain event:
```bash
bin/console make:hexagonal:domain-event order/payment OrderPlaced
```
**Generated files:**
```
src/Order/Payment/Domain/Event/
โโโ OrderPlacedEvent.php # Immutable event representing a business fact
```
**Key features:**
- Readonly class for immutability
- Contains only data (no behavior)
- Represents a fact that happened in the domain
- Can be dispatched from entities or use cases
### 5.16 Create an Event Subscriber (Application or Infrastructure)
Generate an event subscriber with layer choice:
```bash
# Application Layer (for business workflow orchestration)
bin/console make:hexagonal:event-subscriber order/payment OrderPlaced --layer=application
# Infrastructure Layer (for technical concerns)
bin/console make:hexagonal:event-subscriber shared/logging Exception --layer=infrastructure
```
**Generated files (Application):**
```
src/Order/Payment/Application/EventSubscriber/
โโโ OrderPlacedSubscriber.php # Orchestrates use cases in response to events
```
**Generated files (Infrastructure):**
```
src/Shared/Infrastructure/EventSubscriber/
โโโ ExceptionSubscriber.php # Handles technical concerns (logging, monitoring)
```
**Key features:**
- **Application Layer**: Orchestrates business workflows, calls use cases
- **Infrastructure Layer**: Handles framework events, logging, caching
- Implements `EventSubscriberInterface`
- Auto-configured by Symfony
### 5.17 Enhanced Form with Auto-Generated Command/Input
Generate a form type with optional Command and Input DTO:
```bash
# Standard form only
bin/console make:hexagonal:form blog/post Post
# Form + Command + Input DTO in one command!
bin/console make:hexagonal:form blog/post Post --with-command --action=Create
```
**Generated files (with --with-command):**
```
src/Blog/Post/UI/Http/Web/Form/
โโโ PostType.php # Symfony form type
src/Blog/Post/Application/Input/
โโโ CreatePostInput.php # Input DTO with validation
src/Blog/Post/Application/Command/
โโโ CreatePostCommand.php # Command object
โโโ CreatePostCommandHandler.php # Command handler
```
**Benefits:**
- One command generates complete workflow
- Form fields map to Command properties
- Input DTO provides validation layer
- Saves time and ensures consistency
---
## 5.18 Generate Complete CRUD Module ๐
The most powerful command in the bundle - generate an entire CRUD module in seconds:
```bash
bin/console make:hexagonal:crud blog/post Post --route-prefix=/posts
```
**This single command generates 20+ files across all layers:**
```
๐ฆ Domain Layer (3 files):
- Post.php (Entity)
- PostRepositoryInterface.php (Port)
๐ง Infrastructure Layer (2 files):
- DoctrinePostRepository.php (Adapter)
- Post.orm.yml (Doctrine mapping)
๐ฏ Application Layer (15 files):
- CreatePostUseCase.php + CreatePostCommand.php + CreatePostInput.php
- UpdatePostUseCase.php + UpdatePostCommand.php + UpdatePostInput.php
- DeletePostUseCase.php + DeletePostCommand.php + DeletePostInput.php
- GetPostUseCase.php + GetPostCommand.php + GetPostInput.php
- ListPostUseCase.php + ListPostCommand.php + ListPostInput.php
๐ UI Web Layer (6 files):
- CreatePostController.php
- UpdatePostController.php
- DeletePostController.php
- ShowPostController.php
- ListPostController.php
- PostType.php (Form)
```
**With tests:**
```bash
bin/console make:hexagonal:crud blog/post Post --with-tests
```
**Generates 30+ files including:**
- All UseCase tests (5 files)
- All Controller tests (5 files)
**With ID ValueObject:**
```bash
bin/console make:hexagonal:crud blog/post Post --with-id-vo
```
**Additional file generated:**
- PostId.php (ValueObject for typed IDs)
**Complete example with all options:**
```bash
bin/console make:hexagonal:crud blog/post Post \
--route-prefix=/posts \
--with-tests \
--with-id-vo
```
**Generated routes:**
- `GET /posts` - List all posts
- `GET /posts/{id}` - Show single post
- `GET /posts/new` - Create new post form
- `POST /posts/new` - Submit new post
- `GET /posts/{id}/edit` - Edit post form
- `POST /posts/{id}/edit` - Submit edited post
- `DELETE /posts/{id}/delete` - Delete post
**Next steps after generation:**
1. Add properties to your Entity
2. Complete Doctrine ORM mapping
3. Configure form fields in PostType.php
4. Implement UseCase business logic
5. Implement Repository methods
6. Run tests (if generated)
**Perfect for:**
- Rapid prototyping
- Starting new modules
- Learning hexagonal architecture structure
- Scaffolding admin interfaces
---
## 5.19 Powerful `--with-*` Options for Rapid Development โก
All makers support powerful options to generate related files automatically, dramatically speeding up development:
### Controller: `--with-workflow`
Generate complete web workflow in one command:
```bash
bin/console make:hexagonal:controller blog/post CreatePost /posts/create --with-workflow
```
**Generates 6 files:**
- ๐ฏ CreatePostController.php (UI)
- ๐ฏ PostType.php (Form)
- ๐ฏ CreatePostUseCase.php (Application)
- ๐ฏ CreatePostCommand.php + Handler (Application)
- ๐ฏ CreatePostInput.php (Application)
**Impact:** Creates complete CRUD workflow instantly!
### Entity: `--with-repository` and `--with-id-vo`
Generate entity with repository and ID value object:
```bash
bin/console make:hexagonal:entity blog/post Post --with-repository --with-id-vo
```
**Generates 5 files:**
- ๐ฏ Post.php (Domain Entity)
- ๐ฏ Post.orm.yml (Doctrine Mapping)
- ๐ฏ PostRepositoryInterface.php (Domain Port)
- ๐ฏ DoctrinePostRepository.php (Infrastructure)
- ๐ฏ PostId.php (Value Object)
**Impact:** Complete entity setup with persistence!
### UseCase: `--with-test`
Generate use case with its test:
```bash
bin/console make:hexagonal:use-case blog/post CreatePost --with-test
```
**Generates 2 files:**
- ๐ฏ CreatePostUseCase.php (Application)
- ๐ฏ CreatePostTest.php (Tests)
**Impact:** Encourages TDD from the start!
### DomainEvent: `--with-subscriber`
Generate event with its subscriber:
```bash
bin/console make:hexagonal:domain-event order/payment OrderPlaced --with-subscriber
```
**Generates 2 files:**
- ๐ฏ OrderPlacedEvent.php (Domain)
- ๐ฏ OrderPlacedSubscriber.php (Application)
**Impact:** Event-driven architecture ready to use!
### Form: `--with-command`
Already documented in section 5.17
### CLI Command: `--with-use-case`
Generate CLI command with UseCase workflow:
```bash
bin/console make:hexagonal:cli-command blog/post CreatePost app:post:create --with-use-case
```
**Generates 4 files:**
- ๐ฏ CreatePostCommand.php (UI CLI)
- ๐ฏ CreatePostUseCase.php (Application)
- ๐ฏ CreatePostCommand.php + Handler (Application)
- ๐ฏ CreatePostInput.php (Application)
**Impact:** Shares business logic between web and CLI interfaces!
### Summary Table
| Maker | Option | Generates | Use Case |
|-------|--------|-----------|----------|
| `make:hexagonal:controller` | `--with-workflow` | Controller + Form + UseCase + Command + Input | Complete web CRUD |
| `make:hexagonal:cli-command` | `--with-use-case` | CLI + UseCase + Command + Input | CLI with business logic |
| `make:hexagonal:entity` | `--with-repository` | Entity + Mapping + Port + Adapter | Entity with persistence |
| `make:hexagonal:entity` | `--with-id-vo` | Entity + ID ValueObject | Typed IDs |
| `make:hexagonal:use-case` | `--with-test` | UseCase + Test | TDD workflow |
| `make:hexagonal:domain-event` | `--with-subscriber` | Event + Subscriber | Event-driven |
| `make:hexagonal:form` | `--with-command` | Form + Command + Input | Form workflow |
| `make:hexagonal:crud` | `--with-tests` | Complete CRUD + All tests | Full module with tests |
| `make:hexagonal:crud` | `--with-id-vo` | Complete CRUD + ID VO | CRUD with typed IDs |
**Pro Tip:** Combine options for maximum productivity!
```bash
# Option 1: Build feature step-by-step (2 commands)
bin/console make:hexagonal:entity blog/post Post --with-repository --with-id-vo
bin/console make:hexagonal:controller blog/post CreatePost /posts/create --with-workflow
# Option 2: Generate entire CRUD module instantly (1 command) โก
bin/console make:hexagonal:crud blog/post Post --with-tests --with-id-vo
# Option 3: CLI + Web sharing same business logic
bin/console make:hexagonal:use-case blog/post CreatePost --with-test
bin/console make:hexagonal:controller blog/post CreatePost /posts/create
bin/console make:hexagonal:cli-command blog/post CreatePost app:post:create
```
---
## 6. Configuration
Create `config/packages/hexagonal_maker.yaml`:
```yaml
hexagonal_maker:
# Directory where custom skeleton templates are stored
skeleton_dir: '%kernel.project_dir%/config/skeleton'
# Root source directory
root_dir: 'src'
# Root namespace
root_namespace: 'App'
```
7.1 Customizing Templates
## 7.1 Customizing Templates
You can override default templates by creating your own in `config/skeleton/`:
```
config/skeleton/
โโโ src/Module/
โโโ Application/
โ โโโ Command/
โ โ โโโ Command.tpl.php
โ โ โโโ CommandHandler.tpl.php
โ โ โโโ CommandHandlerWithFactory.tpl.php
โ โ โโโ Factory.tpl.php
โ โโโ Query/
โ โโโ Query.tpl.php
โ โโโ QueryHandler.tpl.php
โ โโโ Response.tpl.php
โโโ Domain/
โ โโโ Model/
โ โ โโโ Entity.tpl.php
โ โโโ ValueObject/
โ โ โโโ ValueObject.tpl.php
โ โโโ Port/
โ โโโ RepositoryInterface.tpl.php
โโโ Infrastructure/
โโโ Persistence/
โโโ Doctrine/
โโโ DoctrineRepository.tpl.php
```
7.2 Testing Strategy
## 7.2 Testing Strategy
The bundle generates two types of tests when using `--with-tests`:
### 7.2.1 Unit Tests
Located in `tests/Unit/`, these tests:
- Use mocks and stubs for dependencies
- Test business logic in isolation
- Run extremely fast (milliseconds)
- No database, no framework boot
**Example:**
```php
final class RegisterCommandHandlerTest extends TestCase
{
public function testHandlerExecutesSuccessfully(): void
{
$repository = $this->createMock(UserRepositoryInterface::class);
$repository->expects($this->once())
->method('save');
$handler = new RegisterCommandHandler($repository);
$handler(new RegisterCommand('test@example.com', 'password'));
}
}
```
### 7.2.2 Integration Tests
Located in `tests/Integration/`, these tests:
- Use real dependencies (database, services)
- Test the full stack end-to-end
- Verify actual behavior in production-like environment
- Extend `KernelTestCase` for Symfony integration
**Example:**
```php
final class RegisterCommandHandlerTest extends KernelTestCase
{
public function testCommandIsHandledSuccessfully(): void
{
self::bootKernel();
$commandBus = static::getContainer()->get(MessageBusInterface::class);
$command = new RegisterCommand('test@example.com', 'password');
$commandBus->dispatch($command);
// Verify database changes
$repository = static::getContainer()->get(UserRepositoryInterface::class);
$user = $repository->findByEmail('test@example.com');
$this->assertNotNull($user);
}
}
```
### 7.2.3 InMemory Repositories
The bundle also generates InMemory repository implementations for faster unit testing:
```php
final class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function save(User $user): void
{
$this->users[$user->getId()->value] = $user;
}
public function all(): array
{
return array_values($this->users);
}
}
```
**Benefits:**
- No database setup required
- Tests run 1000x faster
- Easy to verify state changes
- Perfect for TDD
---
7.3 Doctrine ORM Integration
## 7.3 Doctrine ORM Integration
### 7.3.1 Pure Domain Entities + YAML Mapping
In true **Hexagonal Architecture**, the Domain layer must remain **PURE** - completely independent of infrastructure frameworks.
This bundle generates:
1. **Domain Entity** (pure PHP, no Doctrine) - in `Domain/Model/`
2. **Doctrine YAML Mapping** (infrastructure concern) - in `Infrastructure/Persistence/Doctrine/Orm/Mapping/`
This approach maintains **strict separation of concerns** and follows DDD best practices.
### 7.3.2 Generated Files Structure
When you run:
```bash
bin/console make:hexagonal:entity user/account User
```
**Two files are generated:**
**1. Domain Entity (PURE)**
```php
id = $id;
$this->email = $email;
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): string
{
return $this->id;
}
// Business logic methods...
}
```
**2. Doctrine ORM Mapping (Infrastructure)**
```yaml
# src/User/Account/Infrastructure/Persistence/Doctrine/Orm/Mapping/User.orm.yml
App\User\Account\Domain\Model\User:
type: entity
repositoryClass: App\User\Account\Infrastructure\Persistence\Doctrine\DoctrineUserRepository
table: user
id:
id:
type: string
length: 36
fields:
email:
type: string
length: 180
unique: true
createdAt:
type: datetime_immutable
column: created_at
```
### 7.3.3 Why YAML Mapping in Infrastructure Layer?
This is the **correct approach** for true Hexagonal Architecture and DDD:
**๐ฏ Advantages:**
- **Pure Domain** - Zero framework dependencies in domain entities
- **Easy Testing** - No need to mock Doctrine infrastructure
- **Technology Independence** - Switch ORMs without touching domain code
- **True Separation** - Persistence is an infrastructure detail, not a domain concern
- **Follows DDD Principles** - Domain model independent of persistence mechanism
**Configuration Required:**
In `config/packages/doctrine.yaml`:
```yaml
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
# Add one mapping per module
UserAccount:
is_bundle: false
type: yml
dir: '%kernel.project_dir%/src/User/Account/Infrastructure/Persistence/Doctrine/Orm/Mapping'
prefix: 'App\User\Account\Domain\Model'
alias: UserAccount
# Add more modules as needed:
# Product:
# is_bundle: false
# type: yml
# dir: '%kernel.project_dir%/src/Catalog/Product/Infrastructure/Persistence/Doctrine/Orm/Mapping'
# prefix: 'App\Catalog\Product\Domain\Model'
# alias: Product
```
### 7.3.4 YAML Mapping Examples
Here are common YAML mapping patterns you'll use:
**Basic Field Types:**
```yaml
fields:
# String
name:
type: string
length: 255
# Text (unlimited)
description:
type: text
# Numbers
age:
type: integer
price:
type: decimal
precision: 10
scale: 2
# Boolean
isActive:
type: boolean
# Dates
createdAt:
type: datetime_immutable
birthDate:
type: date_immutable
# JSON
metadata:
type: json
# Nullable
middleName:
type: string
length: 255
nullable: true
```
**Unique Constraints:**
```yaml
fields:
email:
type: string
length: 180
unique: true
```
### 7.3.5 Entity Identity Strategies
**Option 1: UUID (Recommended for DDD)**
```yaml
id:
id:
type: uuid
# Doctrine will automatically use UUID type
```
**Option 2: ULID (Sortable UUID)**
```yaml
id:
id:
type: ulid
# Doctrine will automatically use ULID type
```
**Option 3: String-based UUID**
```yaml
id:
id:
type: string
length: 36
# Generate UUID in entity constructor
```
**Option 4: Auto-increment**
```yaml
id:
id:
type: integer
generator:
strategy: AUTO
```
### 7.3.6 Associations (Relationships)
**One-to-Many:**
```yaml
oneToMany:
orders:
targetEntity: App\Domain\Order\Order
mappedBy: user
cascade: ['persist', 'remove']
```
**Many-to-One:**
```yaml
manyToOne:
category:
targetEntity: App\Domain\Category\Category
inversedBy: products
joinColumn:
name: category_id
referencedColumnName: id
nullable: false
```
**Many-to-Many:**
```yaml
manyToMany:
tags:
targetEntity: App\Domain\Tag\Tag
inversedBy: products
joinTable:
name: product_tag
joinColumns:
product_id:
referencedColumnName: id
inverseJoinColumns:
tag_id:
referencedColumnName: id
```
### 7.3.7 Embedded Value Objects
**Address.orm.yml** (Value Object):
```yaml
App\Domain\ValueObject\Address:
type: embeddable
fields:
street:
type: string
length: 255
city:
type: string
length: 100
zipCode:
type: string
length: 10
```
**User.orm.yml** (Entity using embedded):
```yaml
App\Domain\Model\User:
type: entity
table: user
# ... other fields ...
embedded:
address:
class: App\Domain\ValueObject\Address
columnPrefix: address_
```
### 7.3.8 Database Schema Generation
After creating/modifying YAML mapping files:
```bash
# 1. Validate mapping files
bin/console doctrine:schema:validate
# 2. Generate migration from mapping changes
bin/console doctrine:migrations:diff
# 3. Review the generated migration in migrations/
# Then execute it:
bin/console doctrine:migrations:migrate
# For development only - direct schema update (skip migrations)
bin/console doctrine:schema:update --force
```
### 7.3.9 Complete Reference
For complete YAML mapping reference, see:
- [Doctrine YAML Mapping Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/yaml-mapping.html)
- Generated mapping file template in: `Infrastructure/Persistence/Doctrine/Orm/Mapping/`
- Configuration guide: `Infrastructure/Persistence/Doctrine/Orm/Mapping/DOCTRINE_CONFIGURATION.md`
---
7.4 Doctrine Extensions (Gedmo) - Keep Domain Pure ๐ฏ
## 7.4 Doctrine Extensions (Gedmo) - Keep Domain Pure ๐ฏ
This bundle generates pure domain entities with YAML mapping, making it **100% compatible** with Doctrine Extensions (Gedmo) **without polluting your domain layer**.
### 7.4.1 Why YAML Mapping for Extensions?
**๐ช๏ธ Traditional approach (breaks hexagonal architecture):**
```php
use Gedmo\Mapping\Annotation as Gedmo;
class Post
{
#[Gedmo\Slug(fields: ['title'])] // ๐ช๏ธ Domain depends on Gedmo!
private string $slug;
#[Gedmo\Timestampable(on: 'create')] // ๐ช๏ธ Infrastructure concern in Domain!
private \DateTimeInterface $createdAt;
}
```
**๐ฏ Hexagonal approach (domain stays pure):**
```php
// Domain entity - PURE PHP
class Post
{
private string $slug; // ๐ฏ No Gedmo dependency
private \DateTimeInterface $createdAt;
public function __construct(string $title)
{
$this->title = $title;
// slug and createdAt managed automatically by Gedmo via YAML
}
}
```
```yaml
# Infrastructure YAML mapping - Configuration separated
fields:
slug:
type: string
gedmo:
slug:
fields: [title]
createdAt:
type: datetime_immutable
gedmo:
timestampable:
on: create
```
### 7.4.2 Installation
```bash
composer require stof/doctrine-extensions-bundle
```
### 7.4.3 Configuration
**Enable extensions in `config/packages/stof_doctrine_extensions.yaml`:**
```yaml
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable: true # Auto-generate slugs
timestampable: true # Auto-manage created/updated dates
softdeleteable: true # Soft delete (logical deletion)
blameable: true # Track who created/updated
loggable: true # Entity change history
translatable: true # Multi-language content
tree: true # Nested tree structures
```
### 7.4.4 Available Extensions with YAML Examples
#### 1๏ธโฃ **Sluggable** - Auto-generate URL-friendly slugs
**Domain Entity:**
```php
final class Post
{
private string $title;
private string $slug; // Managed by Gedmo
public function __construct(string $title)
{
$this->title = $title;
// No need to manually set slug!
}
public function updateTitle(string $title): void
{
$this->title = $title;
// Slug auto-updates when title changes
}
}
```
**YAML Mapping:**
```yaml
App\Blog\Post\Domain\Model\Post:
type: entity
fields:
title:
type: string
length: 255
slug:
type: string
length: 128
unique: true
gedmo:
slug:
fields: [title] # Generate from title
updatable: true # Update when title changes
separator: '-' # Use hyphens
unique: true # Ensure uniqueness
```
#### 2๏ธโฃ **Timestampable** - Auto-manage created/updated dates
**Domain Entity:**
```php
final class Post
{
private \DateTimeImmutable $createdAt; // Set automatically
private \DateTimeImmutable $updatedAt; // Updated automatically
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
}
```
**YAML Mapping:**
```yaml
fields:
createdAt:
type: datetime_immutable
column: created_at
gedmo:
timestampable:
on: create # Set when entity is created
updatedAt:
type: datetime_immutable
column: updated_at
gedmo:
timestampable:
on: update # Update on every change
publishedAt:
type: datetime_immutable
column: published_at
nullable: true
gedmo:
timestampable:
on: change # Set when specific field changes
field: status
value: published # When status becomes 'published'
```
#### 3๏ธโฃ **SoftDeleteable** - Logical deletion (keep data)
**Domain Entity:**
```php
final class Post
{
private ?\DateTimeImmutable $deletedAt; // Managed by Gedmo
public function isDeleted(): bool
{
return $this->deletedAt !== null;
}
}
```
**YAML Mapping:**
```yaml
App\Blog\Post\Domain\Model\Post:
type: entity
gedmo:
soft_deleteable:
field_name: deletedAt # Field to mark deletion
time_aware: false # Set to true to filter by date
fields:
deletedAt:
type: datetime_immutable
column: deleted_at
nullable: true
```
**Usage:**
```php
// Soft delete (sets deletedAt, doesn't remove from DB)
$entityManager->remove($post);
$entityManager->flush();
// Soft-deleted entities are automatically excluded from queries
$posts = $repository->findAll(); // Excludes deleted posts
// To include deleted entities
$repository->createQueryBuilder('p')
->getQuery()
->setHint(
\Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker::HINT_SOFT_DELETED,
true
);
```
#### 4๏ธโฃ **Blameable** - Track who created/updated
**Domain Entity:**
```php
final class Post
{
private string $createdBy; // User who created
private string $updatedBy; // Last user who updated
}
```
**YAML Mapping:**
```yaml
fields:
createdBy:
type: string
length: 255
column: created_by
gedmo:
blameable:
on: create
updatedBy:
type: string
length: 255
column: updated_by
gedmo:
blameable:
on: update
publishedBy:
type: string
length: 255
column: published_by
nullable: true
gedmo:
blameable:
on: change
field: status
value: published
```
**Configure Blameable Listener:**
```yaml
# config/services.yaml
services:
Gedmo\Blameable\BlameableListener:
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setUserValue, [ '@security.token_storage' ] ]
```
#### 5๏ธโฃ **Translatable** - Multi-language content
**Domain Entity:**
```php
final class Post
{
private string $title; // Translatable
private string $content; // Translatable
private string $locale; // Current locale
public function setTranslatableLocale(string $locale): void
{
$this->locale = $locale;
}
}
```
**YAML Mapping:**
```yaml
App\Blog\Post\Domain\Model\Post:
type: entity
gedmo:
translation:
entity: Gedmo\Translatable\Entity\Translation
locale: locale
fields:
title:
type: string
length: 255
gedmo:
translatable: ~ # This field is translatable
content:
type: text
gedmo:
translatable: ~
locale:
type: string
length: 5
gedmo:
locale: ~ # Stores current locale
```
**Usage:**
```php
// Create post in English
$post = new Post('Hello World', 'Content in English');
$entityManager->persist($post);
$entityManager->flush();
// Add French translation
$post->setTranslatableLocale('fr');
$post->setTitle('Bonjour le monde');
$post->setContent('Contenu en franรงais');
$entityManager->persist($post);
$entityManager->flush();
// Retrieve in specific language
$repository->findTranslationsByLocale($post, 'fr');
```
#### 6๏ธโฃ **Tree (Nested Set)** - Hierarchical structures
**Domain Entity:**
```php
final class Category
{
private int $lft; // Left value
private int $lvl; // Level
private int $rgt; // Right value
private ?int $root; // Root id
private ?self $parent; // Parent category
private Collection $children; // Child categories
}
```
**YAML Mapping:**
```yaml
App\Category\Domain\Model\Category:
type: entity
gedmo:
tree:
type: nested # Use Nested Set algorithm
fields:
name:
type: string
length: 255
lft:
type: integer
gedmo:
tree_left: ~
lvl:
type: integer
gedmo:
tree_level: ~
rgt:
type: integer
gedmo:
tree_right: ~
root:
type: integer
nullable: true
gedmo:
tree_root: ~
manyToOne:
parent:
targetEntity: App\Category\Domain\Model\Category
inversedBy: children
joinColumn:
name: parent_id
referencedColumnName: id
onDelete: CASCADE
gedmo:
tree_parent: ~
oneToMany:
children:
targetEntity: App\Category\Domain\Model\Category
mappedBy: parent
```
**Usage:**
```php
// Create tree structure
$electronics = new Category('Electronics');
$computers = new Category('Computers');
$laptops = new Category('Laptops');
$computers->setParent($electronics);
$laptops->setParent($computers);
// Query tree
$repository->childrenHierarchy(); // Get full tree
$repository->getChildren($electronics); // Get direct children
$repository->getPath($laptops); // Get path from root
```
#### 7๏ธโฃ **Loggable** - Entity change history
**Domain Entity:**
```php
final class Post
{
private string $title; // Versioned
private string $content; // Versioned
// Changes will be logged automatically
}
```
**YAML Mapping:**
```yaml
App\Blog\Post\Domain\Model\Post:
type: entity
gedmo:
loggable: ~ # Enable logging for this entity
fields:
title:
type: string
length: 255
gedmo:
versioned: ~ # Track changes to this field
content:
type: text
gedmo:
versioned: ~
```
**Usage:**
```php
// Changes are logged automatically
$post->setTitle('New Title');
$entityManager->flush();
// Retrieve change history
$logEntries = $entityManager
->getRepository(Gedmo\Loggable\Entity\LogEntry::class)
->getLogEntries($post);
foreach ($logEntries as $log) {
echo $log->getAction(); // create, update, remove
echo $log->getUsername(); // who made the change
echo $log->getLoggedAt(); // when
echo $log->getData(); // what changed
}
```
### 7.4.5 Complete Example: Blog Post with Multiple Extensions
**Domain Entity (100% Pure):**
```php
id = $id;
$this->title = $title;
$this->content = $content;
// All Gedmo fields are managed automatically!
}
public function publish(): void
{
$this->status = 'published';
// publishedAt will be set automatically by Gedmo
}
public function updateContent(string $title, string $content): void
{
$this->title = $title;
$this->content = $content;
// slug and updatedAt will be updated automatically
}
// Getters only - no setters for Gedmo-managed fields
public function getSlug(): string { return $this->slug; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
public function isDeleted(): bool { return $this->deletedAt !== null; }
}
```
**YAML Mapping (Infrastructure Configuration):**
```yaml
# src/Blog/Post/Infrastructure/Persistence/Doctrine/Orm/Mapping/Post.orm.yml
App\Blog\Post\Domain\Model\Post:
type: entity
repositoryClass: App\Blog\Post\Infrastructure\Persistence\Doctrine\DoctrinePostRepository
table: post
gedmo:
soft_deleteable:
field_name: deletedAt
loggable: ~
id:
id:
type: string
length: 36
fields:
title:
type: string
length: 255
gedmo:
versioned: ~
slug:
type: string
length: 128
unique: true
gedmo:
slug:
fields: [title]
updatable: true
unique: true
content:
type: text
gedmo:
versioned: ~
status:
type: string
length: 20
createdAt:
type: datetime_immutable
column: created_at
gedmo:
timestampable:
on: create
updatedAt:
type: datetime_immutable
column: updated_at
gedmo:
timestampable:
on: update
publishedAt:
type: datetime_immutable
column: published_at
nullable: true
gedmo:
timestampable:
on: change
field: status
value: published
deletedAt:
type: datetime_immutable
column: deleted_at
nullable: true
createdBy:
type: string
length: 255
column: created_by
gedmo:
blameable:
on: create
updatedBy:
type: string
length: 255
column: updated_by
gedmo:
blameable:
on: update
```
### 7.4.6 Benefits of YAML-based Extensions
| Benefit | Description |
|---------|-------------|
| ๐ฏ **Pure Domain** | Zero framework/library dependencies in domain entities |
| ๐ฏ **Technology Independence** | Easy to switch from Gedmo to another solution |
| ๐ฏ **Easy Testing** | Domain entities remain simple POPOs (Plain Old PHP Objects) |
| ๐ฏ **Clear Separation** | Infrastructure concerns stay in Infrastructure layer |
| ๐ฏ **True Hexagonal** | Respects dependency inversion principle |
| ๐ฏ **All Extensions Work** | Full compatibility with all Gedmo extensions |
### 7.4.7 References
- [StofDoctrineExtensionsBundle Documentation](https://github.com/stof/StofDoctrineExtensionsBundle)
- [Doctrine Extensions (Gedmo) Documentation](https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc)
- [YAML Mapping Examples](https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/yaml_mapping.md)
---
7.5 Infrastructure Organization ๐๏ธ
## 7.5 Infrastructure Organization ๐๏ธ
The Infrastructure layer contains **Secondary Adapters** - technical implementations of ports (interfaces) defined in the Domain.
### 7.5.1 Recommended Structure
```
Infrastructure/
โโโ Persistence/ โ Database adapters
โ โโโ Doctrine/ โ Doctrine ORM implementation
โ โ โโโ DoctrineUserRepository.php
โ โ โโโ Orm/
โ โ โโโ Mapping/
โ โ โโโ User.orm.yml
โ โโโ InMemory/ โ In-memory for testing (optional)
โ โโโ InMemoryUserRepository.php
โโโ Messaging/ โ Async/Queue adapters
โ โโโ Handler/ โ Message handlers (Symfony Messenger)
โ โ โโโ SendWelcomeEmailHandler.php
โ โโโ Publisher/ โ Event publishers
โ โโโ DomainEventPublisher.php
โโโ Email/ โ Email service adapters
โ โโโ SymfonyMailerService.php
โ โโโ SendGridService.php
โโโ Http/ โ HTTP client adapters (external APIs)
โ โโโ StripePaymentClient.php
โ โโโ GoogleMapsClient.php
โโโ Cache/ โ Cache adapters
โ โโโ RedisCacheAdapter.php
โโโ FileStorage/ โ File storage adapters
โ โโโ LocalFilesystemStorage.php
โ โโโ S3Storage.php
โโโ EventSubscriber/ โ Infrastructure event subscribers
โโโ LoggingSubscriber.php
```
### 7.5.2 Persistence Layer (Doctrine)
**Generated automatically by:** `make:hexagonal:repository`
```bash
bin/console make:hexagonal:repository user/account User
```
**Generates:**
- ๐ฏ Port: `Domain/Port/UserRepositoryInterface.php`
- ๐ฏ Adapter: `Infrastructure/Persistence/Doctrine/DoctrineUserRepository.php`
- ๐ฏ Mapping: `Infrastructure/Persistence/Doctrine/Orm/Mapping/User.orm.yml`
**Example - Domain Port:**
```php
// src/Module/User/Account/Domain/Port/UserRepositoryInterface.php
namespace App\Module\User\Account\Domain\Port;
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(string $id): ?User;
}
```
**Example - Infrastructure Adapter:**
```php
// src/Module/User/Account/Infrastructure/Persistence/Doctrine/DoctrineUserRepository.php
namespace App\Module\User\Account\Infrastructure\Persistence\Doctrine;
use Doctrine\ORM\EntityManagerInterface;
final class DoctrineUserRepository implements UserRepositoryInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function save(User $user): void
{
$this->em->persist($user);
$this->em->flush();
}
}
```
### 7.5.3 Messaging Layer (Async/Queue) โก
**NEW in this bundle!** Generate async message handlers for background processing.
**Generated by:** `make:hexagonal:message-handler`
```bash
# Generate message handler only
bin/console make:hexagonal:message-handler user/account SendWelcomeEmail
# Generate handler + message class
bin/console make:hexagonal:message-handler user/account SendWelcomeEmail --with-message
```
**Generates:**
- ๐ฏ Handler: `Infrastructure/Messaging/Handler/SendWelcomeEmailHandler.php`
- ๐ฏ Message: `Application/Message/SendWelcomeEmailMessage.php` (with `--with-message`)
**Example - Message (DTO):**
```php
// src/Module/User/Account/Application/Message/SendWelcomeEmailMessage.php
namespace App\Module\User\Account\Application\Message;
final readonly class SendWelcomeEmailMessage
{
public function __construct(
public string $userId,
public string $email,
public string $name,
) {
}
}
```
**Example - Message Handler:**
```php
// src/Module/User/Account/Infrastructure/Messaging/Handler/SendWelcomeEmailHandler.php
namespace App\Module\User\Account\Infrastructure\Messaging\Handler;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final readonly class SendWelcomeEmailHandler
{
public function __construct(
private EmailServiceInterface $emailService,
private LoggerInterface $logger,
) {
}
public function __invoke(SendWelcomeEmailMessage $message): void
{
$this->emailService->sendWelcomeEmail(
to: $message->email,
name: $message->name
);
$this->logger->info('Welcome email sent', [
'user_id' => $message->userId,
]);
}
}
```
**Dispatch message from UseCase:**
```php
// src/Module/User/Account/Application/UseCase/CreateUserUseCase.php
use Symfony\Component\Messenger\MessageBusInterface;
final readonly class CreateUserUseCase
{
public function __construct(
private UserRepositoryInterface $repository,
private MessageBusInterface $messageBus, // Inject message bus
) {
}
public function execute(CreateUserCommand $command): void
{
$user = new User(...);
$this->repository->save($user);
// Dispatch async message
$this->messageBus->dispatch(new SendWelcomeEmailMessage(
userId: $user->getId(),
email: $user->getEmail(),
name: $user->getName(),
));
}
}
```
**Configure Messenger (config/packages/messenger.yaml):**
```yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Route all messages to async transport
'App\Module\User\Account\Application\Message\SendWelcomeEmailMessage': async
```
**Start worker:**
```bash
bin/console messenger:consume async
```
### 7.5.4 Email/Http/Cache/FileStorage Adapters
These adapters are **too specific** to auto-generate. Create them manually following the Port & Adapter pattern.
**Example - Email Adapter:**
**1. Define Port (Domain):**
```php
// src/Module/User/Account/Domain/Port/EmailServiceInterface.php
namespace App\Module\User\Account\Domain\Port;
interface EmailServiceInterface
{
public function sendWelcomeEmail(string $to, string $name): void;
}
```
**2. Implement Adapter (Infrastructure):**
```php
// src/Module/User/Account/Infrastructure/Email/SymfonyMailerService.php
namespace App\Module\User\Account\Infrastructure\Email;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
final readonly class SymfonyMailerService implements EmailServiceInterface
{
public function __construct(private MailerInterface $mailer) {}
public function sendWelcomeEmail(string $to, string $name): void
{
$email = (new Email())
->to($to)
->subject('Welcome!')
->html("
Welcome $name!
");
$this->mailer->send($email);
}
}
```
**3. Configure Service:**
```yaml
# config/services.yaml
services:
App\Module\User\Account\Domain\Port\EmailServiceInterface:
class: App\Module\User\Account\Infrastructure\Email\SymfonyMailerService
```
**Benefits:**
- ๐ฏ Easy to switch from SymfonyMailer to SendGrid (just change config)
- ๐ฏ Easy to mock in tests
- ๐ฏ Domain doesn't know about Symfony
---
7.6 Shared Kernel Structure ๐
## 7.6 Shared Kernel Structure ๐
The **Shared Kernel** contains code reused across multiple modules (bounded contexts).
### 7.6.1 Recommended Structure
```
src/
โโโ Module/ โ Modular architecture (bounded contexts)
โ โโโ User/
โ โโโ Blog/
โ โโโ Order/
โโโ Shared/ โ Shared across modules
โโโ Domain/
โ โโโ ValueObject/ โ Shared value objects
โ โ โโโ Uuid.php
โ โ โโโ Email.php
โ โ โโโ DateRange.php
โ โ โโโ Money.php
โ โโโ Exception/ โ Shared domain exceptions
โ โ โโโ NotFoundException.php
โ โ โโโ ValidationException.php
โ โ โโโ DomainException.php
โ โโโ Event/ โ Shared domain events (optional)
โ โโโ DomainEventInterface.php
โโโ Application/
โ โโโ Bus/ โ Bus abstractions
โ โ โโโ CommandBusInterface.php
โ โ โโโ QueryBusInterface.php
โ โ โโโ EventBusInterface.php
โ โโโ UseCase/ โ Shared use case traits
โ โโโ TransactionalTrait.php
โโโ Infrastructure/
โโโ Persistence/
โ โโโ Migrations/ โ Doctrine migrations (centralized)
โ โโโ Version20250106120000.php
โ โโโ Version20250106130000.php
โโโ Bus/
โ โโโ SymfonyCommandBus.php
โ โโโ SymfonyQueryBus.php
โ โโโ SymfonyEventBus.php
โโโ Doctrine/
โโโ Types/ โ Custom Doctrine types
โโโ UuidType.php
โโโ MoneyType.php
```
### 7.6.2 Shared Value Objects
**Example - Shared Email Value Object:**
```php
// src/Shared/Domain/ValueObject/Email.php
namespace App\Shared\Domain\ValueObject;
final readonly class Email
{
private string $value;
public function __construct(string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email: $value");
}
$this->value = strtolower($value);
}
public function getValue(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
}
```
**Usage in modules:**
```php
// Module User
use App\Shared\Domain\ValueObject\Email;
final class User
{
public function __construct(
private Email $email, // Reuse shared Email VO
) {
}
}
// Module Newsletter
use App\Shared\Domain\ValueObject\Email;
final class Subscriber
{
public function __construct(
private Email $email, // Same Email VO!
) {
}
}
```
### 7.6.3 Shared Exceptions
```php
// src/Shared/Domain/Exception/NotFoundException.php
namespace App\Shared\Domain\Exception;
class NotFoundException extends \DomainException
{
public static function forResource(string $resource, string $id): self
{
return new self("$resource with ID '$id' not found");
}
}
// Usage:
throw NotFoundException::forResource('User', $userId);
throw NotFoundException::forResource('Post', $postId);
```
### 7.6.4 Doctrine Migrations (Centralized)
**Configure Doctrine Migrations in Shared:**
```yaml
# config/packages/doctrine_migrations.yaml
doctrine_migrations:
migrations_paths:
'App\Shared\Infrastructure\Persistence\Migrations': 'src/Shared/Infrastructure/Persistence/Migrations'
organize_migrations: false
all_or_nothing: true
```
**Generate migrations:**
```bash
bin/console make:migration
```
**Generated in:**
```
src/Shared/Infrastructure/Persistence/Migrations/Version20250106120000.php
```
**Why centralized migrations?**
- ๐ฏ Single source of truth for database schema
- ๐ฏ Migrations execute in order (no conflicts between modules)
- ๐ฏ Easier to track schema evolution
- ๐ช๏ธ Modules are slightly coupled through DB schema (acceptable trade-off)
### 7.6.5 When to Use Shared vs Module
| Component | Shared | Module | Reasoning |
|-----------|--------|--------|-----------|
| **Email VO** | ๐ฏ | ๐ช๏ธ | Same validation everywhere |
| **Money VO** | ๐ฏ | ๐ช๏ธ | Same currency logic everywhere |
| **Uuid VO** | ๐ฏ | ๐ช๏ธ | Generic identifier |
| **UserException** | ๐ช๏ธ | ๐ฏ | Specific to User module |
| **User Entity** | ๐ช๏ธ | ๐ฏ | Bounded context specific |
| **NotFoundException** | ๐ฏ | ๐ช๏ธ | Generic exception |
| **Migrations** | ๐ฏ | ๐ช๏ธ | Database-wide changes |
| **Bus Interfaces** | ๐ฏ | ๐ช๏ธ | Application-wide infrastructure |
**Golden Rule:**
> If 3+ modules need the same code โ Move to Shared
> If only 1-2 modules need it โ Keep in Module
### 7.6.6 Benefits of Shared Kernel
| Benefit | Description |
|---------|-------------|
| ๐ฏ **DRY Principle** | Avoid duplicating Email, Uuid, Money across modules |
| ๐ฏ **Consistency** | Same validation logic everywhere |
| ๐ฏ **Maintainability** | Fix once, applies everywhere |
| ๐ **Coupling** | Modules depend on Shared (acceptable trade-off) |
### 7.6.7 References
- [Shared Kernel Pattern (DDD)](https://martinfowler.com/bliki/BoundedContext.html)
- [Symfony Doctrine Migrations](https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html)
---
## 7. Best Practices
### 7.1 Best Practices & Design Principles
**๐ Complete Documentation:**
- [**Best Practices Guide**](https://ahmed-bhs.github.io/hexagonal-maker-bundle/advanced/) - Architecture patterns and implementation guidelines
- [**SOLID Principles**](https://ahmed-bhs.github.io/hexagonal-maker-bundle/SOLID.md) - How hexagonal architecture enforces SOLID principles
- [**Domain vs Application Logic**](https://ahmed-bhs.github.io/hexagonal-maker-bundle/advanced/domain-vs-application.html) - Decision guide for business logic placement
- [**Error Handling Strategy**](https://ahmed-bhs.github.io/hexagonal-maker-bundle/advanced/error-handling-strategy.html) - Exception handling best practices
- [**Anti-Patterns to Avoid**](https://ahmed-bhs.github.io/hexagonal-maker-bundle/advanced/anti-patterns-pitfalls.html) - Common mistakes and how to avoid them
**Quick summary:**
- Keep Domain pure (zero framework dependencies)
- Use Value Objects (immutable with `readonly`)
- CQRS separation (Commands change state, Queries read data)
- Port/Adapter pattern (interfaces in domain, implementations in infrastructure)
- Factories for complex creation
See [ARCHITECTURE-EN.md - Best Practices](ARCHITECTURE-EN.md#7-best-practices) | [ARCHITECTURE.md - Bonnes pratiques (FR)](ARCHITECTURE.md#7-bonnes-pratiques) for detailed best practices with code examples.
---
## 8. Additional Resources
### Documentation
- [Complete Architecture Guide](ARCHITECTURE-EN.md) | [Guide Complet d'Architecture (FR)](ARCHITECTURE.md) - Detailed explanation of hexagonal architecture concepts with diagrams
- [SOLID Principles Guide](SOLID-EN.md) | [Guide des Principes SOLID (FR)](SOLID.md) - How hexagonal architecture respects SOLID principles
- [Practical Examples](EXAMPLES-EN.md) | [Exemples Pratiques (FR)](EXAMPLES.md) - Complete real-world examples with full code
### Learn More
- [Doctrine YAML Mapping Reference](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/yaml-mapping.html)
- [Hexagonal Architecture (Alistair Cockburn)](https://alistair.cockburn.us/hexagonal-architecture/)
- [Domain-Driven Design (Eric Evans)](https://www.domainlanguage.com/ddd/)
---
## 9. License
This software is published under the [MIT License](LICENSE).