Come creare cast personalizzati implementando l’interfaccia CastsAttributes. Presenta anche pattern avanzati come Value Object, cast inbound e Castables.
I cast di Eloquent sono il meccanismo che converte il valore grezzo letto dal database in un tipo PHP e, al salvataggio, esegue la conversione inversa. Si definiscono con il metodo casts.
Fa l’hash al salvataggio (da combinare con un cast in sola lettura)
AsStringable::class
Stringa ↔ oggetto Stringable
AsArrayObject::class
JSON ↔ istanza ArrayObject
AsCollection::class
JSON ↔ istanza Collection
AsArrayObject e AsCollection sono implementati internamente in Laravel come cast personalizzati per permettere la modifica diretta di specifici offset dell’array.
// src/Illuminate/Contracts/Database/Eloquent/CastsAttributes.phpinterface CastsAttributes{ /** * Converte il valore grezzo dal DB in un valore PHP (lettura) * * @param array<string, mixed> $attributes tutti gli attributi del modello */ public function get(Model $model, string $key, mixed $value, array $attributes); /** * Converte un valore PHP in una forma salvabile nel DB (scrittura) * * @param array<string, mixed> $attributes tutti gli attributi del modello */ public function set(Model $model, string $key, mixed $value, array $attributes);}
L’argomento $attributes contiene tutti gli attributi del modello, quindi puoi eseguire conversioni che coinvolgono più colonne (vedi il pattern Value Object più avanti).
Viene generato app/Casts/AsMoney.php. Come esempio implementiamo un cast che converte un importo (salvato come intero) in 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{ /** * Converte l'intero del DB (in centesimi) in un oggetto Money */ public function get( Model $model, string $key, mixed $value, array $attributes, ): Money { return new Money((int) $value); } /** * Converte un oggetto Money in un intero salvabile nel DB */ public function set( Model $model, string $key, mixed $value, array $attributes, ): int { if ($value instanceof Money) { return $value->amount; } return (int) $value; }}
Applica il cast al modello.
<?phpnamespace App\Models;use App\Casts\AsMoney;use Illuminate\Database\Eloquent\Model;class Order extends Model{ protected function casts(): array { return [ 'price' => AsMoney::class, ]; }}
Aggreghiamo address_line_one e address_line_two in 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{ /** * Costruisce l'oggetto Address da più colonne */ public function get( Model $model, string $key, mixed $value, array $attributes, ): Address { return new Address( $attributes['address_line_one'], $attributes['address_line_two'], ); } /** * Scompone l'oggetto Address in un array indicizzato per colonna * * @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, ]; }}
Se set restituisce un array, Eloquent usa le chiavi come nomi di colonna e salva ciascun valore nella colonna corrispondente. Nei cast su una sola colonna restituisci una stringa o un intero.
Applicazione al modello e utilizzo:
<?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);// puoi accedere come oggetto Addressecho $user->address->lineOne;// modificando il Value Object, il salvataggio si riflette sul DB automaticamente$user->address = new Address('123 Main St', 'Apt 4B');$user->save();
Gli attributi convertiti in Value Object vengono messi in cache da Eloquent. Accedendo due volte allo stesso attributo, ottieni la stessa istanza.Per disabilitare la cache aggiungi la proprietà $withoutObjectCaching alla classe cast.
class AsAddress implements CastsAttributes{ public bool $withoutObjectCaching = true; // ...}
Cast che convertono solo in scrittura verso il DB e non in lettura. Implementano l’interfaccia CastsInboundAttributes.L’uso tipico è l’hashing. Quando salvi password o segreti, converti; in lettura restituisci direttamente l’hash.
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, ) {} /** * Applica l'hash al salvataggio */ public function set( Model $model, string $key, mixed $value, array $attributes, ): string { return is_null($this->algorithm) ? bcrypt($value) : hash($this->algorithm, $value); }}
Castables: portare la logica di cast sul Value Object
Un Value Object che implementa l’interfaccia Castable ha il metodo castUsing che restituisce la classe di cast. Il modello non deve conoscere la classe cast, e la logica di dominio resta ordinata.
<?phpnamespace App\ValueObjects;use App\Casts\AsAddress;use Illuminate\Contracts\Database\Eloquent\Castable;class Address implements Castable{ /** * Restituisce la classe da usare per il cast di questo oggetto * * @param array<string, mixed> $arguments */ public static function castUsing(array $arguments): string { return AsAddress::class; }}
Nel modello indichi la classe Value Object al posto della classe cast.
protected function casts(): array{ return [ 'address' => Address::class, ];}
Combinando Castable con una classe anonima, puoi mantenere Value Object e logica di cast in un unico file.
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, ]; } }; }}
Cast, $appends e $hidden sono meccanismi indipendenti, ma quando li combini serve attenzione.
class User extends Model{ protected function casts(): array { return [ 'address' => AsAddress::class, ]; } // esclude address in toArray() / toJson() protected $hidden = ['address_line_one', 'address_line_two']; // include l'address castato nel risultato serializzato protected $appends = ['address'];}
In $hidden indica i nomi di colonna del DB. Non l’attributo prodotto dal cast (address), ma i nomi originali delle colonne (address_line_one, address_line_two).