PHPackages                             dbravoan/dba-ddd-skeleton - 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. dbravoan/dba-ddd-skeleton

ActiveLibrary[Framework](/categories/framework)

dbravoan/dba-ddd-skeleton
=========================

A skeleton package for implementing DDD and Hexagonal Architecture in Laravel.

v1.5.0(1mo ago)04MITPHPPHP ^8.1

Since Feb 13Pushed 1mo agoCompare

[ Source](https://github.com/dbravoan/dba-ddd-skeleton)[ Packagist](https://packagist.org/packages/dbravoan/dba-ddd-skeleton)[ Docs](https://github.com/dbravoan/dba-ddd-skeleton)[ RSS](/packages/dbravoan-dba-ddd-skeleton/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (12)Versions (8)Used By (0)

DBA DDD Skeleton
================

[](#dba-ddd-skeleton)

[![Latest Stable Version](https://camo.githubusercontent.com/4259fbfcb2de93cbb23051d7a9308519749de75b55dffcceb225854a303dbe9a/68747470733a2f2f706f7365722e707567782e6f72672f64627261766f616e2f6462612d6464642d736b656c65746f6e2f762f737461626c65)](https://packagist.org/packages/dbravoan/dba-ddd-skeleton)[![Total Downloads](https://camo.githubusercontent.com/6f7140b7a91d8a0a3175d029b958ee3abc2b89c258f543ebdd8a5b1776087245/68747470733a2f2f706f7365722e707567782e6f72672f64627261766f616e2f6462612d6464642d736b656c65746f6e2f646f776e6c6f616473)](https://packagist.org/packages/dbravoan/dba-ddd-skeleton)[![License](https://camo.githubusercontent.com/f36349978215fbb93b23a79ac451eec548a76d56fb77d0930210fb7cbb844fb5/68747470733a2f2f706f7365722e707567782e6f72672f64627261766f616e2f6462612d6464642d736b656c65746f6e2f6c6963656e7365)](https://packagist.org/packages/dbravoan/dba-ddd-skeleton)

Este paquete proporciona una estructura robusta y herramientas para implementar **Domain-Driven Design (DDD)** y **Arquitectura Hexagonal** en aplicaciones Laravel de forma modular y desacoplada.

Está diseñado para ser instalado como una dependencia (`composer require`) en tu proyecto Laravel, proporcionando las capas base, abstracciones y comandos necesarios para construir módulos de dominio sin reinventar la rueda.

> "No inyectes lógica de negocio en tus controladores. No acoples tu dominio a tu framework."

Este paquete no es solo una colección de archivos; es una propuesta arquitectónica diseñada para resolver los problemas comunes en proyectos Laravel que crecen descontroladamente: *Fat Controllers, Modelos gigantes y lógica dispersa*.

---

📚 Introducción Teórica
----------------------

[](#-introducción-teórica)

### ¿Por qué complicarnos la vida?

[](#por-qué-complicarnos-la-vida)

En un proyecto pequeño (CRUD), el patrón MVC estándar de Laravel es perfecto. Pero cuando la lógica de negocio crece, surgen problemas:

1. **Acoplamiento**: Tu lógica de negocio depende de Eloquent, de las Requests HTTP y del Framework. Si mañana quieres cambiar algo, sufres.
2. **Testabilidad**: Para testear una regla de negocio simple, necesitas levantar todo el framework (Feature Tests lentos) en lugar de testear la clase aislada (Unit Tests rápidos).
3. **Mantenibilidad**: ¿Dónde está la lógica de "Calcular Precio con Descuento"? ¿En el Modelo? ¿En el Controlador? ¿En un Helper?

### La Solución: Capas Concéntricas

[](#la-solución-capas-concéntricas)

Este esqueleto propone dividir tu código en tres capas con responsabilidades claras y estrictas:

1. **Dominio (`Domain`)**:

    - *Qué es*: El núcleo de tu negocio. Clases PHP puras. Entidades, Value Objects, Interfaces de Repositorio.
    - *Regla de Oro*: **Cero dependencias del framework**. Aquí no existe `Illuminate\` ni `Eloquent`.
2. **Aplicación (`Application`)**:

    - *Qué es*: Los casos de uso de tu sistema. "Crear Usuario", "Buscar Producto", "Aplicar Descuento".
    - *Cómo funciona*: Recibe una petición (DTO), orquesta las entidades del Dominio y persiste los cambios usando Interfaces.
    - *Estructura*: Bus de Comandos (Command/Query + Handler) o Servicios de Aplicación.
3. **Infraestructura (`Infrastructure`)**:

    - *Qué es*: El mundo real. La implementación técnica.
    - *Componentes*: Controladores HTTP, Implementaciones de Repositorios (Eloquent), Colas (Redis), APIs externas.
    - *Responsabilidad*: Conectar el mundo exterior con tu capa de Aplicación.

---

🚀 Características
-----------------

[](#-características)

- **Separación Estricta de Capas**: Domain, Application e Infrastructure.
- **Generadores de Código**: Comandos `artisan` para crear módulos completos con un solo comando.
- **Domain Events**: Publicación automática de eventos de dominio desde el repositorio. Tu lógica nunca se olvida de publicar.
- **Criteria Pattern**: Sistema de filtros, ordenación y paginación avanzado y desacoplado de Eloquent.
- **Bus de Mensajes**: Abstracciones para Command Bus, Query Bus y Event Bus con implementaciones nativas de Laravel.
- **Repositorios**: Interfaces y contratos para desacoplar la persistencia.
- **Value Objects**: Primitivos de dominio listos para usar (Uuid, Email, etc.).

---

📦 Instalación
-------------

[](#-instalación)

Requiere el paquete en tu proyecto Laravel:

```
composer require dbravoan/dba-ddd-skeleton
```

### Publicar Configuración (Opcional)

[](#publicar-configuración-opcional)

Si necesitas personalizar los stubs (plantillas de código) que usa el generador:

```
php artisan vendor:publish --tag=dba-ddd-skeleton-stubs
```

---

🛠️ Configuración Inicial
------------------------

[](#️-configuración-inicial)

### 1. Estructura de Directorios

[](#1-estructura-de-directorios)

Por defecto, este paquete asume que tu código de dominio vivirá en `src/`, fuera de la carpeta `app/` estándar de Laravel, para mantenerlo agnóstico al framework.

Asegúrate de configurar tu `composer.json` para cargar las clases desde `src/`.

**Ejemplo `composer.json`:**

```
"autoload": {
    "psr-4": {
        "App\\": "app/",
        "MyCompany\\Context\\": "src/Context/"
    }
}
```

*Recuerda ejecutar `composer dump-autoload` después de cambiar esto.*

### 2. Service Provider

[](#2-service-provider)

El paquete incluye un `DddSkeletonServiceProvider` que se auto-descubre. Registra automáticamente:

- **`CommandBus`** → `LaravelCommandBus` (síncrono, reflexión)
- **`QueryBus`** → `LaravelQueryBus` (síncrono, reflexión)
- **`EventBus`** → `LaravelEventBus` (síncrono, sobreescribible a async)

Para registrar tus propios handlers y repositorios, consulta la sección **[Service Providers](#-service-providers-organizaci%C3%B3n-del-contenedor)**.

### 3. Qué incluye el Paquete

[](#3-qué-incluye-el-paquete)

```
Shared/
├── Domain/
│   ├── Aggregate/       AggregateRoot (record, pullDomainEvents)
│   ├── Bus/
│   │   ├── Command/     Command, CommandBus, CommandHandler (interfaces)
│   │   ├── Event/       DomainEvent, EventBus, DomainEventSubscriber (interfaces)
│   │   └── Query/       Query, QueryBus, QueryHandler, Response (interfaces)
│   ├── Criteria/        Criteria, Filters, FilterGroup, Order (pattern completo)
│   ├── Security/        SqlInjectionProtector
│   └── ValueObject/     Uuid, StringValueObject, IntValueObject, etc.
├── Infrastructure/
│   ├── Bus/
│   │   ├── Command/     LaravelCommandBus, CommandNotRegisteredError
│   │   ├── Event/
│   │   │   └── Laravel/ LaravelEventBus, LaravelQueueEventBus, ProcessDomainEventJob
│   │   └── Query/       LaravelQueryBus, QueryNotRegisteredError
│   ├── Criteria/        RequestCriteriaBuilder
│   ├── Laravel/         ApiController, Providers/RepositoryServiceProvider
│   └── Persistence/
│       ├── Eloquent/    EloquentRepository, EloquentCriteriaConverter
│       └── QueryBuilder/ QueryBuilderRepository, QueryBuilderCriteriaConverter
└── Console/
    └── Commands/        MakeModuleCommand + 25 stubs

```

---

💡 Uso: Creando un Nuevo Módulo
------------------------------

[](#-uso-creando-un-nuevo-módulo)

La funcionalidad estrella de este paquete es el generador de módulos. Olvida crear carpetas y archivos a mano.

Ejecuta el siguiente comando para crear un nuevo módulo (ej: `Product` dentro del contexto `Catalog`):

```
# php artisan dba:make:module
php artisan dba:make:module Catalog Product
```

Esto generará automáticamente la siguiente estructura en `src/Catalog/Product`:

```
src/Catalog/Product/
├── Application/
│   ├── Create/          # Caso de uso de creación (Command + Handler)
│   ├── Delete/          # Caso de uso de borrado
│   ├── Find/            # Caso de uso de búsqueda (Query + Handler)
│   ├── Response/        # DTOs de respuesta (Response + Responses)
│   ├── SearchByCriteria/# Búsqueda y conteo por criterios
│   └── Update/          # Caso de uso de actualización
├── Domain/
│   ├── Product.php                     # Entidad / Agregado
│   ├── ProductCreatedDomainEvent.php   # Evento de dominio
│   ├── ProductId.php                   # Value Object
│   ├── ProductName.php                 # Value Object
│   └── ProductRepository.php          # Interfaz del Repositorio
└── Infrastructure/
    ├── Controller/
    │   ├── CreateProductController.php
    │   ├── DeleteProductController.php
    │   ├── FindProductController.php
    │   ├── UpdateProductController.php
    │   └── SearchProductsByCriteriaController.php
    └── Persistence/
        └── EloquentProductRepository.php

```

### Inyección de Dependencias

[](#inyección-de-dependencias)

Para que Laravel sepa qué implementación usar cuando inyectas una interfaz de dominio, debes hacer el binding en un ServiceProvider. Consulta la sección **[Service Providers](#-service-providers-organizaci%C3%B3n-del-contenedor)** para ver cómo crear un `RepositoryServiceProvider` y un `DomainServiceProvider` dedicados.

---

🏗️ Anatomía de un Caso de Uso (Ej: Crear Producto)
--------------------------------------------------

[](#️-anatomía-de-un-caso-de-uso-ej-crear-producto)

Veamos el flujo de datos completo para entender el desacoplamiento:

1. **El Controlador (`Infrastructure`)**Recibe el Request HTTP. Su única misión es extraer los datos, encapsularlos en un DTO (*Command*) y despacharlos al Bus.

    ```
    public function __invoke(Request $request): JsonResponse {
        $command = new CreateProductCommand($request->input('name')...);
        $this->bus->dispatch($command); // Desacoplamiento total
        return response()->json(..., 201);
    }
    ```
2. **El Comando (`Application`)**Es un DTO (Data Transfer Object) inmutable. Solo transporta datos, no tiene lógica.

    ```
    final class CreateProductCommand {
        public function __construct(private string $name) {}
        public function name(): string { return $this->name; }
    }
    ```
3. **El Manejador (`Application`)**Recibe el Comando y ejecuta la lógica. Orquesta el dominio.

    ```
    final class CreateProductCommandHandler {
        public function __construct(private ProductRepository $repository) {}

        public function __invoke(CreateProductCommand $command): void {
            $product = Product::create(new ProductId(...), new ProductName(...));
            $this->repository->save($product);
        }
    }
    ```

    *Nota: Aquí usamos la interfaz `ProductRepository`, no Eloquent directamente. Esto nos permite testear este Handler con un MockRepository sin tocar la base de datos.*
4. **El Repositorio (`Infrastructure`)**La implementación real que habla con la base de datos. Además, **publica automáticamente los eventos de dominio** tras persistir el agregado.

    ```
    final class EloquentProductRepository extends EloquentRepository implements ProductRepository {
        public function save(Product $product): void {
            $this->model->updateOrCreate(
                ['id' => $product->id()->value()],
                $product->toPrimitives()
            );

            // Publica automáticamente los Domain Events grabados en el agregado
            $this->publishEvents($product);
        }
    }
    ```

---

📣 Domain Events: Publicación Automática
---------------------------------------

[](#-domain-events-publicación-automática)

Los Domain Events son la forma en que un agregado comunica que algo ha ocurrido en el dominio. Este skeleton implementa el patrón de forma **automática y transparente** desde la capa de infraestructura.

### Flujo Completo

[](#flujo-completo)

 ```
sequenceDiagram
    participant H as CommandHandler
    participant A as AggregateRoot
    participant R as Repository
    participant EB as EventBus
    participant S as Subscriber

    H->>A: Product::create(...)
    A->>A: record(ProductCreatedDomainEvent)
    H->>R: save($product)
    R->>R: Persist to DB (Eloquent)
    R->>A: pullDomainEvents()
    A-->>R: [ProductCreatedDomainEvent]
    R->>EB: publish(...$events)
    EB->>S: notify(ProductCreatedDomainEvent)
```

      Loading ### ¿Cómo funciona?

[](#cómo-funciona)

1. **El Agregado graba eventos** — Al ejecutar una acción de dominio (ej: `Product::create()`), la entidad llama internamente a `$this->record(new ProductCreatedDomainEvent(...))`. Los eventos se acumulan en memoria.
2. **El Repositorio publica** — Tras persistir el agregado, el repositorio llama a `$this->publishEvents($product)`. Este método (heredado de `EloquentRepository`) hace `pullDomainEvents()` del agregado y los envía al `EventBus`.
3. **El EventBus despacha** — El bus recorre los subscribers registrados y ejecuta la lógica reactiva (enviar email, actualizar caché, sincronizar otro bounded context, etc.).

### Anatomía de un Domain Event

[](#anatomía-de-un-domain-event)

Cada módulo genera automáticamente su evento `Created`. Puedes crear más eventos siguiendo el mismo patrón:

```
final class ProductCreatedDomainEvent extends DomainEvent
{
    public function __construct(
        string $aggregateId,
        private readonly string $name,
        string $eventId = null,
        string $occurredOn = null
    ) {
        parent::__construct($aggregateId, $eventId, $occurredOn);
    }

    // Serialización para colas/persistencia
    public static function fromPrimitives(string $aggregateId, array $body, string $eventId, string $occurredOn): self
    {
        return new self($aggregateId, $body['name'], $eventId, $occurredOn);
    }

    public static function eventName(): string { return 'product.created'; }

    public function toPrimitives(): array { return ['name' => $this->name]; }
}
```

### Binding del EventBus

[](#binding-del-eventbus)

El `DddSkeletonServiceProvider` ya registra automáticamente el `LaravelEventBus` como implementación de `EventBus`. Solo necesitas **etiquetar tus subscribers** para que el bus los descubra:

```
// En tu DomainServiceProvider (ver sección Service Providers)
$this->app->tag([
    SendWelcomeEmailOnUserCreated::class,
    CreateAuditLogOnUserCreated::class,
], 'dba_ddd.domain_event_subscriber');
```

> **Nota**: Si no registras ningún subscriber, los repositorios funcionan igualmente. Los eventos simplemente se descartan. Esto permite una adopción gradual.

---

🔧 Service Providers: Organización del Contenedor
------------------------------------------------

[](#-service-providers-organización-del-contenedor)

Para mantener la arquitectura limpia y desacoplada, recomendamos crear **dos Service Providers dedicados** en tu aplicación Laravel. Estos no vienen en el paquete — los creas tú porque contienen los bindings específicos de **tu** dominio.

### RepositoryServiceProvider

[](#repositoryserviceprovider)

Responsable de vincular cada interfaz de repositorio del dominio con su implementación de infraestructura (Eloquent).

```
php artisan make:provider RepositoryServiceProvider
```

```
