DEV Community

Cover image for Aprende eloquent con ejemplos!!! Lección 4  - Model Scopes
Johan Tovar
Johan Tovar

Posted on

Aprende eloquent con ejemplos!!! Lección 4  - Model Scopes

Bienvenidos de nuevo a “Eloquent con ejemplos”, en la pasada entrega aprendimos los conceptos básicos de los Modelos y una nueva herramienta llamada Tinker. Hasta ahora hemos tocado solamente un poco de la superficie de Eloquent y su funcionamiento, como habrás notado, a pesar de que este es un curso de Laravel, todavía no hemos construido ninguna ruta o páginas web!

Laravel es un ecosistema muy grande que abarca mucho más que las páginas web estándar. Aprender a mirar una sección aislada de ella, como lo estamos haciendo con Eloquent, será muy útil para desarrollar tanto nuestra velocidad y flujo de trabajo, como nuestra comprensión de cada uno de ellas, por lo tanto, seguiremos trabajando con la línea de comandos y Tinker por un tiempo más. Puede sentirse un poco incómodo si no estás acostumbrado a trabajar de esta manera, pero al final tendrá grandes beneficios.

En esta lección vamos a empezar a hacer algunas consultas de base de datos!

Cosas que aprenderemos:

  • Model scopes

Uno de los mayores objetivos de esta serie es ayudar a las personas a ser competentes, y no sólo conocedoras, sobre el uso de las herramientas artisan, es por ello que repasaremos una vez más como construir nuestras tablas y datos. ¡No te preocupes, solo será un repaso y lo haremos rápidamente!

1) Aquí hay un comando que aún no hemos utilizado con migrate - "reset". Esto revertirá todas las migraciones para que podamos empezar de nuevo.

php artisan migrate:reset
Enter fullscreen mode Exit fullscreen mode

2) Eliminemos los archivos creados en entregas pasadas:

  • El factory DogFactory
  • El seeder DogsTableSeeder
  • El modelo Dog
  • La migración _create_dogs_table

Probablemente deberías ejecutar en la terminal composer dump-autoload, ya que los archivos a veces se registran en el cargador automático y causan problemas más adelante cuando están "desaparecidos".

3) ¿Recuerda cómo hacer la migración y el modelo al mismo tiempo? ¡Espero que si!, igual aquí tienes la respuesta:

php artisan make:model Dogs -mfs
Enter fullscreen mode Exit fullscreen mode

Esto crea nuestro modelo, junto con la migración, factory y seeder respectivamente.

4) En nuestra migración, queremos que los perros tengan un nombre, un género y un campo de edad. Agrega a la función up() dichos campos, de manera que te quede así:

public function up()
{
    Schema::create('dogs', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('gender')->nullable();
        $table->integer('age')->nullable();
        $table->timestamps();
    });
}
Enter fullscreen mode Exit fullscreen mode

5) Para este ejercicio, todos deberíamos tener los mismos datos para que podamos hacer algunas consultas específicas, por lo que usaremos model factories para crear introducir datos semillas en lugar de usar Faker. Vayamos a nuestro DogSeeder y coloquemos el siguiente código:

Nota: Aunque usamos model factories, no la configuraremos todavía como es debido, si embargo esto no impedirá que la usemos sin problemas. En proximas lecciones nos dedicaremos a establecer nuestra factory como es debido.

public function run()
{
    Dog::factory()->create(['name' => 'Joe', 'age' => 5 , gender => male]);
    Dog::factory()->create(['name' => 'Jock', 'age' => 7, gender => male ]);
    Dog::factory()->create(['name' => 'Jessie', 'age' => 2, gender => female ]);
    Dog::factory()->create(['name' => 'Jane', 'age' => 9, gender => female ]);
}
Enter fullscreen mode Exit fullscreen mode

1) Configuremos el archivo seeder central para que ejecute nuestro nuevo DogSeeder. Para esto, debemos ubicarnos en el archivo database\seeders\DatabaseSeeder y hacer la siguiente invocación:

public function run()
{
    $this->call(DogSeeder::class);
}
Enter fullscreen mode Exit fullscreen mode

7) Finalmente ejecutemos nuestras migraciones junto con los archivos seeders:

php artisan migrate --seed
Enter fullscreen mode Exit fullscreen mode

Model scopes:

Muy bien, ejecutemos Tinker y empecemos a consultar!

Tenemos perros de cuatro edades distintas (2, 5, 7 y 9 años), dos de ellos son hembras y dos son machos. Nos gustaría ver a todos los perros mayores de 6 años. La forma sencilla de hacer esto es simplemente poner una cláusula where() en nuestro modelo Dog, así:

php artisan tinker
Psy Shell v0.10.8 (PHP 8.0.5  cli) by Justin Hileman

App\Models\Dog::where('age', '>', 6)->get();
Enter fullscreen mode Exit fullscreen mode

Deberías ver a los encantadores Jock y Jane en pantalla.

Algo que confunde a muchas personas cuando comienzan con Eloquent es el hecho de que este se basa en QueryBuilder, pero los resultados vuelven como un objeto Collection. Esto significa que vas a encontrarte con muchas funciones con nombre similar que parecen ser diferentes dependiendo de si estás creando la consulta o manipulando los registros devueltos. En este caso estamos utilizando where() para crear una cláusula where en nuestra consulta, no para filtrar el conjunto de resultados. Para este curso usaremos las funciones querybuilder para crear instrucciones sql, te recomiendo ampliar mas sobre el tema en la documentación oficial de Laravel sobre las Query Builder.

