La propiedad $casts de Laravel es una de esas comodidades silenciosas que definen cómo manejas los datos sin llamar demasiado la atención. Agrega un tipo integrado como boolean o datetime al array, y Eloquent convierte automáticamente las cadenas de texto sin procesar de la base de datos en algo más amigable antes de que el valor llegue a tus controladores o vistas. Mantiene tu código ordenado. Pero en el momento en que intentas llamar a tu propio helper directamente dentro de ese array, el framework se resiste. Escribir algo como 'username' => 'encrypt_data()' no funcionará. Laravel espera una palabra clave de cast nativa o una clase que implemente la interfaz CastsAttributes. Ese requisito existe porque la capa de casting se ejecuta profundamente dentro del ciclo de hidratación y serialización de Eloquent; necesita un objeto predecible con un contrato claro en lugar de una cadena de función arbitraria que podría no existir en tiempo de ejecución.
Por qué las funciones helper fallan en el array $casts
Cuando Eloquent carga un modelo, escanea el array $casts para decidir cómo transformar cada columna. El framework reconoce primitivos específicos —string, integer, array, encrypted, etcétera— y reconoce nombres de clases totalmente calificados que implementan CastsAttributes. No evalúa la cadena como código. Por lo tanto, 'encrypt_data()' se trata como un tipo de cast desconocido, lo que provoca un error. Incluso si Laravel lo analizara, no habría una forma confiable de pasar la instancia del modelo, la clave del atributo, el valor actual y los atributos circundantes a tu función en el orden correcto. La interfaz existe precisamente para estandarizar ese intercambio entre tu lógica personalizada y el funcionamiento interno de Eloquent.
Tienes dos formas sólidas de cerrar esta brecha. Una favorece la reutilización a largo plazo. La otra favorece la velocidad cuando solo necesitas un parche rápido dentro de un único modelo.
Método 1: Escribir una clase de cast personalizada
Si la misma transformación se aplica a múltiples campos o se extiende a varios modelos, una clase de cast dedicada es la opción más limpia. Vive en su propio archivo, puede probarse mediante pruebas unitarias de forma aislada y mantiene tus modelos libres de código repetitivo (boilerplate).
Comienza creando app/Casts/CustomEncrypt.php. El namespace debe coincidir con tu configuración de autoloading, típicamente App\Casts. La clase debe implementar Illuminate\Contracts\Database\Eloquent\CastsAttributes, lo que te obliga a definir dos métodos: get y set.
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CustomEncrypt implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return decrypt_data($value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return encrypt_data($value);
}
}
Las firmas son importantes. Eloquent pasa cuatro argumentos a cada método. $model es la instancia que se está poblando o guardando, lo que te permite inspeccionar otros campos si tu lógica depende de ellos. $key es el nombre de la columna que se está transformando actualmente. $value es la cadena sin procesar o null que proviene de la base de datos en un get, o el valor proporcionado por el usuario en un set. $attributes es el array completo de columnas sin procesar para esa fila. No necesitas usar los cuatro cada vez, pero la interfaz los requiere.
En el método get anterior, decrypt_data($value) se ejecuta después de que Eloquent obtiene la fila de la base de datos y antes de que el valor llegue a tu modelo. En el método set, encrypt_data($value) se ejecuta antes del INSERT o UPDATE, asegurando que la base de datos nunca vea texto plano.
Para conectarlo, haz referencia a la clase dentro del array $casts de tu modelo:
protected $casts = [
'username' => CustomEncrypt::class,
'password' => CustomEncrypt::class,
];
Debido a que estás utilizando la constante de la clase, el autoloader se encarga del resto. Si tu aplicación necesita cambiar el esquema de cifrado más adelante, editas un solo archivo y cada campo mapeado cambia su comportamiento instantáneamente. Esa centralización es difícil de superar cuando gestionas datos sensibles en muchas tablas.
Método 2: Utilizar un Accessor y un Mutator
A veces no quieres un archivo nuevo para una transformación que solo importa en un lugar. La clase Attribute de Laravel te permite definir la lógica de get y set directamente en el modelo utilizando la sintaxis de closures de PHP 8+.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function username(): Attribute
{
return Attribute::make(
get: fn ($value) => decrypt_data($value),
set: fn ($value) => encrypt_data($value),
);
}
Aquí, el nombre del método debe coincidir con la columna o el atributo al que te diriges. Cuando Eloquent lee username de la base de datos, pasa el valor sin procesar a través del closure get. Cuando asignas un nuevo valor a la propiedad username del modelo, el closure set lo cifra antes de que Eloquent construya la consulta.
Este enfoque destaca en prototipos o modelos heredados donde un directorio completo de clases de cast parece excesivo. La desventaja es la repetición. Si más adelante decides que email, phone y backup_code necesitan el mismo tratamiento, terminarás copiando esos closures en múltiples métodos o modelos. Ese ruido se acumula. También te impide reutilizar la lógica en una prueba unitaria sin arrancar todo el modelo.
Choosing Between the Two
Use a custom cast class when you care about reuse, testing, and keeping models slim. It signals to other developers that this transformation is a first-class concept in your application, not a one-off hack.
Use an accessor when the logic is truly localized, experimental, or unlikely to outlive the current sprint. It lets you move fast without scattering small files across the codebase. Just be ready to refactor into a cast class once the same pattern appears a second or third time.
Practical Details to Keep in Mind
Custom casts are powerful, but they introduce behavior that can surprise you if you are not looking. First, remember that get receives whatever the database returned, including null. If decrypt_data does not tolerate null input, guard against it:
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return is_null($value) ? null : decrypt_data($value);
}
Second, casts run during array and JSON serialization. When you return a model as an API resource or call toArray(), the cast get logic still applies. That is usually what you want, but it is worth remembering if you are exposing decrypted values and need to layer additional visibility controls on top.
Third, and most importantly, casting happens after retrieval. You cannot query against the transformed value. A query like User::where('username', 'john_doe')->first() sends the string john_doe straight to the database. It never passes through your decrypt logic. If the column is encrypted at rest, that query will find nothing unless you search against the encrypted ciphertext itself. Plan your database access patterns accordingly, because casts are not a substitute for native database functions or indexed plaintext columns.
Custom cast classes also make great homes for small bits of configuration. If you need constructor arguments—perhaps passing a cipher mode or a format string—Laravel supports them through the $casts array using an expression like 'field' => CustomEncrypt::class . ':arg', though that is a step beyond the basic setup described here.
The Real Takeaway
The restriction against dropping helper functions directly into $casts is not arbitrary bureaucracy. It nudges you toward code that is explicit, testable, and reusable. Custom cast classes turn scattered inline logic into dependable components you can share across models without duplication. Accessors keep the door open for quick, localized fixes when a separate file feels like ceremony. Master both, choose based on the scope of the problem, and your Eloquent layer will stay readable long after the application grows.
