Explica cómo funcionan los scopes locales y globales. Analiza la implementación interna del framework, como SoftDeletingScope, y presenta casos de uso prácticos como multi-tenant o filtros de publicación.
Los scopes se invocan como métodos. Se pueden encadenar.
use App\Models\Post;// Solo los posts publicados$posts = Post::published()->get();// Publicados y populares, ordenados de más nuevo a más antiguo$posts = Post::published()->popular()->latest()->get();
Al conectar un scope con orWhere, a veces necesitas agrupar lógicamente.
// Con closure (funciona seguro pero es más verboso)$users = User::popular()->orWhere(function (Builder $query) { $query->active();})->get();// Usando el método de orden superior queda más limpio$users = User::popular()->orWhere->active()->get();
Un scope global es una clase que implementa la interfaz Illuminate\Database\Eloquent\Scope. Esta interfaz solo requiere un método apply.
// Definición de la interfaz en el framework// src/Illuminate/Database/Eloquent/Scope.phpinterface Scope{ public function apply(Builder $builder, Model $model);}
Dentro de apply añades restricciones al query builder.
En algunas consultas concretas querrás desactivar el scope.
use App\Models\Scopes\ActiveScope;// Excluir un scope concretoUser::withoutGlobalScope(ActiveScope::class)->get();// Excluir un scope definido con closureUser::withoutGlobalScope('active')->get();// Excluir todos los scopes globalesUser::withoutGlobalScopes()->get();// Excluir varios scopesUser::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();// Excluir todos menos los indicadosUser::withoutGlobalScopesExcept([TenantScope::class])->get();
Ver cómo el trait estándar SoftDeletes aprovecha los scopes globales revela un patrón de implementación útil.SoftDeletingScope implementa la interfaz Scope.
// src/Illuminate/Database/Eloquent/SoftDeletingScope.phpclass SoftDeletingScope implements Scope{ protected $extensions = [ 'Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed', ]; /** * Aplica el scope al query builder * Añade la restricción para obtener solo registros con deleted_at NULL */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); } /** * Extiende el query builder con macros * Deja disponibles métodos como withTrashed() y onlyTrashed() */ public function extend(Builder $builder) { foreach ($this->extensions as $extension) { $this->{"add{$extension}"}($builder); } // Sobrescribe la operación delete() para que actualice deleted_at en su lugar $builder->onDelete(function (Builder $builder) { $column = $this->getDeletedAtColumn($builder); return $builder->update([ $column => $builder->getModel()->freshTimestampString(), ]); }); }}
Implementación de withTrashed()
withTrashed() en realidad llama a withoutGlobalScope($this). Es decir, se excluye a sí mismo (SoftDeletingScope) para que los registros eliminados se puedan recuperar.
protected function addWithTrashed(Builder $builder){ $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { if (! $withTrashed) { return $builder->withoutTrashed(); } return $builder->withoutGlobalScope($this); });}
De forma parecida, onlyTrashed() se excluye a sí mismo y añade whereNotNull('deleted_at').
protected function addOnlyTrashed(Builder $builder){ $builder->macro('onlyTrashed', function (Builder $builder) { $model = $builder->getModel(); $builder->withoutGlobalScope($this)->whereNotNull( $model->getQualifiedDeletedAtColumn() ); return $builder; });}
La interfaz Scope no define el método extend, pero el builder de Eloquent lo llama automáticamente si el scope lo tiene. Es útil cuando quieres añadir macros propias.
En el panel de administración quieres ver también los posts no publicados, pero en el frontend solo los ya publicados.
<?phpnamespace App\Models\Scopes;use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Scope;class PublishedScope implements Scope{ public function apply(Builder $builder, Model $model): void { $builder->where('status', 'published') ->where('published_at', '<=', now()); }}
En el panel de administración se excluye el scope con withoutGlobalScope.
// Frontend: solo publicados (PublishedScope se aplica automáticamente)$posts = Post::latest()->get();// Admin: se muestran todos los posts$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
Cuando añadas columnas dentro de un scope global, utiliza addSelect en lugar de select. Si usas select, sobrescribirás las columnas que la consulta original haya seleccionado.
// Mal ejemplo: sobrescribe el select originalpublic function apply(Builder $builder, Model $model): void{ $builder->select('id', 'tenant_id', 'name');}// Buen ejemplo: añade la columna al select existentepublic function apply(Builder $builder, Model $model): void{ $builder->addSelect('tenant_id');}