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.

Crear atributos computados en Eloquent
Photo by Sigmund / Unsplash

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.