Etiqueta: ULID

  • 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.