Mutadors i Accessors

Com personalitzar atributs Eloquent: accessors, mutadors, attribute casting i casts personalitzats.

Què són els accessors i mutadors?#

Els accessors i mutadors permeten transformar els valors dels atributs d'Eloquent quan els llegeixes o els escrius. Un accessor modifica el valor quan accedeixes a l'atribut (lectura), i un mutador modifica el valor quan l'assignes (escriptura). Això permet que les dades es guardin a la base de dades en un format i es presentin a l'aplicació en un altre.

Definir accessors#

Per definir un accessor, crea un mètode protegit al model amb el nom de l'atribut que retorna una instància de Attribute. El paràmetre get del constructor rep el valor original i retorna el valor transformat:

use Illuminate\Database\Eloquent\Casts\Attribute;
 
class User extends Model
{
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
        );
    }
}

Ara, cada cop que accedeixes a $user->name, el valor es retorna amb la primera lletra en majúscula:

$user = User::find(1);
echo $user->name; // "joan" a la BD → "Joan" a l'aplicació

Pots definir accessors per a atributs que no existeixen a la base de dades. Això és útil per crear atributs virtuals calculats a partir d'altres camps:

class User extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) =>
                $attributes['first_name'] . ' ' . $attributes['last_name'],
        );
    }
}
 
echo $user->full_name; // "Joan Garcia"

El segon paràmetre del closure ($attributes) conté tots els atributs del model, cosa que permet calcular valors basant-se en múltiples camps.

Caching d'accessors#

Per defecte, Eloquent no guarda en cache el resultat dels accessors. Si l'accessor fa una operació costosa, pots activar el caching amb shouldCache():

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) =>
            $attributes['first_name'] . ' ' . $attributes['last_name'],
    )->shouldCache();
}

Definir mutadors#

Un mutador transforma el valor quan l'assignes a l'atribut. Es defineix amb el paràmetre set de Attribute:

class User extends Model
{
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => strtolower($value),
        );
    }
}

Ara, quan assignes un valor a name, es guarda en minúscules a la base de dades:

$user->name = 'JOAN GARCIA';
// Es guarda com "joan garcia" a la BD
echo $user->name; // Es mostra com "Joan garcia"

Un mutador pot modificar múltiples columnes a la vegada retornant un array:

class User extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) =>
                $attributes['first_name'] . ' ' . $attributes['last_name'],
            set: function (string $value) {
                $parts = explode(' ', $value, 2);
                return [
                    'first_name' => $parts[0],
                    'last_name' => $parts[1] ?? '',
                ];
            },
        );
    }
}
 
$user->full_name = 'Joan Garcia';
// Guarda first_name = 'Joan' i last_name = 'Garcia'

Un cas pràctic és el hashing de contrasenyes:

class User extends Model
{
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => bcrypt($value),
        );
    }
}
 
$user->password = 'secret'; // Es guarda hashejat automàticament

Attribute Casting#

El casting d'atributs converteix automàticament els tipus de dades quan llegeixes o escrius atributs. A diferència dels accessors i mutadors, els casts són declaratius: defins el tipus i Laravel s'encarrega de la conversió. Defineix els casts a la propietat $casts del model o al mètode casts():

class Article extends Model
{
    protected function casts(): array
    {
        return [
            'published' => 'boolean',
            'published_at' => 'datetime',
            'options' => 'array',
            'price' => 'decimal:2',
            'metadata' => 'collection',
            'created_at' => 'datetime:Y-m-d',
        ];
    }
}

Alternativament, pots utilitzar la propietat $casts:

protected $casts = [
    'published' => 'boolean',
    'published_at' => 'datetime',
    'options' => 'array',
];

Tipus de casts disponibles#

Laravel inclou casts per als tipus més comuns:

// Booleans: converten 0/1 a true/false
$article->published; // true (no "1")
 
// Dates: retornen instàncies de Carbon
$article->published_at; // Carbon instance
$article->published_at->format('d/m/Y'); // "15/03/2024"
$article->published_at->diffForHumans(); // "fa 2 dies"
 
// Arrays: converteix JSON de la BD a array PHP
$article->options; // ['color' => 'blue', 'size' => 'large']
$article->options = ['color' => 'red'];
$article->save(); // Es guarda com JSON a la BD
 
// Col·leccions: com array però retorna una Collection
$article->metadata->get('key');
$article->metadata->has('key');
 
// Decimals: formata amb el nombre de decimals especificat
$article->price; // 29.99 (no 29.990000000001)
 
// Integers i floats
'votes' => 'integer',
'rating' => 'float',
 
// Dates amb format personalitzat
'birthday' => 'date:Y-m-d',
'published_at' => 'datetime:Y-m-d H:i',
 
// Enums (PHP 8.1+)
'status' => ArticleStatus::class,
 
// Objectes immutables
'published_at' => 'immutable_datetime',
'options' => AsCollection::class,

Cast d'Enums#

A partir de PHP 8.1, pots fer cast d'atributs directament a enums:

enum ArticleStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}
 
class Article extends Model
{
    protected function casts(): array
    {
        return [
            'status' => ArticleStatus::class,
        ];
    }
}
 
$article->status; // ArticleStatus::Published
$article->status = ArticleStatus::Draft;
$article->save();

Cast d'arrays i JSON#

El cast array converteix una columna JSON de la base de dades a un array PHP, i viceversa:

class Product extends Model
{
    protected $casts = [
        'options' => 'array',
    ];
}
 
$product = Product::find(1);
$options = $product->options; // Array PHP
 
$options['color'] = 'red';
$product->options = $options;
$product->save(); // Es guarda com JSON
 
// Amb AsArrayObject pots modificar directament sense reassignar
protected $casts = [
    'options' => AsArrayObject::class,
];
 
$product->options['color'] = 'red'; // Funciona directament
$product->save();

Casts personalitzats#

Quan els casts integrats no cobreixen les teves necessitats, pots crear casts personalitzats. Un cast personalitzat és una classe que implementa la interfície CastsAttributes:

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
 
class Address implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): AddressValueObject
    {
        return new AddressValueObject(
            json_decode($value, true)
        );
    }
 
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return json_encode([
            'street' => $value->street,
            'city' => $value->city,
            'zip' => $value->zip,
        ]);
    }
}

Per utilitzar-lo al model:

class User extends Model
{
    protected function casts(): array
    {
        return [
            'address' => Address::class,
        ];
    }
}
 
$user->address; // AddressValueObject
$user->address->city; // "Andorra la Vella"

Casts amb paràmetres#

Pots crear casts que acceptin paràmetres implementant el constructor:

class CurrencyFormat implements CastsAttributes
{
    public function __construct(
        protected string $currency = 'EUR',
        protected int $decimals = 2,
    ) {}
 
    public function get(Model $model, string $key, mixed $value, array $attributes): string
    {
        return number_format($value / 100, $this->decimals) . ' ' . $this->currency;
    }
 
    public function set(Model $model, string $key, mixed $value, array $attributes): int
    {
        return (int) ($value * 100);
    }
}
protected function casts(): array
{
    return [
        'price' => CurrencyFormat::class . ':EUR,2',
    ];
}

Casts d'entrada o sortida#

Si el cast només necessita transformar el valor en una direcció, pots implementar CastsInboundAttributes (només per escriptura):

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
 
class Hash implements CastsInboundAttributes
{
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return bcrypt($value);
    }
}

Això és útil per a casos com el hashing de contrasenyes, on transformes el valor en guardar-lo però no necessites cap transformació en llegir-lo.