Aprende eloquent con ejemplos!!! Lección 5 - Global scopes, Soft-deletes y Traits

Aprende eloquent con ejemplos!!! Lección 5 - Global scopes, Soft-deletes y Traits

¡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:

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

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:

// 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:


//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:

// 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 artisan make:migration add_softdeletes_to_dogs_table

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

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:

//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:

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 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 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, espero les sea de ayuda.