Crear atributos computados en Eloquent
Los accessors de Eloquent son una herramienta muy potente para hacer tu código más DRY. Además, si los juntamos con el cacheo que nos provee Eloquent, podemos hacer algunas tareas repetitivas de forma mucho más rápida y eficiente.
Una de las cosas que más me gustan de Eloquent (a parte de ser increíble) son los accessors o los atributos computados.
¿Qué es un accessor en Eloquent?
Un accessor es una función que actuará como si fuera un attributo (ej. una columna de tu base de datos). Supongamos que tenemos la siguiente tabla:
products
- id BIGINT
- name VARCHAR(255)
- price_in_cents UNSIGNED_INT
Si quisiéramos, por ejemplo, formatear el precio para mostrarlo como € XX.YY, podríamos usar un accessor y estandarizar su formato tanto en back-end como en front-end.
¿Cómo se crea un accessor en Eloquent?
Nuestro modelo de Eloquent será similar a este:
class Product extends Model {
protected $fillable = ['name', 'price_in_cents'];
}
En nuestra aplicación, queremos mostrar el precio de cada producto con dos decimales, y precedido por "€". Para hacer esto, podríamos crear un servicio o una facade para formatear los precios en cada lugar que necesitemos, o podemos usar un atributo:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Product extends Model {
protected $fillable = ['name', 'price_in_cents'];
public function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->price_in_cents / 100, 2),
);
}
}
Ahora, en cualquier sitio que necesitemos, podremos acceder a formatted_price
como si fuera una columna más de nuestra base de datos:
<article>
<h2>{{ $product->name }}</h2>
<p>{{ $product->formatted_price }}</p>
</article>
Usando accessors de Eloquent en aplicaciones API de Laravel
Sin embargo, el comportamiento de los accessors no es tan obvio cuando trabajas con una API (o con otras herramientas como Livewire o Inertia, que trabajan internamente a través de APIs).
El problema viene cuando estos atributos no están incluidos en las respuestas JSON de tu servidor. Veamos por qué!
En algunos casos, computar atributos puede ser un proceso costoso, por lo que Laravel no los incluye al serializar el modelo a JSON. Tenemos que configurar el modelo para incluirlo usando el atributo $appends
:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Product extends Model {
protected $fillable = ['name', 'price_in_cents'];
protected $appends = ['formatted_price'];
public function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->price_in_cents / 100, 2),
);
}
}
Para hacerlo, tenemos que añadir al array de $appends
el nombre del atributo (preferiblemente en snake_case) y listo. Ahora nuestro atributo personalizado formatted_price
será incluido en todas las respuestas y podrá ser usado también en Livewire e Inertia!
Cacheando los atributos de Eloquent
Por defecto, los accessors no se cachean. Esto quiere decir que cada vez que llamemos a $product->formatted_price
se va a ejecutar el código de nuevo.
En muchos casos esto no traerá ningún problema, ya que no incluirán ninguna computación "gorda". Sin embargo, puede haber casos en los que los Attribute contengan un cálculo complejo o alguna consulta a la base de datos. En estos casos, ser llamado 100 veces dentro de un bucle for
puede hacer que nuestra aplicación vaya muy lenta.
Para prevenir esto podemos cachear la propiedad, de forma que solo se ejecutará la primera vez y el resto de llamadas recogerá la versión cacheada de la respuesta. Para ello, podemos usar el método shouldCache()
de los Attribute
de Eloquent. Solo tenemos que añadirlo a la respuesta del Attribute::make()
y el resultado será cacheado en este modelo durante la petición HTTP actual:
public function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->price_in_cents / 100, 2),
)->shouldCache();
}
Resumen
Los accessors de Eloquent son una herramienta muy potente para hacer tu código más DRY. Además, si los juntamos con el cacheo que nos provee Eloquent, podemos hacer algunas tareas repetitivas de forma mucho más rápida y eficiente.