La proprietà $casts di Laravel è una di quelle comodità silenziose che modellano il modo in cui gestisci i dati senza attirare troppa attenzione. Inserisci un tipo integrato come boolean o datetime nell'array, ed Eloquent converte automaticamente le stringhe grezze del database in qualcosa di più amichevole prima che il valore raggiunga i tuoi controller o le tue view. Mantiene il codice pulito. Ma nel momento in cui provi a chiamare direttamente la tua funzione helper all'interno di quell'array, il framework oppone resistenza. Scrivere qualcosa come 'username' => 'encrypt_data()' non funzionerà. Laravel si aspetta o una parola chiave di cast nativa o una classe che implementi l'interfaccia CastsAttributes. Questo requisito esiste perché lo strato di casting viene eseguito in profondità nel ciclo di idratazione e serializzazione di Eloquent; ha bisogno di un oggetto prevedibile con un contratto chiaro, piuttosto che di una stringa di funzione arbitraria che potrebbe non esistere a runtime.

Perché le funzioni helper falliscono nell'array $casts

Quando Eloquent carica un modello, scansiona l'array $casts per decidere come trasformare ogni colonna. Il framework riconosce tipi primitivi specifici—string, integer, array, encrypted, e così via—e riconosce i nomi di classi completamente qualificati che implementano CastsAttributes. Non valuta la stringa come codice. Pertanto, 'encrypt_data()' viene trattata come un tipo di cast sconosciuto, il che genera un errore. Anche se Laravel la analizzasse, non ci sarebbe un modo affidabile per passare l'istanza del modello, la chiave dell'attributo, il valore corrente e gli attributi circostanti alla tua funzione nell'ordine corretto. L'interfaccia esiste proprio per standardizzare questo scambio tra la tua logica personalizzata e le componenti interne di Eloquent.

Hai due modi solidi per colmare questa lacuna. Uno favorisce il riutilizzo a lungo termine. L'altro favorisce la velocità quando hai solo bisogno di una rapida soluzione all'interno di un singolo modello.

Metodo 1: Scrivere una classe di Cast personalizzata

Se la stessa trasformazione si applica a più campi o si estende su diversi modelli, una classe di cast dedicata è la scelta più pulita. Vive nel proprio file, può essere testata singolarmente tramite unit test e mantiene i tuoi modelli liberi da codice ripetitivo.

Inizia creando app/Casts/CustomEncrypt.php. Il namespace deve corrispondere alla tua configurazione di autoloading, tipicamente App\Casts. La classe deve implementare Illuminate\Contracts\Database\Eloquent\CastsAttributes, il che ti obbliga a definire due metodi: get e 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);
    }
}

Le firme sono importanti. Eloquent passa quattro argomenti a ciascun metodo. $model è l'istanza che viene popolata o salvata, il che ti permette di ispezionare altri campi se la tua logica dipende da essi. $key è il nome della colonna attualmente oggetto di cast. $value è la stringa grezza o null proveniente dal database durante un get, o il valore fornito dall'utente durante un set. $attributes è l'intero array grezzo delle colonne per quella riga. Non è necessario usarli tutti e quattro ogni volta, ma l'interfaccia li richiede.

Nel metodo get sopra, decrypt_data($value) viene eseguito dopo che Eloquent recupera la riga dal database e prima che il valore arrivi al tuo modello. Nel metodo set, encrypt_data($value) viene eseguito prima dell'INSERT o dell'UPDATE, garantendo che il database non veda mai il testo in chiaro.

Per collegarlo, fai riferimento alla classe all'interno dell'array $casts del tuo modello:

protected $casts = [
    'username' => CustomEncrypt::class,
    'password' => CustomEncrypt::class,
];

Poiché stai utilizzando la costante della classe, l'autoloader si occupa del resto. Se la tua applicazione in seguito dovesse cambiare lo schema di crittografia, ti basterà modificare un singolo file e ogni campo mappato cambierà comportamento istantaneamente. Questa centralizzazione è difficile da battere quando gestisci dati sensibili in molte tabelle.

Metodo 2: Utilizzare un Accessor e un Mutator

A volte non si desidera un nuovo file per una trasformazione che conta solo in un punto. La classe Attribute di Laravel ti permette di definire la logica di get e set direttamente sul modello utilizzando la sintassi closure di 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),
    );
}

In questo caso, il nome del metodo deve corrispondere alla colonna o all'attributo che stai mirando. Quando Eloquent legge username dal database, passa il valore grezzo attraverso la closure get. Quando assegni un nuovo valore alla proprietà username del modello, la closure set lo cripta prima che Eloquent costruisca la query.

Questo approccio brilla nei prototipi o nei modelli legacy dove una directory completa di classi di cast sembrerebbe eccessiva. L'aspetto negativo è la ripetizione. Se in seguito decidi che email, phone e backup_code necessitano dello stesso trattamento, finirai per copiare quelle closure in più metodi o modelli. Quel rumore si accumula. Inoltre, ti impedisce di riutilizzare la logica in un unit test senza avviare l'intero modello.

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.