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
- Forzar y usar un lenguaje ubicuo (ubiquitous language) consistente en nombres, namespaces y propiedades.
- Preferir una organización de dominio orientada al modelo (Model) en lugar de carpetas técnicas por patrón.
- Usar repositorios que trabajen con agregados; evitar DAOs que exponen CRUD arbitrario.
- Generar identidad en el dominio (UUID/ULID o una estrategia de nextIdentity en el repositorio).
- 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
- Revisar nombres y namespaces: ¿hablan el lenguaje del dominio?
- ¿La estructura de carpetas refleja conceptos del negocio (Model) en vez de patrones técnicos?
- ¿Las entidades encapsulan comportamiento o son solo DTOs con setters?
- ¿Los repositorios trabajan con agregados y no exponen update() genérico?
- ¿La identidad se genera en el dominio (UUID/nextIdentity) en lugar de setId() posterior a la persistencia?
- ¿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.
Deja un comentario