Aprende eloquent con ejemplos!!! Lección 6 - Accesors, Mutators y Casts
Johan Tovar
Posted on June 7, 2021
¡Bienvenido a la lección seis de Eloquent con ejemplos!
Muchas veces dentro de la construcción de nuestras aplicaciones, nos encontraremos con que queremos (o debemos) mostrar cierto tipo de información en un formato determinado, y aunque muchas veces esto lo solucionaremos con el uso de un poco de CSS, código javascript o incluso “helpers” de Laravel o del propio PHP, no nos serán ajenas aquellas situaciones en donde dichos formatos requieran de cierta lógica más compleja, lo que quizás nos obligue a llenar nuestras vistas de código para cumplir con esta tarea.
En este sentido, si nos decantamos por depositar esta lógica dentro de las vistas, estaremos incurriendo en lo que se considera una mala práctica, rompiendo con una cantidad importantes de principios y estándares propio del paradigma MVC y de la programación en general, eso sin mencionar lo confusas y sucias que se verán, llevándonos a un escenario lleno de inconvenientes de cara a la escalabilidad, reusabilidad, mantenimiento y posiblemente hasta de seguridad, sin embargo, en la lección de hoy abordaremos un poco sobre algunas herramientas y trucos que nos ofrece eloquent para encargarnos de este tipo de situaciones, no solo eliminando los inconvenientes, sino que además nos aporta una gran variedad de ventajas adicionales.
Cosas que aprenderemos:
• Accesors
• Mutators
• Casts
Accesors:
Como hemos visto en las entregas pasadas, para acceder a cualquier campo de nuestra base de datos lo hacemos de una manera muy sencilla, solo haciendo un llamado a la propiedad correspondiente dentro de nuestro modelo, sin embargo, muchas veces, aunque este dato esta correctamente guardado, necesitamos mostrar un formato distinto al que ya tiene, es aquí donde los accesors entran en acción y nos salvan el día. Mediante el uso de accesors podremos manipular un registro de datos mientras sigue formando parte del modelo, cambiándolo de la forma que deseemos, vamos a verlo mediante un ejemplo:
Todos los nombres de nuestros perros se encuentran registrados en letra capital, así que cambiemos eso y hagamos que siempre se muestren en mayúsculas, para ello, coloquemos el siguiente código dentro de nuestro modelo Dog
:
function getNameAttribute($value){
return strtoupper($value);
}
A simple vista, parece que solo hemos agregado un método como cualquier otro, pero en cierta forma esto tiene su magia. En primer lugar, tenemos la palabra ‘get’ que indica que queremos obtener el valor, seguido del nombre del atributo (en nuestro caso ‘name’), el cual puede variar según nuestras necesidades, finalizando entonces con la palabra ‘attribute’, todo perfectamente construido bajo una notación ‘camelCase’. Lo anterior es una convención ya establecida y debemos seguirla siempre para que todo funcione.
Ahora bien, pasemos a tinker y consultemos un registro cualquiera de nuestra tabla ‘dogs’, en el siguiente ejemplo hemos elegido a nuestro amigo ”Jock”, obteniendo como respuesta “JOCK”:
>>> Dog::find(2)->name
...
=> "JOCK"
>>>
¡Listo! así de fácil hemos modificado nuestro registro, sin usar más que un simple método y siguiendo la convención establecida para ello. No importa en que forma este guardado el nombre de nuestros amigos caninos en la base de datos, a partir de ahora, siempre se mostraran en mayúsculas donde sea que lo necesitemos, pero, algo muy importante a tener en cuenta es la palabra siempre, porque a pesar que nuestro nombre seguirá guardado en su formato original, la respuesta que obtendremos sera la modificada y esto es muy importante a la hora de necesitar trabajar con el formato original, así que debemos estar muy pendientes con eso.
Ahora bien, si bien es fácil crear y manejar un accesors, no podemos obviar que el ejemplo anterior es sumamente sencillo, con simple CSS podríamos obtener el mismo resultado, sin embargo, es evidente el mundo de posibilidades que se nos abren al usar accesors, siendo métodos en los que podemos depositar cualquier lógica y retornar un formato totalmente elaborado, incluso podríamos agregar atributos a nuestros modelos construidos a partir de la información guardada en la base de datos.
//En el modelo Dog agreguemos
function getIdWithNameAttribute($value)
{
return "{$this->id} : {$this->name}";
}
//En tinker
>>> Dog::find(2)->idWithName
...
=> "2 : JOCK"
>>>
Así de fácil hemos agregado un atributo al modelo Dog
sin que éste exista en la base de datos, algo muy útil cuando queremos crear atributos dinámicos, ayudándonos a ahorrar espacio en la base de datos y ganando reusabilidad.
Ahora que tenemos claro cómo usar nuestros accesors y las ventajas que nos brinda, trabajemos en un ejemplo diferente. Digamos que el cliente ha decido abrir una pequeña tienda y nuestro jefe nos ha encomendado el desarrollo de dicha característica. Para fines prácticos de esta lección, solo abordaremos los productos, dejando quizás las demás partes de esta tarea para futuras entregas.
Pongamos manos a la obra entonces.
Nuestra tienda, entre otras cosas debe contar con una tabla productos, la cual manejará por ahora solo unos pocos campos: id, name, slug, price y options. Ahora bien, definida nuestra entidad productos, debemos pasar a la creación de los archivos respectivos para el correcto funcionamiento, a estas alturas ya deberíamos saber cuáles son estos archivos y como crearlos: modelo, factory y seeder, ¿Lo tienes? ... estoy seguro que sí, pero si no es el caso, la invitación está abierta para revisar las entregas anteriores donde esta explicado detalladamente el paso a paso que debemos seguir.
Pues bien, con el objeto de estandarizar un poco la construcción de nuestros datos, haremos solamente el seeder y el migrations correspondiente a los productos, quedando el resto como ejercicio y práctica de cada quien. Nuestros archivos deberían verse algo como esto:
Migration
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug');
$table->decimal('price');
$table->json('options');
$table->boolean('active');
$table->timestamps();
});
}
Modelo
//Product.php
//Agregar propiedades fillables
protected $fillable = [
'name', 'price', 'active', 'options'
];
Factory
//ProductFactory
public function run()
{
Product::factory()->create([
'name' => 'Collares',
'price' => rand(1000,9000),
'slug' => function ($attr) {
return Str::slug($attr['name']);
},
'options' => json_encode([
'color' => $this->faker->randomElement([
'rojo', 'azul', 'verde'
])
])
]);
}
Seeder
//ProductSeeder.php
public function run()
{
Product::factory()->create([
'name' => 'Collares',
'price' => 1400.5,
'slug' => function ($attr) {
return Str::slug($attr['name']);
},
'options' => json_encode(['color' => 'azul'])
]);
}
Solo estamos sembrando un solo registro en nuestra base de datos, podríamos tener tantos como quisiéramos, pero para los fines del ejemplo es más que suficiente. Vayamos a tinker y consultemos nuestro producto:
>>> Product::find(1)
=> App\Models\Product {#4123
id: 1,
name: "Collares",
slug: "collares",
price: "1400.50",
options: "{"color": "azul"}",
active: 1,
created_at: "2021-06-06 16:58:45",
updated_at: "2021-06-06 16:58:45",
}"
A simple vista todos los campos están correctos, sin embargo, para poder mostrar correctamente la información como los usuarios la esperan, debemos hacer unas pequeñas modificaciones, empecemos entonces por el campo precios. En todo sistema informático encontraremos que el formato de separación de decimales es con notación de puntos, esto está bien y es perfecto para nuestra aplicación al momento de realizar sus cálculos, pero para nuestros clientes, quienes están acostumbrados a ver una coma (,) cumpliendo con esta función y al punto (.) como separador de millares, quizás pueda ser objeto de confusión, vamos a darle entonces un formato más familiar:
En nuestro modelo Product
coloquemos el siguiente accesor
public function getFormattedPriceAttribute()
{
return number_format($this->convertedPrice, 2, ',', '.');
}
Si consultamos en tinker, veremos ahora el nuevo formato.
>>> Product::find(1)->price
=> "1400.50"
>>> Product::find(1)->formattedPrice
=> "1.400,50"
>>>
Ahora le hemos dado un formato mucho más legible a nuestros precios, sin modificar el valor original del mismo, sin embargo sigue siendo un ejemplo muy fácil aunque demostrativo del poder de eloquent, pero, si imaginamos por un momento un ecommerce internacional con manejo de precios en varias divisas, debiendo mostrarlos según las preferencias del usuario o por la región en donde se encuentra, esto nos llevaría a la implementación de una lógica que se encargue de hacer la conversión necesaria antes de mostrar los precios, sin duda un escenario en el que los accesors pueden ayudarnos:
public function getFormattedPriceAttribute()
{
return number_format($this->convertedPrice, 2, ',', '.');
}
public function getConvertedPriceAttribute()
{
return $this->convertPrice($this->price);
}
private function getConvertPrice ($price)
{
//lógica para determinar preferencias
$rate =3;
//conversión de precio
return $price * $rate;
}
Hemos modificado nuestro primer accesor y ahora no trabaja con el campo price
directamente, sino que se vale del resultado de un nuevo accesor, para así retornar nuestro precio convertido y formateado al mismo tiempo. Tomemos en cuenta que es una implementación simple y con fines ilustrativos, son obvias las mejoras necesarias, pero queda de parte de cada uno de nosotros completar dicha tarea.
Mutators:
Los mutators son la otra cara de los accesors. Como ya vimos, con estos últimos podemos tomar los valores de la base de datos y mostrarlos de una manera distinta a su forma original, ahora con los mutators, es exactamente lo contrario, podremos capturar el valor antes de que entre en la base de datos y modificarlos antes de que sean persistidos, pudiendo hacer cosas como cifrar contraseñas, limpiar los datos, entre muchas otras tareas. Veamos un ejemplo:
function setNameAttribute($value){
return $this->attributes['name'] = strtoupper($value);
}
Podemos ver la similitud en la construcción entre ambas herramientas, a penas y se nota la diferencia entre la antes usada palabra “get” (accesors) y “set”, propia de los mutators, es debido esta pequeña diferencia en la construcción del nombre de ambos métodos que la gente a veces se confunde, pero con práctica no se debería tener ningún problema.
Al igual que los accesors, podemos crear propiedades nuevas que no estén relacionadas con campos existente en nuestra base de datos, sin embargo, aunque no generará ningún error al momento de guardar nuestro modelo, los valores de estas propiedades no serán persistido.
Las ventajas de usar los mutators recae sobre varios tópicos, entre los cuales podemos mencionar, la facilidad con la que podemos estandarizar la información que vamos a almacenar dentro de nuestra base de datos, por ejemplo, si te fijas en el código de arriba, esta función lo que hace es guardar el nombre en mayúsculas, sin importar el formato en el que este pueda venir, algo conveniente de cara a mantener un estándar en el formato de nuestros datos.
Por otro lado, con los mutators también podemos detectar el cambio en alguna propiedad de nuestros modelos, lo que nos ayudaría en el caso de tener que modificar tanto el formato que entra a nuestra propiedad, como cualquier otra que debiera cambiar como un efecto necesario para mantener la coherencia en la información, ejemplo de ello pudiera ser los llamados ‘slugs’. Antes de pasar al ejemplo, debemos tener en cuenta que la siguiente es una forma entre muchas que existen, siendo alguna incluso más conveniente para hacer este cambio, pero para fines prácticos de la lección nos viene perfecto:
public function setNameAttribute($value){
$this->slug = Str::slug($value);
return $this->attributes['name'] = strtoupper($value);
}
Como es costumbre vayamos a la consola y hagamos la consulta respectiva de la siguiente manera:
>>> $product = Product::find(1)
=> App\Models\Product {#4056
id: 1,
name: "Collares",
slug: "collares",
…
}
>>> $product->name = "Collares PARA perrOs"
=> "Collares PARA perrOs"
>>> $product->name //Nombre mutado
=> "COLLARES PARA PERROS"
>>> $product->slug //Slug generado por el mutator
=> "collares-para-perros"
Observemos como en primer lugar, el name de nuestro producto y su slug se imprimen según el valor almacenado en la base de datos, al asignarle un nuevo nombre, nuestro mutator ha modificado el formato extraño que le hemos dado, convirtiendo toda la cadena a mayúsculas modificando al mismo tiempo el slug, manteniendo así la coherencia entre ambos datos.
Casts:
Como sabemos, los modelos de Laravel poseen una variedad de propiedades que los dotan de múltiples funciones y utilidades, siendo casts
una de ella y, como bien se señala en su documentación, esta propiedad “proporciona un método práctico de convertir atributos en tipos de datos comunes”, haciendo este trabajo de manera automática al obtener y/o capturar el valor del atributo declarado, veamos cómo funciona:
>>> $product = Product::find(1)
…
>>> $product->active
=> 1
Observemos como el campo active, aun cuando en la migración esta declarado como boolean
, nuestro modelo lo representa con valor int
de “1”. Cambiemos esto y hagamos que active se vea como un verdadero boolean:
// En nuestro modelo
protected $casts = [
'active' => 'boolean'
];
//En tinker
>>> $product = Product::find(1)
…
>>> $product->active
=> true
Ahora nuestro campo active dejó de verse como un tipo int
y ahora luce como todo un boolean
, encargándose el modelo de esta tarea de manera automática. Otra propiedad con una misión similar es dates
, pero como podemos notar se enfoca solo en datos tipos fecha, sin embargo, su uso podríamos decir que es obligatorio, no porque Laravel y sus modelos así nos lo imponga, sino por los exagerados beneficios que esta nos brinda.
Cuando trabajamos con fechas, es siempre un constante dolor de cabeza el solo pensar todo lo que a ellas se refiere, desde distintos formatos de entrada y salida, necesidad de trabajar con partes específicas de ellas (como el día, mes u hora), operaciones entre ellas, validaciones, hasta un sin fin de otros inconvenientes que su uso conlleva, por suerte, existe un extraordinario paquete llamado Carbon, el cual ya tiene todo pensado y nos facilita este enorme trabajo. La razón de que hablemos de este grandioso paquete es porque, cuando declaramos un atributo dentro de la propiedad dates
, nuestro modelo se encargará de tomar nuestra fecha de la base de datos y convertirla en un objeto Carbon
, con esto, podremos hacer uso de todas sus ventajas haciendo nuestra vida más fácil.
Aunque en nuestro ejemplo de hoy no hemos agregado ningún atributo tipo date, basta con que sepamos que al igual que los casts, basta con declarar nuestro campo tipo fecha dentro del array dates
, sin embargo, para ilustrar un poco el poder de trabajar con este tipo de objeto, podemos elegir cualquiera de los campos created_at
y updated_at
, (también deleted_at
si estamos trabajando con softdeletes) y verlos en acción. Cuando llamamos a cualquiera de estos campos, podremos ver que los mismos son una instancia del objeto Carbon
, por lo que podremos hacer cosas como estas:
>>> $product->created_at->diffForHumans(now())
=> "25 minutes before"
>>> $product->created_at->toDateString()
=> "2021-06-06"
>>> $product->created_at->addYears(5)->toDayDateTimeString()
=> "Sat, Jun 6, 2026 12:08 AM"
>>> $product->created_at->toDateTimeString()
=> "2021-06-06 00:08:39"
>>> $product->created_at->toDayDateTimeString()
=> "Sun, Jun 6, 2021 12:08 AM"
>>> $product->created_at->monthName
=> "June"
Como puedes ver, con solo tener un objeto carbon, podemos hacer un sin fin de cosas, aquí solo mostramos unas pocas utilidades, pero la invitación está abierta para revisar todo lo que este maravilloso paquete nos ofrece. Podemos visitar el siguiente enlace para CARBON y conocer un poco más sobre él.
Por último, nos queda conocer los llamados custom casts. Al igual que casts
, con ellos podemos modificar el tipo de dato de nuestros campos, pero en este caso, este tipo de dato no será un tipo básico, en su lugar, será un tipo de dato construido por nosotros mediante una clase destinada a ello.
En nuestro ejemplo definimos un campo tipo json
llamado options, su misión es contener información adicional del producto, la cual por conveniencia debemos guardarla en este formato, sin embargo, al momento de recuperar a información, necesitamos que esta información se comporte de una manera que ningún tipo de dato básico puede hacerlo, queremos poder moldear este comportamiento y dotarlo de ciertas mejoras que faciliten nuestro trabajo d desarrollo, para ello hagamos lo siguiente:
Paso1
Primero vamos a crear una clase que se encargue de darle cierto comportamiento a nuestro atributo. Esta clase sera un Value Object, en ella podremos darle el comportamiento que deseamos a nuestro atributo, pero no tenemos ningún comando para crearla, así que debemos proceder manualmente:
Creamos un archivo llamado OptionVO.php dentro del directorio App/ValueObjects,y coloquemos lo siguiente:
class OptionsVO
{
protected const COLOR_HEX = [
'rojo' => '#FF0000',
'verde' => '#00FF00',
'azul' => '#0000FF',
];
public string $color;
public function __construct($options)
{
$this->color = data_get($options, 'color');
}
public function colorHex()
{
return self::COLOR_HEX[$this->color];
}
public function toArray()
{
return [
'color' => $this->color
];
}
public function toJson()
{
return json_encode($this->toArray());
}
}
Básicamente, solo estamos creando un objeto con una propiedad que retorna el valor almacenado en la base de datos, un método el cual nos devuelve el equivalente hexadecimal según el color, y un par de métodos más que devuelven a nuestro objeto en un formato específico (json y array), estos últimos nos ayudarán cuando hagamos la implementación de la clase casteable
. El comportamiento dado es bastante simple, además solo estamos pasando una sola propiedad, pero podemos sentirnos libres de ser creativos y agregar lo que queramos y/o necesitemos.
Los Value Object son útiles en el modelado de aplicaciones, mediante su implementación, podemos dar forma a conceptos de menor peso dentro de ellas, como pueden ser direcciones, fechas, unidades de medidas o conversión, entre otros.
Paso 2
Ahora tenemos que crear la clase que se encargará de la magia de la transformación, valiéndonos esta vez del comando creado para esta tarea, vamos a la consola y ejecutemos lo siguiente:
php artisan make:cast ProductOptionsCast
Esto creará un archivo en el directorio App/Casts, con el nombre que le demos a nuestra clase, en el depositaremos el siguiente código:
public function get($model, $key, $value, $attributes)
{
return new OptionsVO(json_decode($value));
}
public function set($model, $key, $value, $attributes)
{
if (!$value instanceof OptionsVO) {
throw new InvalidArgumentException('The given value is not an OptionsVO instance.');
}
return [
'options' => $value->toJson()
];
}
Esta clase está compuesta por dos métodos: El primero de ellos es get
, este se ejecuta al momento de recuperar la información almacenada en la base de datos, la cual recordemos se encuentra en formato json, así que lo decodificamos y construimos un objeto OptionsVO
a partir del resultado. El segundo es set
y se activa cuando se le asigna un valor a la propiedad, que en nuestro caso es options, este debe recibir una instancia del objeto OptionsVO
para que funcione, de lo contrario salta una exception.
Paso 3
Por último, debemos indicarle al modelo Product
que debe modificar el tipo de dato de su propiedad options, para lo cual procedemos de la misma manera que ya vimos más arriba con respecto a $cast
:
//Modelo Product.php
protected $casts = [
…
'options' => ProductOptionsCast::class,
];
Y con esto es suficiente, ahora podemos hacer uso de nuestro nuevo tipo options:
>>> $product = Product::find(1)
=> App\Models\Product {#3338
...
options: "{"color": "azul"}",
...
}
>>> $product->options
=> App\Valuebjects\OptionsVO {#4267
+color: "azul",
}
>>> $product->options->color
=> "azul"
>>> $product->options->colorHex()
=> "#0000FF"
¡Lo hemos hecho!, nuestro casts trabaja perfectamente y sin mayores complicaciones, si necesitamos agregar más comportamiento, todo se encuentra encapsulado en una sola clase, lo que facilitará muchos nuestro trabajo al momento de la integración de nuevas options.
Nota
Antes de finalizar por hoy, debemos saber que quedan algunas modificaciones que debemos hacer en cuanto a la factory y el seeder de los productos para que no se rompa la aplicación al momento de crear o sembrar nuevos registros. Queda como ejercicio el realizar dichas modificaciones, pero siempre podemos ver la solución en el repositorio de la serie.
Quédate atento a la próxima entrega, si tienes alguna duda puedes contactarme en mi cuenta de twitter @johantovar o déjala en los comentarios. Hasta entonces y que tengas un feliz y exitoso día.
Repositorio de práctica: jtovarto/serie-eloquent-con-ejemplo
Posted on June 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.