En proyectos Laravel, es común que los modelos Eloquent tengan atributos como simples strings, números o fechas. Pero… ¿qué pasa cuando queremos que esos valores tengan reglas de negocio y validaciones propias? Ahí es donde entran los Value Objects, una técnica muy utilizada en arquitectura limpia y Domain-Driven Design (DDD). En este post te voy a mostrar cómo utilizar Value Objects en modelos Eloquent para enriquecer tu dominio, mejorar la calidad del código y evitar errores silenciosos.
Un Value Object es un objeto inmutable que representa un valor del dominio con sus propias reglas y comportamientos.
No se identifica por un ID, sino por su valor. Ejemplos típicos:
En resumen: un Value Object encapsula un valor y sus reglas, de modo que no haya forma de crear un valor inválido.
Laravel es muy flexible y nos permite integrarlos de forma simple pero poderosa.
Con Custom Casts podemos lograr que un atributo de base de datos se convierta automáticamente a un objeto de dominio cuando lo obtenemos, y viceversa cuando lo guardamos.
Beneficio | Descripción |
---|---|
Validaciones centralizadas | Las reglas de negocio viven en el Value Object, no en varios lugares del código. |
Inmutabilidad | Una vez creado, el valor no se puede modificar de forma insegura. |
Código más expresivo | El tipo de dato transmite su intención y reglas de uso. |
Menos errores | No podés guardar valores inválidos accidentalmente. |
Supongamos que tenemos usuarios y queremos que el campo email
siempre sea válido.
<?php
namespace App\ValueObjects;
final class Email
{
public function __construct(private string $address)
{
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("El email '{$address}' no es válido");
}
}
public function value(): string
{
return $this->address;
}
public function domain(): string
{
return substr(strrchr($this->address, "@"), 1);
}
public function __toString(): string
{
return $this->address;
}
}
📌 Claves:
domain()
para enriquecer el valor.<?php
namespace App\Casts;
use App\ValueObjects\Email;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
final class EmailCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): Email
{
return new Email($value);
}
public function set($model, string $key, $value, array $attributes): string
{
if ($value instanceof Email) {
return $value->value();
}
return (string) $value;
}
}
📌 Claves:
get
devuelve un Email
al leer de la DB.set
guarda un string al escribir.<?php
namespace App\Models;
use App\Casts\EmailCast;
use Illuminate\Database\Eloquent\Model;
final class User extends Model
{
protected $casts = [
'email' => EmailCast::class,
];
}
use App\Models\User;
use App\ValueObjects\Email;
// Crear usuario
$user = new User();
$user->name = 'Juan Pérez';
$user->email = new Email('juan@example.com');
$user->save();
// Obtener usuario y usar métodos del Value Object
$user = User::first();
echo "Dominio del email: " . $user->email->domain();
Podemos tener varios atributos mapeados a Value Objects:
<?php
namespace App\Models;
use App\Casts\EmailCast;
use App\Casts\PriceCast;
use Illuminate\Database\Eloquent\Model;
final class Product extends Model
{
protected $casts = [
'email_contacto' => EmailCast::class,
'precio' => PriceCast::class,
];
}
Esto te permite que cada valor importante en tu dominio tenga su propia lógica encapsulada.
Me dedico a crear soluciones web eficientes y a compartir mi conocimiento con la comunidad de desarrolladores.