Vamos a hablar de los Query Scopes. Son una herramienta potente, nadie lo duda. Te permiten limpiar tus consultas y hacerlas legibles. Pero tienen un lado oscuro que termina pasando factura, sobre todo en equipos donde no todo el mundo es especialista en backend: la magia implícita.
Puedes añadir PHPDocs hasta aburrirte, pero al final estás llamando a métodos que no aparecen definidos en tu modelo. Es magia, y la magia no escala bien.
#¿Qué son los Scopes y por qué nos enamoramos de ellos?
Imagina esta consulta típica para obtener usuarios activos y populares:
1use App\Models\User;
2
3$users = User::query()
4 ->where('votes', '>', 100)
5 ->where('active', 1)
6 ->orderBy('created_at')
7 ->get();
Funciona, pero cuando la lógica se repite o se complica, la abstracción mediante scopes es la solución natural. Los defines en el modelo:
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7
8class User extends Model
9{
10 public function scopePopular(Builder $query): void
11 {
12 $query->where('votes', '>', 100);
13 }
14
15 public function scopeActive(Builder $query): void
16 {
17 $query->where('active', 1);
18 }
19}
Y tu consulta se transforma en algo mucho más expresivo:
1$users = User::query()
2 ->popular()
3 ->active()
4 ->orderBy('created_at')
5 ->get();
La legibilidad mejora de forma evidente. El problema viene después.
#La factura de la magia: Autocompletado y código inflado
Tu IDE no tiene ni idea de que esos métodos popular() o active() existen. Se resuelven en tiempo de ejecución gracias al prefijo scope. Para ayudar a tu editor de código, tienes que recurrir a anotaciones:
1/**
2 * @method static Builder popular()
3 * @method static Builder active()
4 */
5class User extends Model
Es un parche. El verdadero problema estructural es otro: los modelos se hinchan. Acabas teniendo una clase de modelo que, además de relaciones, mutadores y lógica de negocio, está repleta de docenas de métodos de scope. Pierdes la capacidad de ver lo importante de un vistazo.
#La alternativa organizada: Constructores de consultas personalizados
La solución es mover toda esa lógica de consulta a una clase dedicada: un Custom Query Builder. No es más que una clase que extiende Illuminate\Database\Eloquent\Builder.
Así se crea, por ejemplo, en app/Eloquent/QueryBuilders/UserQueryBuilder.php:
1<?php
2
3namespace App\Eloquent\QueryBuilders;
4
5use Illuminate\Database\Eloquent\Builder;
6
7class UserQueryBuilder extends Builder
8{
9 public function popular(): self
10 {
11 return $this->where('votes', '>', 100);
12 }
13
14 public function active(): self
15 {
16 return $this->where('active', 1);
17 }
18}
Luego, le dices a tu modelo User que utilice este constructor por defecto:
1<?php
2
3namespace App\Models;
4
5use App\Eloquent\QueryBuilders\UserQueryBuilder;
6use Illuminate\Database\Eloquent\Model;
7
8class User extends Model
9{
10 // Usar nuestro Builder personalizado
11 public function newEloquentBuilder($query): UserQueryBuilder
12 {
13 return new UserQueryBuilder($query);
14 }
15
16 // Para tener hints de tipo correctos
17 public static function query(): UserQueryBuilder
18 {
19 return parent::query();
20 }
21}
El resultado es que ahora tu consulta funciona exactamente igual, pero con autocompletado full, navegación directa al código y cero magia:
1$users = User::query()
2 ->popular() // Tu IDE sabe que este método existe
3 ->active()
4 ->orderBy('created_at')
5 ->get();
#Ventaja extra: Resolución dinámica de Builders
Esta aproximación abre la puerta a estrategias más sofisticadas. Imagina que quieres un constructor de consultas diferente según el estado del modelo:
1public function newEloquentBuilder($query): UserQueryBuilder
2{
3 if ($this->status === State::Pending) {
4 return new PendingUserQueryBuilder($query); // Extiende UserQueryBuilder
5 }
6
7 return new UserQueryBuilder($query);
8}
De este modo, evitas tener un único Builder gigante y puedes agrupar la lógica de consulta por contextos específicos, manteniendo cada clase cohesionada y con una responsabilidad clara.
#Conclusión: Cuándo usar cada herramienta
- Usa Scopes cuando tengas 2 o 3 métodos muy simples y el modelo no esté sobresaturado. Son rápidos de implementar.
- Apostilla por Custom Query Builders cuando la lógica de consulta crezca, trabajes en equipo o valores la mantenibilidad a largo plazo. El esfuerzo inicial de configurarlos se paga solo con el primer viaje que haga clic derecho -> "Ir a la definición" y acierte.
Los builders personalizados organizan tu código, eliminan la magia y hacen que tu base sea más robusta y fácil de navegar. Para mí, es el camino claro una vez el proyecto pasa de cierta complejidad.