Aprende cómo crear casts personalizados implementando la interfaz CastsAttributes. Incluye patrones avanzados como Value Object, casts entrantes y Castables.
Los casts de Eloquent convierten los valores en bruto obtenidos de la base de datos a tipos de PHP y, al guardar, aplican la conversión inversa. Se declaran en el método casts.
Lista de los casts que Laravel ofrece de forma predeterminada.
Cast
Descripción
integer / int
Convierte a entero.
float / double
Convierte a punto flotante.
string
Convierte a cadena.
boolean / bool
Convierte a booleano (incluidos 0/1).
array
Cadena JSON ↔ array de PHP.
collection
Cadena JSON ↔ instancia de Collection.
object
Cadena JSON ↔ instancia stdClass.
datetime
Cadena ↔ instancia de Carbon.
immutable_datetime
Cadena ↔ instancia de CarbonImmutable.
date
Cadena ↔ Carbon (sin hora).
timestamp
Cadena ↔ timestamp UNIX.
encrypted
Cifra al guardar, descifra al leer.
hashed
Aplica hash al guardar (se combina con casts de solo lectura).
AsStringable::class
Cadena ↔ objeto Stringable.
AsArrayObject::class
JSON ↔ instancia de ArrayObject.
AsCollection::class
JSON ↔ instancia de Collection.
AsArrayObject y AsCollection se implementan internamente en Laravel como casts personalizados para permitir modificar directamente offsets concretos del array.
// src/Illuminate/Contracts/Database/Eloquent/CastsAttributes.phpinterface CastsAttributes{ /** * Convierte el valor en bruto de la BD a un valor PHP (lectura) * * @param array<string, mixed> $attributes todos los atributos del modelo */ public function get(Model $model, string $key, mixed $value, array $attributes); /** * Convierte un valor PHP a una forma persistible en la BD (escritura) * * @param array<string, mixed> $attributes todos los atributos del modelo */ public function set(Model $model, string $key, mixed $value, array $attributes);}
Como el argumento $attributes contiene todos los atributos del modelo, es posible hacer conversiones que abarquen varias columnas (ver el patrón Value Object más abajo).
Se crea app/Casts/AsMoney.php. Como ejemplo, vamos a implementar un cast que convierte un importe (almacenado como entero) en un Value Object Money.
<?phpnamespace App\Casts;use App\ValueObjects\Money;use Illuminate\Contracts\Database\Eloquent\CastsAttributes;use Illuminate\Database\Eloquent\Model;class AsMoney implements CastsAttributes{ /** * Convierte el entero de la BD (en yenes) a un objeto Money */ public function get( Model $model, string $key, mixed $value, array $attributes, ): Money { return new Money((int) $value); } /** * Convierte el objeto Money en el entero que se guarda en la BD */ public function set( Model $model, string $key, mixed $value, array $attributes, ): int { if ($value instanceof Money) { return $value->amount; } return (int) $value; }}
Aplica el cast en el modelo.
<?phpnamespace App\Models;use App\Casts\AsMoney;use Illuminate\Database\Eloquent\Model;class Order extends Model{ protected function casts(): array { return [ 'price' => AsMoney::class, ]; }}
Con esto, $order->price devuelve una instancia de Money.
Combina las dos columnas address_line_one y address_line_two en un Value Object Address.
<?phpnamespace App\ValueObjects;use Illuminate\Contracts\Support\Arrayable;class Address implements Arrayable, \JsonSerializable{ public function __construct( public readonly string $lineOne, public readonly string $lineTwo, ) {} public function toArray(): array { return [ 'line_one' => $this->lineOne, 'line_two' => $this->lineTwo, ]; } public function jsonSerialize(): array { return $this->toArray(); }}
<?phpnamespace App\Casts;use App\ValueObjects\Address;use Illuminate\Contracts\Database\Eloquent\CastsAttributes;use Illuminate\Database\Eloquent\Model;use InvalidArgumentException;class AsAddress implements CastsAttributes{ /** * Construye el objeto Address a partir de varias columnas */ public function get( Model $model, string $key, mixed $value, array $attributes, ): Address { return new Address( $attributes['address_line_one'], $attributes['address_line_two'], ); } /** * Descompone el objeto Address en un array por columnas * * @return array<string, string> */ public function set( Model $model, string $key, mixed $value, array $attributes, ): array { if (! $value instanceof Address) { throw new InvalidArgumentException('The given value is not an Address instance.'); } return [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ]; }}
Si set devuelve un array, Eloquent interpreta las claves como nombres de columna y guarda cada valor en su columna correspondiente. En casts de una sola columna se devuelve una cadena o un entero.
Aplicación al modelo y uso:
<?phpnamespace App\Models;use App\Casts\AsAddress;use Illuminate\Database\Eloquent\Model;class User extends Model{ protected function casts(): array { return [ 'address' => AsAddress::class, ]; }}
$user = User::find(1);// Puedes obtenerlo como objeto Addressecho $user->address->lineOne;// Al modificar el Value Object, los cambios se reflejan automáticamente en la BD al guardar$user->address = new Address('123 Main St', 'Apt 4B');$user->save();
Eloquent cachea los atributos convertidos en Value Objects. Si accedes dos veces al mismo atributo, se devuelve la misma instancia.Si quieres desactivar la caché, añade la propiedad $withoutObjectCaching a la clase del cast.
class AsAddress implements CastsAttributes{ public bool $withoutObjectCaching = true; // ...}
Casts que solo aplican la conversión al escribir en la BD, sin transformar nada al leer. Se implementan mediante la interfaz CastsInboundAttributes.Un uso típico es el hashing. Solo se transforma al guardar contraseñas o valores secretos; al leer se devuelve el hash tal cual.
php artisan make:cast AsHash --inbound
<?phpnamespace App\Casts;use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;use Illuminate\Database\Eloquent\Model;class AsHash implements CastsInboundAttributes{ public function __construct( protected string|null $algorithm = null, ) {} /** * Aplica hash al guardar */ public function set( Model $model, string $key, mixed $value, array $attributes, ): string { return is_null($this->algorithm) ? bcrypt($value) : hash($this->algorithm, $value); }}
Para pasar parámetros a un cast personalizado, especifícalos tras el nombre de la clase separados por dos puntos. Varios parámetros se separan por comas.
Castables: colocar la lógica de cast en el propio Value Object
Un Value Object que implementa la interfaz Castable proporciona un método castUsing que devuelve su clase de cast. Con esto, el modelo no necesita conocer la clase de cast y la lógica de dominio queda mejor organizada.
<?phpnamespace App\ValueObjects;use App\Casts\AsAddress;use Illuminate\Contracts\Database\Eloquent\Castable;class Address implements Castable{ /** * Devuelve la clase que se usará para castear este objeto * * @param array<string, mixed> $arguments */ public static function castUsing(array $arguments): string { return AsAddress::class; }}
En el modelo, indica la clase del Value Object en lugar de la del cast.
protected function casts(): array{ return [ 'address' => Address::class, ];}
Combinando Castable con una clase anónima puedes reunir el Value Object y la lógica de cast en un solo archivo.
class Address implements Castable{ public static function castUsing(array $arguments): CastsAttributes { return new class implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): Address { return new Address( $attributes['address_line_one'], $attributes['address_line_two'], ); } public function set(Model $model, string $key, mixed $value, array $attributes): array { return [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ]; } }; }}
Los casts y $appends / $hidden son mecanismos independientes, pero al combinarlos hay que tener cuidado.
class User extends Model{ protected function casts(): array { return [ 'address' => AsAddress::class, ]; } // Excluye address en toArray() / toJson() protected $hidden = ['address_line_one', 'address_line_two']; // Incluye el address casteado en la serialización protected $appends = ['address'];}
En $hidden se especifican nombres de columna de la BD. No debes indicar el atributo generado por el cast (address), sino las columnas originales (address_line_one, address_line_two).