PHPackages                             ahmed-bhs/hexagonal-maker-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. ahmed-bhs/hexagonal-maker-bundle

ActiveSymfony-bundle[Framework](/categories/framework)

ahmed-bhs/hexagonal-maker-bundle
================================

Hexagonal Architecture Maker Bundle for Symfony - Generate Commands, Queries, and more with CQRS pattern

v0.1(5mo ago)812MITPHPPHP &gt;=8.1CI passing

Since Jan 7Pushed 5mo ago3 watchersCompare

[ Source](https://github.com/ahmed-bhs/hexagonal-maker-bundle)[ Packagist](https://packagist.org/packages/ahmed-bhs/hexagonal-maker-bundle)[ Docs](https://github.com/ahmed-bhs/hexagonal-maker-bundle)[ RSS](/packages/ahmed-bhs-hexagonal-maker-bundle/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (2)Used By (0)

Hexagonal Architecture Maker Bundle for Symfony
===============================================

[](#hexagonal-architecture-maker-bundle-for-symfony)

 [![Hexagonal Architecture](docs/images/hexagonal-architecture.jpg)](docs/images/hexagonal-architecture.jpg)

 **A complete Symfony Maker bundle for generating Hexagonal Architecture (Ports &amp; Adapters) components**

 [![Latest Version](https://camo.githubusercontent.com/f773b6d2a1383e9fb12d8156fd5a4fd22aa07bc934ff1ae6966bfdaa26cd59b2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61686d65642d6268732f68657861676f6e616c2d6d616b65722d62756e646c652e737667)](https://packagist.org/packages/ahmed-bhs/hexagonal-maker-bundle) [![CI Status](https://github.com/ahmed-bhs/hexagonal-maker-bundle/workflows/CI/badge.svg)](https://github.com/ahmed-bhs/hexagonal-maker-bundle/actions) [![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE) [![PHP Version](https://camo.githubusercontent.com/7663c9d53dc13cedaf0660a8745a7e77d2dd711257f36aa86ebce12a0600ef42/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d626c75652e737667)](https://www.php.net/) [![Symfony](https://camo.githubusercontent.com/4d5f299aa838ce346847dafd7e62b76d8023d38f953ed3ec63c22611a21bc83c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73796d666f6e792d362e34253230253743253230372e782d626c75652e737667)](https://symfony.com/)

### ☕ Support This Project

[](#-support-this-project)

If this project helped you or saved you time, consider buying me a coffee!

[![Buy Me A Coffee](https://camo.githubusercontent.com/746cc9ad4a76e292d2944acca7de5316703f986f785af02174280b38c7c8adac/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4275792532304d6525323041253230436f666665652d537570706f72742d4646444430303f7374796c653d666f722d7468652d6261646765266c6f676f3d6275792d6d652d612d636f66666565266c6f676f436f6c6f723d626c61636b)](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
-----------------

[](#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
-----------

[](#quick-start)

```
# 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-features)

### 1.1 Core CQRS Components

[](#11-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

[](#12-complete-maker-commands-summary)

**18 makers covering all hexagonal layers + tests + events + rapid CRUD:**

LayerMaker CommandWhat it generates**Domain**`make:hexagonal:entity`Domain entities + XML mapping + AggregateRoot support**Domain**`make:hexagonal:value-object`Immutable VOs with patterns (Money/Status/ID) + Doctrine types**Domain**`make:hexagonal:exception`Business exceptions with factory methods**Domain**`make:hexagonal:domain-event`Domain events with aggregate support**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
-----------------------------

[](#2-why-hexagonal-architecture)

> **📚 [Read the complete guide: WHY-HEXAGONAL.md](WHY-HEXAGONAL.md)**

### 2.1 What the Founders Say

[](#21-what-the-founders-say)

#### Alistair Cockburn - Creator of Hexagonal Architecture (2005)

[](#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)

[](#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)

[](#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)

[](#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

[](#22-key-principles-from-the-masters)

PrincipleAuthorMeaning**Dependency Inversion**Uncle BobHigh-level modules should not depend on low-level modules. Both should depend on abstractions.**Ports &amp; Adapters**Alistair CockburnThe core defines ports (interfaces), the outside world provides adapters (implementations).**Screaming Architecture**Uncle BobYour architecture should scream what the application does, not what framework it uses.**Ubiquitous Language**Eric EvansThe code should speak the language of the domain experts, not technical jargon.**Isolation**AllBusiness logic must be isolated from technical concerns (UI, DB, frameworks).### Quick Summary

[](#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

[](#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
---------------

[](#3-installation)

```
composer require ahmedbhs/hexagonal-maker-bundle
```

The bundle will auto-register if you use Symfony Flex. Otherwise, add it to `config/bundles.php`:

```
return [
    // ...
    AhmedBhs\HexagonalMakerBundle\HexagonalMakerBundle::class => ['dev' => true],
];
```

---

4. Complete Architecture Generation
-----------------------------------

[](#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

[](#41-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

[](#411-step-by-step-architecture-generation)

```
# 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 --aggregate-root --events=UserRegistered,UserActivated

# 1.2 Create Domain Exceptions (with factory method patterns)
bin/console make:hexagonal:exception user/account UserNotFoundException --pattern=not-found --entity=User
bin/console make:hexagonal:exception user/account InvalidUserStateException --pattern=invalid-state --entity=User

# 1.3 Create Value Objects (with patterns and Doctrine types)
bin/console make:hexagonal:value-object user/account UserId --pattern=id
bin/console make:hexagonal:value-object user/account Email
bin/console make:hexagonal:value-object user/account AccountBalance --pattern=money --with-doctrine-type --storage-type=integer

# 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

[](#412-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)
│       ├── In/                                       # Input/Driving Ports (Primary)
│       │   └── CreateUserUseCaseInterface.php       ← make:hexagonal:port --type=in
│       └── Out/                                      # Output/Driven Ports (Secondary)
│           └── 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

[](#413-understanding-the-architecture)

LayerResponsibilityDependenciesMakers 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, DTOsDomain 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)

[](#42-dependency-flow-hexagonal-rule)

 ```
%%{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 & QueriesUse Cases"]
        Reg["• RegisterCommand"]
        Find["• FindByIdQuery"]

        Commands --- Reg
        Commands --- Find
    end

    subgraph DOMAIN["💎 DOMAIN LAYER - CORE"]
        Entities["📦 Entities & Value Objects"]
        EntList["• User• Email, UserId"]
        Ports["🔗 PortsInterfaces"]
        PortList["• UserRepositoryInterface"]

        Entities --- EntList
        Ports --- PortList
    end

    subgraph INFRA["🔌 INFRASTRUCTURE LAYER"]
        Adapters["🔧 AdaptersImplementations"]
        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
```

      Loading **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

[](#43-quick-start-5-command-complete-module)

Want to generate a complete module in just 5 commands? Here's a copy-paste ready script:

```
# 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
-------------------

[](#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)

[](#51-create-a-command-write-operation)

Generate a CQRS Command for state-changing operations:

```
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:**

```
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:**

```
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:**

```
bin/console make:hexagonal:command user/account register --factory --with-tests
```

### 5.2 Create a Query (Read Operation)

[](#52-create-a-query-read-operation)

Generate a CQRS Query for data retrieval:

```
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)

[](#53-create-a-repository-port--adapter)

Generate a repository interface (Port) and its infrastructure implementation (Adapter):

```
bin/console make:hexagonal:repository user/account User
```

**Generated files (with `port_style: in_out`):**

```
src/User/Account/
├── Domain/Port/Out/                       # Output/Driven Port (Secondary)
│   └── UserRepositoryInterface.php        # Port (interface)
└── Infrastructure/Persistence/Doctrine/
    └── DoctrineUserRepository.php         # Adapter (implementation)

```

### 5.3.1 Create a Port Interface

[](#531-create-a-port-interface)

Generate standalone port interfaces for Input (driving) or Output (driven) ports:

```
# Create an Output Port (external service interface)
bin/console make:hexagonal:port user/account EmailNotifier --type=out

# Create an Input Port (use case interface)
bin/console make:hexagonal:port user/account CreateUser --type=in
```

**Generated files:**

```
src/User/Account/Domain/Port/
├── In/
│   └── CreateUserUseCaseInterface.php    # Input port (what app offers)
└── Out/
    └── EmailNotifierInterface.php         # Output port (what app needs)

```

### 5.4 Create a Domain Entity

[](#54-create-a-domain-entity)

Generate a domain entity in the core layer with optional AggregateRoot support:

```
# Basic entity
bin/console make:hexagonal:entity user/account User --properties=email:string,age:int

# Aggregate root with domain events
bin/console make:hexagonal:entity order/ordering Order \
  --aggregate-root \
  --events=OrderPlaced,OrderConfirmed,OrderCancelled \
  --properties=customerId:string,total:int
```

**Options:**

- `--properties` - Entity properties (format: `name:type,name:type`)
- `--aggregate-root` - Add AggregateRoot trait for domain events
- `--events` - Domain events to generate (comma-separated)
- `--with-repository` - Generate repository interface + implementation
- `--with-id-vo` - Generate ID value object

**Generated files:**

```
src/User/Account/Domain/Model/
└── User.php  # Domain entity with business logic

src/User/Account/Infrastructure/Persistence/Doctrine/Orm/Mapping/Model/
└── User.orm.xml  # Doctrine XML mapping

```

**AggregateRoot example:**

```
final class Order
{
    use AggregateRoot;

    public static function create(string $id, string $customerId): self
    {
        $order = new self($id, $customerId);
        $order->recordThat(new OrderPlaced($id, $customerId, new \DateTimeImmutable()));
        return $order;
    }
}
```

### 5.5 Create a Value Object

[](#55-create-a-value-object)

Generate an immutable value object with optional patterns and Doctrine type:

```
# Basic value object
bin/console make:hexagonal:value-object user/account Email

# Money pattern (with arithmetic operations)
bin/console make:hexagonal:value-object order/catalog Price \
  --pattern=money \
  --with-doctrine-type \
  --storage-type=integer

# Status pattern (with state machine)
bin/console make:hexagonal:value-object order/ordering OrderStatus \
  --pattern=status \
  --statuses=pending,confirmed,shipped,delivered \
  --transitions="pending:confirmed;confirmed:shipped;shipped:delivered"

# ID pattern (with UUID validation)
bin/console make:hexagonal:value-object user/account UserId --pattern=id
```

**Options:**

- `--pattern` - VO pattern: `money`, `status`, `id`, `generic`
- `--with-doctrine-type` - Generate Doctrine DBAL custom type
- `--storage-type` - DB storage: `string`, `integer`, `text`, `json`
- `--statuses` - For status pattern: comma-separated values
- `--transitions` - For status pattern: state machine transitions

**Generated files:**

```
src/User/Account/Domain/ValueObject/
└── Email.php  # Immutable value object with validation

# With --with-doctrine-type:
src/Order/Catalog/Infrastructure/Persistence/Doctrine/Type/
└── PriceType.php  # Doctrine DBAL type (auto-registered in doctrine.yaml)

```

**Money pattern features:**

- `add()`, `subtract()`, `multiply()` - Arithmetic operations
- `isZero()`, `isGreaterThan()`, `equals()` - Comparisons
- `format()` - Currency formatting

**Status pattern features:**

- State constants and `TRANSITIONS` array
- `canTransitionTo()` - State machine validation
- Factory methods: `pending()`, `confirmed()`, etc.
- Query methods: `isPending()`, `isConfirmed()`, etc.

### 5.6 Create a Domain Exception

[](#56-create-a-domain-exception)

Generate a business exception with optional factory method patterns:

```
# Basic exception
bin/console make:hexagonal:exception user/account InvalidEmailException

# Not Found pattern (with factory methods)
bin/console make:hexagonal:exception order/ordering OrderNotFoundException \
  --pattern=not-found \
  --entity=Order

# Invalid State pattern
bin/console make:hexagonal:exception order/ordering InvalidOrderStateException \
  --pattern=invalid-state \
  --entity=Order

# Already Exists pattern
bin/console make:hexagonal:exception user/account UserAlreadyExistsException \
  --pattern=already-exists \
  --entity=User
```

**Options:**

- `--pattern` - Exception pattern: `not-found`, `invalid-state`, `already-exists`
- `--entity` - Related entity name for factory methods

**Generated files:**

```
src/User/Account/Domain/Exception/
└── InvalidEmailException.php  # Domain exception for business rule violations

```

**Not Found pattern example:**

```
final class OrderNotFoundException extends \DomainException
{
    public static function withId(string $id): self
    {
        return new self(sprintf('Order with ID "%s" was not found.', $id));
    }

    public static function withCriteria(string $criteria): self { /* ... */ }
    public static function forOrder(string $identifier): self { /* ... */ }
}
```

**Invalid State pattern example:**

```
final class InvalidOrderStateException extends \DomainException
{
    public static function cannotTransition(string $from, string $to): self { /* ... */ }
    public static function invalidOperation(string $operation, string $state): self { /* ... */ }
}
```

### 5.7 Create an Input DTO

[](#57-create-an-input-dto)

Generate an input DTO with validation constraints:

```
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

[](#58-create-a-use-case)

Generate a use case (application service):

```
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)

[](#59-create-a-web-controller-ui-layer)

Generate a web controller for HTTP requests:

```
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

[](#510-create-a-symfony-form)

Generate a Symfony form type:

```
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)

[](#511-create-a-cli-command-ui-layer)

Generate a console command:

```
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:**

```
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)

[](#512-create-a-use-case-test-tests)

Generate a test for your use case (Application layer):

```
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)

[](#513-create-a-controller-test-tests)

Generate a test for your web controller (UI layer):

```
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)

[](#514-create-a-cli-command-test-tests)

Generate a test for your console command (UI layer):

```
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)

[](#515-create-a-domain-event-domain-layer)

Generate an immutable domain event with aggregate and properties:

```
# Basic event
bin/console make:hexagonal:domain-event order/payment OrderPlaced

# With aggregate and properties
bin/console make:hexagonal:domain-event order/ordering OrderPlaced \
  --aggregate=Order \
  --properties=customerId:string,totalAmount:int,shippingAddress:string

# With event subscriber
bin/console make:hexagonal:domain-event order/ordering OrderPlaced \
  --aggregate=Order \
  --properties=customerId:string \
  --with-subscriber
```

**Options:**

- `--aggregate` - The aggregate this event belongs to
- `--properties` - Event properties (format: `name:type,name:type`)
- `--with-subscriber` - Generate event subscriber

**Generated files:**

```
src/Order/Payment/Domain/Event/
└── OrderPlacedEvent.php  # Immutable event representing a business fact

```

**Generated event example:**

```
final readonly class OrderPlaced implements DomainEvent
{
    public function __construct(
        public string $orderId,
        public string $customerId,
        public int $totalAmount,
        public \DateTimeImmutable $occurredAt,
    ) {
    }

    public function occurredOn(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }

    public function aggregateId(): string
    {
        return $this->orderId;
    }
}
```

**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)

[](#516-create-an-event-subscriber-application-or-infrastructure)

Generate an event subscriber with layer choice:

```
# 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

[](#517-enhanced-form-with-auto-generated-commandinput)

Generate a form type with optional Command and Input DTO:

```
# 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 🚀
------------------------------------

[](#518-generate-complete-crud-module-)

The most powerful command in the bundle - generate an entire CRUD module in seconds:

```
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:**

```
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:**

```
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:**

```
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 ⚡
--------------------------------------------------------

[](#519-powerful---with--options-for-rapid-development-)

All makers support powerful options to generate related files automatically, dramatically speeding up development:

### Controller: `--with-workflow`

[](#controller---with-workflow)

Generate complete web workflow in one command:

```
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`

[](#entity---with-repository-and---with-id-vo)

Generate entity with repository and ID value object:

```
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`

[](#usecase---with-test)

Generate use case with its test:

```
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`

[](#domainevent---with-subscriber)

Generate event with its subscriber:

```
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`

[](#form---with-command)

Already documented in section 5.17

### CLI Command: `--with-use-case`

[](#cli-command---with-use-case)

Generate CLI command with UseCase workflow:

```
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

[](#summary-table)

MakerOptionGeneratesUse Case`make:hexagonal:controller``--with-workflow`Controller + Form + UseCase + Command + InputComplete web CRUD`make:hexagonal:cli-command``--with-use-case`CLI + UseCase + Command + InputCLI with business logic`make:hexagonal:entity``--with-repository`Entity + Mapping + Port + AdapterEntity with persistence`make:hexagonal:entity``--with-id-vo`Entity + ID ValueObjectTyped IDs`make:hexagonal:use-case``--with-test`UseCase + TestTDD workflow`make:hexagonal:domain-event``--with-subscriber`Event + SubscriberEvent-driven`make:hexagonal:form``--with-command`Form + Command + InputForm workflow`make:hexagonal:crud``--with-tests`Complete CRUD + All testsFull module with tests`make:hexagonal:crud``--with-id-vo`Complete CRUD + ID VOCRUD with typed IDs**Pro Tip:** Combine options for maximum productivity!

```
# 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
----------------

[](#6-configuration)

Create `config/packages/hexagonal_maker.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'

    # Port namespace style: Domain\Port\In (driving) and Domain\Port\Out (driven)
    port_style: 'in_out'
```

### 6.1 Port Namespace Structure

[](#61-port-namespace-structure)

The bundle follows standard hexagonal architecture terminology with `In` and `Out` port namespaces:

```
Domain/
└── Port/
    ├── In/                              # Input/Driving Ports (Primary)
    │   └── CreateUserUseCaseInterface.php   # What the app CAN DO
    └── Out/                             # Output/Driven Ports (Secondary)
        ├── UserRepositoryInterface.php      # What the app NEEDS
        └── EmailNotifierInterface.php

```

- **Port\\In**: Interfaces for use cases - define what the application offers to the outside world (implemented by Application layer)
- **Port\\Out**: Interfaces for external dependencies - define what the application needs from infrastructure (implemented by Infrastructure layer)

**7.1 Customizing Templates**7.1 Customizing Templates
-------------------------

[](#71-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
--------------------

[](#72-testing-strategy)

The bundle generates two types of tests when using `--with-tests`:

### 7.2.1 Unit Tests

[](#721-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:**

```
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

[](#722-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:**

```
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

[](#723-inmemory-repositories)

The bundle also generates InMemory repository implementations for faster unit testing:

```
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
----------------------------

[](#73-doctrine-orm-integration)

### 7.3.1 Pure Domain Entities + XML Mapping

[](#731-pure-domain-entities--xml-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 XML Mapping** (infrastructure concern) - in `Infrastructure/Persistence/Doctrine/Orm/Mapping/Model/`
3. **Auto-configured** `doctrine.yaml` mapping
4. **Auto-configured** `services.yaml` exclusions

This approach maintains **strict separation of concerns** and follows DDD best practices.

### 7.3.2 Generated Files Structure

[](#732-generated-files-structure)

When you run:

```
bin/console make:hexagonal:entity user/account User
```

**Two files are generated:**

**1. Domain Entity (PURE)**

```

```

### 7.3.3 Why XML Mapping in Infrastructure Layer?

[](#733-why-xml-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 (Auto-generated):**

The bundle automatically configures `config/packages/doctrine.yaml`:

```
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        types:
            # Auto-added when using --with-doctrine-type
            user_balance: App\User\Account\Infrastructure\Persistence\Doctrine\Type\AccountBalanceType

    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: true
        mappings:
            # Auto-added when creating entities
            UserAccount:
                is_bundle: false
                type: xml
                dir: '%kernel.project_dir%/src/User/Account/Infrastructure/Persistence/Doctrine/Orm/Mapping/Model'
                prefix: 'App\User\Account\Domain\Model'
                alias: UserAccount
```

The bundle also auto-configures `config/services.yaml` to exclude Domain classes from autowiring:

```
services:
    App\:
        resource: ../src/
        exclude:
            # Auto-added per module
            - ../src/User/Account/Domain/Model/
            - ../src/User/Account/Domain/ValueObject/
```

### 7.3.4 YAML Mapping Examples

[](#734-yaml-mapping-examples)

Here are common YAML mapping patterns you'll use:

**Basic Field Types:**

```
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:**

```
fields:
    email:
        type: string
        length: 180
        unique: true
```

### 7.3.5 Entity Identity Strategies

[](#735-entity-identity-strategies)

**Option 1: UUID (Recommended for DDD)**

```
id:
    id:
        type: uuid
        # Doctrine will automatically use UUID type
```

**Option 2: ULID (Sortable UUID)**

```
id:
    id:
        type: ulid
        # Doctrine will automatically use ULID type
```

**Option 3: String-based UUID**

```
id:
    id:
        type: string
        length: 36
        # Generate UUID in entity constructor
```

**Option 4: Auto-increment**

```
id:
    id:
        type: integer
        generator:
            strategy: AUTO
```

### 7.3.6 Associations (Relationships)

[](#736-associations-relationships)

**One-to-Many:**

```
oneToMany:
    orders:
        targetEntity: App\Domain\Order\Order
        mappedBy: user
        cascade: ['persist', 'remove']
```

**Many-to-One:**

```
manyToOne:
    category:
        targetEntity: App\Domain\Category\Category
        inversedBy: products
        joinColumn:
            name: category_id
            referencedColumnName: id
            nullable: false
```

**Many-to-Many:**

```
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

[](#737-embedded-value-objects)

**Address.orm.yml** (Value Object):

```
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):

```
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

[](#738-database-schema-generation)

After creating/modifying YAML mapping files:

```
# 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

[](#739-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 🎯
----------------------------------------------------

[](#74-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?

[](#741-why-yaml-mapping-for-extensions)

**🌪️ Traditional approach (breaks hexagonal architecture):**

```
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):**

```
// 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
    }
}
```

```
# 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

[](#742-installation)

```
composer require stof/doctrine-extensions-bundle
```

### 7.4.3 Configuration

[](#743-configuration)

**Enable extensions in `config/packages/stof_doctrine_extensions.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

[](#744-available-extensions-with-yaml-examples)

#### 1️⃣ **Sluggable** - Auto-generate URL-friendly slugs

[](#1️⃣-sluggable---auto-generate-url-friendly-slugs)

**Domain Entity:**

```
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:**

```
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

[](#2️⃣-timestampable---auto-manage-createdupdated-dates)

**Domain Entity:**

```
final class Post
{
    private \DateTimeImmutable $createdAt;  // Set automatically
    private \DateTimeImmutable $updatedAt;  // Updated automatically

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}
```

**YAML Mapping:**

```
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)

[](#3️⃣-softdeleteable---logical-deletion-keep-data)

**Domain Entity:**

```
final class Post
{
    private ?\DateTimeImmutable $deletedAt;  // Managed by Gedmo

    public function isDeleted(): bool
    {
        return $this->deletedAt !== null;
    }
}
```

**YAML Mapping:**

```
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:**

```
// 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

[](#4️⃣-blameable---track-who-createdupdated)

**Domain Entity:**

```
final class Post
{
    private string $createdBy;  // User who created
    private string $updatedBy;  // Last user who updated
}
```

**YAML Mapping:**

```
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:**

```
# config/services.yaml
services:
    Gedmo\Blameable\BlameableListener:
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setUserValue, [ '@security.token_storage' ] ]
```

#### 5️⃣ **Translatable** - Multi-language content

[](#5️⃣-translatable---multi-language-content)

**Domain Entity:**

```
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:**

```
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:**

```
// 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

[](#6️⃣-tree-nested-set---hierarchical-structures)

**Domain Entity:**

```
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:**

```
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:**

```
// 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

[](#7️⃣-loggable---entity-change-history)

**Domain Entity:**

```
final class Post
{
    private string $title;     // Versioned
    private string $content;   // Versioned
    // Changes will be logged automatically
}
```

**YAML Mapping:**

```
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:**

```
// 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

[](#745-complete-example-blog-post-with-multiple-extensions)

**Domain Entity (100% Pure):**

```