El código que acabamos de ejecutar funciona, pero tiene algunos problemas desde el punto de vista de diseño y mantenimiento. Con este código en nuestro controlador es como si estuviéramos codificando instrucciones sql en toda nuestra aplicación. Imagina que comenzamos a usar dicha instrucción en varios lugares de nuestra aplicación, podría darse el caso que esa edad cambie por alguna razón, obligándonos a tener que a cambiarla en todos y cada uno de esos lugares, tarea bastante tediosa y poco práctica, sin mencionar que podríamos saltarnos uno o varios de ellos, generando posibles errores e inconsistencias. Por otra parte, tampoco dicha instrucción no explica el por qué usamos esa edad en específico o cuales son sus casos de uso y/o de aplicación.

Usemos algo llamado "scopes" para hacer una restricción más limpia.

class Dog extends Model
{
    function scopeAgeGreaterThan($query, $age) {
        return $query->where('age', '>', $age);
    }

    function scopeGender($query, $gender) {
        return $query->where('gender',$gender);
    }
}
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, tenemos dos métodos arriba que siguen a una convención de nomenclatura muy específica, un subproceso común en Laravel. La función debe comenzar con "scope" y el primer parámetro será la instancia de QueryBuilder inyectada, dicho argumento puede tener cualquier nombre que quieras darle, pero normalmente $q o $query se utiliza para mayor claridad, depende de ti el nombre y la cantidad de parámetros subsecuentes que quieras inyectar, recomiendo el uso de nombre descriptivos como $age o $gender que dan una idea clara de la razón de su existencia dentro de cada método. Ahora puedes utilizar esta función de esta forma (reinicia la sesión de Tinker para que actualice los último cambios):

App\Models\Dog::ageGreaterThan(6)get();
Enter fullscreen mode Exit fullscreen mode

Aquí hemos usado el primero de nuestros scopes, observa la respuesta en pantalla y verás que es exactamente la misma que hemos obtenido anteriormente, ¿la diferencia?, podremos usarla en cualquier parte de nuestra aplicación, con el plus de solo tener que modificarla en un solo lugar. Por otro lado es bueno mencionar que los scopes toman una instancia de QueryBuilder y la retornan, lo que permite encadenar cualquier cantidad de funciones como varios where(), orderBy(), entre otros. Veamos un ejemplo claro.

Vamos a nuestro modelo, y agreguemos:

App\Models\Dog::ageGreaterThan(6)->gender('female')->get();
Enter fullscreen mode Exit fullscreen mode

Fácilmente hemos filtrado nuestro primer resultado y ahora solo nos aparece la hermosa Jane en pantalla, sin clausulas where visibles ni largas líneas de código, todo muy limpio, compacto y totalmente reusable. ¡Genial!

En la documentación oficial de laravel encontrarás un apartado dedicado al uso de clausulas where y lo que ellos llaman logical-grouping. Recomiendo encarecidamente su lectura, te será de mucha ayuda.

Esto es agradable, sin embargo todavía no está muy claro cuál es el propósito de dichos scopes, así que, vamos a darles un sentido: Digamos que a cierta edad, nuestro amigos caninos requieren de un tratamiento especial que les ayudará en su calidad de vida cuando lleguen a una edad avanza, ademas, este tratamiento es diferente según su género. Bajo este supuesto, vamos a hacer algunos cambios en nuestro modelo:

class Dog extends Model
{
    function scopeAgeGreaterThan($query, $age) {
        return $query->where('age', '>', $age);
    }

    function scopeGender($query, $gender) {
        return $query->where('gender',$gender);
    }

   function dogsForSpecialTreatment($gender) {
        return $this->AgeGreaterThan(6)->gender($gender);
    }
}
Enter fullscreen mode Exit fullscreen mode

Reiniciemos tinker una vez mas:

(new App\Models\Dog)->dogsForSpecialTreatment('female')->get();
Enter fullscreen mode Exit fullscreen mode

Resaltemos un par de cosas en el código de arriba. Tenemos un método común dogsForSpecialTreatment($gender) en nuestro modelo, el cual hace uso de scopes dentro de sus instrucciones, creando la consulta necesaria para cumplir con los requerimientos de nuestro tratamiento especial, puedes jugar cambiando el género y verás como se alterna el resultado entre Jock y Jane. Es importante destacar que ahora nuestro modelo tiene un método más descriptivo, cuyo nombre denota un propósito bien definido. Por otro lado, observa que al hacer uso de este método, ya no lo hacemos de la misma forma que con nuestros scopes, pues a este último accedemos mediante la forma static::, mientras que a nuestro nuevo método accedemos como a cualquier método de clase común, debiendo para ello crear una instancia de nuestro modelo usando new.

El ejemplo anterior se mantiene intencionalmente simple, y por lo tanto, si te preguntas ¿Por qué no hacer un solo **scope 'dogsForSpecialTreatment()' con toda la lógica dentro de él?** , pues dejame decirte que estas en lo cierto y tiene sentido. Sin embargo, las aplicaciones del mundo real suelen tener un conjunto de criterios que definen un cierto estado o grupo (por ejemplo, límites de edad o el género), los cuales en algún punto dado, como algún tipo de reporte, se necesita trabajar con algún tipo de subconjunto derivado (todos los perros mayores de 6 años y que sean hembras). Si piensas en diseñar y crear tus modelos a partir de piezas más granulares, en lugar de una gran función completa y monolítica, facilitará el mantenimiento de tus aplicaciones, te mantendrá alejado de la duplicación de código e incluso te ayudará a darle un significado explícito que exprese que es lo que hace, algo de bastante ayuda para cuando vuelvas a hacer cambios unos meses más tarde.

La siguiente lección introducirá alcances globales (global scopes), así como los ámbitos Laravel incorporados, como soft-deletes. No seguiremos repitiendo todos los pasos de reconstrucción cada día, así que vuelve a esta lección si necesitas ayuda para recordar cómo restablecer la base de datos para lecciones posteriores.

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.

Top comments (0)