Errores comunes de DDD en PHP y cómo evitarlos

Resumen: este artículo recopila los errores más comunes al aplicar Domain-Driven Design en PHP y propone alternativas prácticas para mejorar nombres, estructura de código, modelos y límites entre capas.

Introducción

DDD no es una receta fija: son decisiones de modelado. Este artículo identifica malas prácticas frecuentes en proyectos PHP y muestra alternativas que ayudan a mantener el dominio expresivo, testable y consistente.

Prerrequisitos

Se asume familiaridad básica con PHP moderno, PSR, namespaces y conceptos OOP (clases, interfaces, visibilidad). También conviene conocer el vocabulario básico de DDD (aggregate, repository, value object).

Desarrollo

A continuación se resumen problemas comunes y recomendaciones concretas extraídas del análisis de proyectos reales.

Procedimiento

  1. Forzar y usar un lenguaje ubicuo (ubiquitous language) consistente en nombres, namespaces y propiedades.
  2. Preferir una organización de dominio orientada al modelo (Model) en lugar de carpetas técnicas por patrón.
  3. Usar repositorios que trabajen con agregados; evitar DAOs que exponen CRUD arbitrario.
  4. Generar identidad en el dominio (UUID/ULID o una estrategia de nextIdentity en el repositorio).
  5. Diferenciar claramente PO (Parameter Object), VO (Value Object) y DTO y aplicar cada uno en su lugar.

Cada punto se ilustra con ejemplos y patrones a continuación.

Ejemplos

Namespaces y raíz significativa — evite App si puede usar un namespace alineado al dominio:

<?php
namespace App\IAM\Domain\User;

// Mejor: nombre alineado al contexto de negocio

namespace ECommerce\IAM\Domain\User;
Lenguaje del código: PHP (php)

Organización de carpetas: agrupar por Model en lugar de fragmentar por patrones tácticos mejora la navegación y la expresividad.

<?php
namespace ECommerce\IAM\Domain\Model\User;
namespace ECommerce\IAM\Domain\Model\Credentials;
Lenguaje del código: PHP (php)

Nombre de repositorio y propiedad inyectada: prefiera nombres del dominio en la propiedad para leer mejor el código.

<?php
namespace ECommerce\IAM\Application\CreateUser;

use ECommerce\IAM\Domain\Model\UserRepository;

class CreateUserHandler
{
    public function __construct(private readonly UserRepository $users) {}

    public function __invoke(CreateUserCommand $command)
    {
        $user = User::create($command->email, $command->password);

        $this->users->add($user);
    }
}
Lenguaje del código: PHP (php)

Getters: en DDD es preferible usar nombres que reflejen el dominio (username() en vez de getUsername()).

<?php
final class User
{
    private string $username;

    public function username(): string
    {
        return $this->username;
    }
}
Lenguaje del código: PHP (php)

Anémico vs rico: coloque comportamiento y reglas en la entidad para proteger invariantes.

<?php
// Anémico (anti-pattern)
final class User
{
    public function __construct(
        private Uuid $id,
        private string $name,
        private string $email,
    ) {}

    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
}

// Rico (recomendado)
final class User
{
    public function __construct(
        private Uuid $id,
        private string $name,
        private string $email,
    ) {}

    public function changeName(string $newName): void
    {
        // validar reglas de negocio
        $this->name = $newName;
    }
}
Lenguaje del código: PHP (php)

DAO vs Repository: un DAO expone CRUD de bajo nivel; un Repository trabaja con agregados y oculta la persistencia.

<?php
// DAO (operaciones SQL, arreglos)
final class UserDAO
{
    public function find(int $id): array { /* ... */ }
    public function insert(array $data): void { /* ... */ }
    public function update(int $id, array $data): void { /* ... */ }
}

// Repository (trabaja con agregados)
final class MySQLUserRepository implements UserRepository
{
    public function byId(UserId $id): ?User { /* ... */ }
    public function add(User $user): void { /* ... */ }
}
Lenguaje del código: PHP (php)

Identidad: evite depender de auto-increment del motor. Genere identidad en el dominio (UUID/ULID) o exponga nextIdentity() desde el repositorio.

<?php
use Ramsey\Uuid\Uuid;

final class UserId
{
    private function __construct(private string $value) {}

    public static function generate(): self
    {
        return new self(Uuid::uuid4()->toString());
    }

    public function asString(): string
    {
        return $this->value;
    }
}
Lenguaje del código: PHP (php)

DTO, Parameter Object y Value Object: defínalos por su propósito y no los confundas.

<?php
// Parameter Object: agrupa parámetros para llamadas
deconstructing
readonly class Credentials
{
    public function __construct(public string $email, public string $password) {}
}

// DTO: usado para exponer datos fuera del dominio
final readonly class UserShowDTO
{
    public function __construct(public string $email, public string $name) {}
}

// Value Object: inmutable y con invariantes
final readonly class FullName
{
    private function __construct(private string $firstName, private string $lastName) {}

    public static function create(string $firstName, string $lastName): self
    {
        // validar invariantes aquí
        return new self($firstName, $lastName);
    }

    public function asString(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
}
Lenguaje del código: PHP (php)

Checklist

  1. Revisar nombres y namespaces: ¿hablan el lenguaje del dominio?
  2. ¿La estructura de carpetas refleja conceptos del negocio (Model) en vez de patrones técnicos?
  3. ¿Las entidades encapsulan comportamiento o son solo DTOs con setters?
  4. ¿Los repositorios trabajan con agregados y no exponen update() genérico?
  5. ¿La identidad se genera en el dominio (UUID/nextIdentity) en lugar de setId() posterior a la persistencia?
  6. ¿Se diferencian claramente PO, VO y DTO en los límites del sistema?

Conclusión

DDD efectivo en PHP depende de decisiones de modelado: imponer lenguaje ubicuo, poner comportamiento en las entidades, usar repositorios centrados en agregados y mantener fronteras claras entre PO/VO/DTO. Aplicar estas prácticas mejora claridad, seguridad y testabilidad.

Si quieres, puedo preparar una segunda parte con más ejemplos prácticos o refactorizaciones paso a paso.

Comments

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *