Aprende eloquent con ejemplos!!! Lección 5 - Global scopes, Soft-deletes y Traits
Tags: laravel, eloquent, webdev, php
Published: 2021-05-17 00:49:33


¡Bienvenido a la lección cinco de Eloquent con ejemplos!

Como has podido ver a lo largo de las entregas pasadas, nos hemos enfocado en las herramientas que tenemos a disposición con el marco de laravel, esto claro con el objetivo de agilizar nuestro flujo de trabajo y así podernos enfocar en la resolución de problemas reales de nuestra aplicación. Es primordial tener siempre presente que el aprendizaje es un proceso largo y continuo, si quieres sacar el mayor provecho de todo lo que **Eloquent** y el marco de Laravel nos ofrece, pues no dejes de chequear la documentación oficial, seguir al tanto de las novedades que semanalmente el equipo de laravel nos revela y por supuesto de repasar y compartir el material que en estas entregas pongo a tu disposición.

Hasta ahora hemos modificado algo de nuestro código y nos hemos separado un poco de los ejemplos de la fuente original de este curso, sin embargo, la idea y esencia es la misma, pues se trata de ejemplificar con prácticas más realistas que las que nos encontramos comúnmente en diversos tutoriales. En la pasada entrega aprendimos sobre los **model scopes** (ámbitos locales), no sólo como una forma de agregar cláusulas `where` y hacer consultas, sino como una idea de organizar nuestro código. Hoy vamos a ampliar en eso mostrándote que son los **global scopes** (alcances globales) y la introducción los **traits** en Laravel.

Cosas que aprenderemos:
- Global scopes
- Soft-deletes
- Traits


## Model Scopes:
Los **model scopes** (alcances globales) no son "globales" en el sentido de una variable global, sino que se aplican a todos los registros de un modelo concreto como adición a cualquier otra restricción. Por lo general, estos están reservados para más restricciones a nivel de sistema y pueden funcionar junto con un **trait** como **soft-deletes** que analizaremos más adelante en esta lección o requisitos de multiinquilino (multi-tenant).

Hay dos maneras de usar **global scope**; creando una clase independiente o simplemente usando el propio modelo. Una clase independiente hace que el ámbito sea reutilizable, por supuesto, ya que, podría extenderla a cualquier modelo que la requiera, en el segundo solo estaría dentro del modelo donde declaremos el **global scope**. Veamos este último caso aplicando un **global scope** en nuestro modelo Dog para el manejo de edades de nuestros caninos.

En primer lugar, vayamos a nuestro modelo Dog.php e importemos la clase Builder que necesitaremos para que todo esto funcione:

```php
use Illuminate\Database\Eloquent\Builder;
```

La mayoría de las clases Laravel tienen una función `boot` que se llama como parte de la creación de instancias de la clase. Aquí es donde normalmente registramos cosas como observables y eventos, así como agregar nuestro **global scope** en este ejemplo. Recuerda que debes asegurarte de llamar al método `parent`
```php
protected static function boot ()
{
    //llamado al método boot padre
    parent::boot();

   //nuestro global scope
    static::addGlobalScope('age', function (Builder $builder) {
        $builder->where('age', '>', 8);
    });
}
```
> El método ` addGlobalScope()` admite dos parámetros, en el primero de ellos pasamos una cadena que corresponde al nombre de nuestro **global scope**, con la que nos referiremos mas adelante cuando necesitemos invocarlo (en este caso ‘age’), el segundo es un **closure** en el que especificamos las condiciones con las que filtraremos nuestra respuesta.

Con el método anterior, lo que hemos hechos es restringir nuestra consulta `all()` aplicando un filtro para que solo nos devuelva aquellos caninos cuya edad sea mayor a 8, de esta manera, si haz seguido el curso hasta acá, verás como al hacer la consulta obtendremos solo un resultado, la bella Jane:

```php
// ejecutamos tinker y aplicamos la consulta

>>> App\Models\Dog::all();

=> Illuminate\Database\Eloquent\Collection {#4057
     all: [
       App\Models\Dog {#4058
         id: 4,
         name: "Jane",
         gender: "female",
         age: 9,
         created_at: "2021-05-02 23:13:42",
         updated_at: "2021-05-02 23:13:42",
       },
     ],
   }
```

Como podrás ver, no hemos usado ningún método adicional como si lo hicimos en la entrega pasada con los **local scopes**, acá solo le indicamos a la aplicación que nos devuelva TODOS los registros de nuestros modelos, siendo ese “todos” aquellos registros que cumplen con la condición definida.

En tal sentido, es importante dejar claro, que a partir de la declaración de este **global scope**, nuestra aplicación siempre ignorará cualquier otro registro que no cumpla con el criterio dado, por lo que cada vez que invoques al modelo Dog, este solo tomará en cuenta aquellos que si lo cumplen, veámoslo con un ejemplo:

```php

//Solicitamos el perro con ID 2 cuya edad es 7
>>> App\Models\Dog::find(2);

//Obtenemos null
=> null
```

Como podrás observar, al hacer una consulta sobre un registro específico, en este caso el `ID` con valor **2**, la respuesta obtenida es null, a pesar que en nuestra base de datos esta registrado nuestro amigo Jock, esto debido a que no cumple con el criterio establecido, lo mismo ocurrirá con el llamado a otros métodos como el de actualizar el registro, así que ten esto presente cuando vayas hacer uso de esta útil herramienta.

Ahora bien, a pesar del comportamiento explicado arriba, aún es posible acceder al verdadero TODOS de nuestra tabla `dogs`, entiéndase el resultado que obteníamos ante de la declaración de nuestro **global scope**, para ello podemos hacer uso de la siguiente instrucción:

```php
// trae todos los 4 registros
App\Models\Dog::withoutGlobalScope('age')->get();
```
 Ahora si obtendremos todos los registros contenidos en nuestra tabla, tal y como lo habríamos obtenido con un simple `all()` sin la declaración del **global scope**. Presta atención al hecho de que al método `withoutGlobalScope()` pasamos como parámetro el nombre del scope, algo perfectamente útil si tenemos varios **scopes** declarados y solo queremos eludir alguno(s) de ellos.

Es una recomendación obligatoria el dejar notas y/o comentarios dentro del modelo explicando el uso de los **global scope** que declaramos, esto con la finalidad de no crear dudas sobre posibles resultados que se obtengan al hacer consultas o cualquier otra acción que se vea afectado por dichos **scopes**, resultaría bastante frustrante para cualquier desarrollador el no poder entender porque no obtiene la respuesta esperada, lo que llevaría quizás mucho tiempo escudriñando y realizando pruebas para dar al fin con la causa. Puede parecer obvio cuando está leyendo este sencillo ejemplo, pero agregue DocBlocks, anotaciones y todas las demás capturas que encuentre en un código base en vivo real y evitar pérdida de tiempo valioso en un futuro!

## Soft-deletes:
Todos saben que es mejor no eliminar los registros de la base de datos, ¿verdad? Puede sonar un poco a broma puesto que muchos programadores de bases de datos son un anatema respecto a borrar cualquier cosa, pero es un asunto serio. En realidad, la eliminación de registros - especialmente accidentalmente - puede conducir a todo tipo de problemas, como registros huérfanos y falta de datos históricos para su análisis. Es mucho mejor práctica "marcarlos" como activos o no. Sin embargo, hacer esto significa agregar un campo **"active_flag"** en todas las tablas y luego recordar ponerlo en todas las cláusulas `where`. No es lo ideal. Lo bueno es que laravel ya lo tiene resuelto ¡Global scopes!

Debido a que este tipo de **global scope** es tan universalmente deseado, Laravel ha hecho lo que Laravel hace mejor - han preparado una solución fácil de usar llamada *"Soft Deletes"*. Funciona entre bastidores, en combinación con algunas funciones adicionales, para marcar los registros eliminados con un campo de marca de tiempo llamado *"deleted_at"*. ¡Incluso hay una función de migraciones para agregar esto!

Aunque **soft-deletes** trabaja como un **global scope**, su configuración es ligeramente diferente a como te mostré más arriba, así que echemos un vistazo rápido.

Debemos empezar por agregar el campo correspondiente en nuestra migración. Aunque podemos agregarlo dentro de la migración existente, es recomendado siempre ir paso a paso y crear un nuevo archivo que lo haga, así llevamos una secuencia de la evolución de nuestras migraciones y por ende de nuestra aplicación, así que vayamos a la terminal y ejecutemos:


```php
php artisan make:migration add_softdeletes_to_dogs_table
```

Esto creará nuestro un archivo migrations, vamos a abrirlo y coloquemos lo siguiente:

```php
class AddSoftdeletesToDogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('dogs', function (Blueprint $table) {
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('dogs', function (Blueprint $table) {
             $table->dropSoftDeletes();
        });
    }
}
```

Ya solo queda correr nuevamente el comando migrate (a estas alturas ya debería saber cómo), y listo, si revisas la base de datos veras que se ha agrado un nuevo campo a la tabla dogs:  `deleted_at`, el cual acepta valores NULL.

Como siguiente paso, debemos indicarle a nuestro modelo Dog que haga uso de esta función y del nuevo campo, para ello, demos ir hasta él y agregar lo siguiente:


```php
//importamos la librería
use Illuminate\Database\Eloquent\SoftDeletes;

class Dog extends Model
{
    //Invocamos el trait
    use SoftDeletes;
```

También queremos que el campo `deleted_at` sea una instancia del objeto Carbon. Esta es una característica realmente encantadora de Laravel. Para hacer esto, solo tenemos que agregar cualquier campo fecha al atributo $dates en la parte superior del modelo de la siguiente forma:

```php
protected $dates = ['deleted_at'];
```
Cualquier campo tipo fecha que coloques dentro de este array se convertirá en una instancia de clase Carbon. Esto te da una multitud de funciones fáciles de trabajar cuando necesitas formatear, comparar o manipular fechas. Si quieres saber más (y sugiero que lo hagas) pues revisar la documentación de [Carbon](https://carbon.nesbot.com/docs/) en su sitio oficial.

¡Eso es todo! ¡Ya terminaste! El uso de la función **Eloquent** `delete()` marcará automáticamente el `timestamp` de ese registro como eliminado, y ya no aparecerá en ningún resultado a menos que ignore específicamente el ámbito, esto es porque **soft-deletes** actua filtrando todos los registros que tengan el campo `deleted_at` igualo a `NULL` y devolviéndolos. Todas las instrucciones que hacen posible el trabajo de **soft-deletes**, se encuentran depositadas en el trait que invocamos dentro de nuestro modelo, lo que la hace totalmente reutilizable y adaptable a cualquier modelo que tenga tu aplicación. Como nota importante a este punto, te recomiendo que evites actualizar el campo `deleted_at` manualmente, si quieres ver como manipular este campo y/o ver como adaptar esta herramienta a tus necesidades, deberías dirigirte a la documentación oficial de laravel sobre [soft-deletes](https://laravel.com/docs/8.x/eloquent#soft-deleting) o darle un ojo al **trait**, siempre es bueno y muy educativo leer el código que otros escriben.

## Traits:
Tendemos a usar *traits* en Laravel para agrupar funciones relacionadas, incluso si no están destinadas a ser reutilizadas con otras clases. Si abres SoftDeletes en tu IDE, verás un ejemplo en el que claramente se están reutilizando, pero en casos como el sistema de autenticación, que también se basa en *traits*, se usan para mantener todas las funciones de contraseña, funciones de restablecimiento, entre otras, juntas para un espacio de trabajo más limpio.

Los *traits* no son parte de laravel en sí mismo, pues son un elemento directo del propio PHP, en ellos no hay secretos ocultos o clases abstractas de las que se deba heredar. Su utilidad es bastante amplia, pues nos permite depositar nuestra lógica dentro de ellas y poder ser reutilizada donde sea que se necesite. Para usar un trait solo basta con crearlo (normalmente en un nuevo directorio Traits), e importarlo en la clase que lo necesite a través del *"use FooTrait"* en la parte superior y listo.

Su uso es muy versatil, por ejemplo es muy común para mi usarlo en los modelos, en particular he trabajado en varios proyectos en donde se ha hecho necesario que el `ID` de las tablas sean mucho más complejos que un simple entero incremental, o también que contengan un campo `slug`, este tipo de necesidades siempre las soluciono a través de un *trait*, lo que me permite reutilizar el mismo código en todos mis modelos estando depositado este en un solo lugar y solo siendo invocado en donde lo necesito, así de simple, también son usados a menudo en controladores como funciones auxiliares, lo que permite que el controlador simplemente enumere las funciones principales de ruta y cambie todas las funciones de "acción" a una clase de servicio independiente. Esto realmente se reduce a tu propio estilo y preferencia – toma solo lo que tenga sentido para ti y te ayude a mantener tu código limpio.

Espero que estas primeras lecciones te estén ayudando a crear confianza con las herramientas que Laravel ofrece, puestas allí para ayudarte, y que estés empezando a ver cuánto puedes mejorar y ser más productivo en tus aplicaciones si confías en ellas.

En las próximas entregas vamos a estudiar la presentación de nuestros datos, así como la creación y actualización de registros, algunas técnicas más avanzadas con `where()` y un primer vistazo a las relaciones.

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 inicio de semana.

## Nota
Aprovecho para compartir un pequeño repositorio de lo que hemos visto hasta ahora, la verdad todo ha sido muy sencillo, sin embargo, para quienes quieran hacer sus propias pruebas o revisar algo que quizás se les haya escapado, pues les dejo el [link del repositorio](https://github.com/jtovarto/serie-eloquent-con-ejemplo), espero les sea de ayuda.