Meccanismo di scope locali e globali. Analizziamo implementazioni interne del framework come SoftDeletingScope e mostriamo casi d’uso pratici come multi-tenant e filtri pubblicato/non pubblicato.
Gli scope definiti si invocano come metodi. Puoi anche concatenarli.
use App\Models\Post;// solo i post pubblicati$posts = Post::published()->get();// post pubblicati e popolari in ordine cronologico inverso$posts = Post::published()->popular()->latest()->get();
Concatenando uno scope con orWhere, può essere necessario un raggruppamento logico.
// con una closure (sicuro ma verboso)$users = User::popular()->orWhere(function (Builder $query) { $query->active();})->get();// usando gli higher-order si scrive in modo più conciso$users = User::popular()->orWhere->active()->get();
Un global scope è una classe che implementa l’interfaccia Illuminate\Database\Eloquent\Scope. L’interfaccia richiede un unico metodo apply.
// definizione dell'interfaccia nel framework// src/Illuminate/Database/Eloquent/Scope.phpinterface Scope{ public function apply(Builder $builder, Model $model);}
All’interno del metodo apply aggiungi il vincolo al query builder.
A volte vuoi disattivare uno scope per una query specifica.
use App\Models\Scopes\ActiveScope;// esclude uno scope specificoUser::withoutGlobalScope(ActiveScope::class)->get();// esclude uno scope definito con closureUser::withoutGlobalScope('active')->get();// esclude tutti i global scopeUser::withoutGlobalScopes()->get();// esclude più scopeUser::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();// esclude tutti tranne quelli indicatiUser::withoutGlobalScopesExcept([TenantScope::class])->get();
Osservare come il trait standard SoftDeletes usa i global scope fa capire i pattern implementativi.SoftDeletingScope implementa l’interfaccia Scope.
// src/Illuminate/Database/Eloquent/SoftDeletingScope.phpclass SoftDeletingScope implements Scope{ protected $extensions = [ 'Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed', ]; /** * Applica lo scope al query builder * aggiunge il vincolo per recuperare solo i record con deleted_at NULL */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); } /** * Estende il query builder con macro * abilita withTrashed() / onlyTrashed() ecc. */ public function extend(Builder $builder) { foreach ($this->extensions as $extension) { $this->{"add{$extension}"}($builder); } // sovrascrive l'operazione delete() per aggiornare deleted_at $builder->onDelete(function (Builder $builder) { $column = $this->getDeletedAtColumn($builder); return $builder->update([ $column => $builder->getModel()->freshTimestampString(), ]); }); }}
L'implementazione di withTrashed()
In pratica withTrashed() chiama withoutGlobalScope($this). Escludendo lo stesso SoftDeletingScope, i record cancellati diventano di nuovo recuperabili.
protected function addWithTrashed(Builder $builder){ $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { if (! $withTrashed) { return $builder->withoutTrashed(); } return $builder->withoutGlobalScope($this); });}
Anche onlyTrashed() esclude lo scope e aggiunge 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; });}
L’interfaccia Scope non definisce il metodo extend, ma il builder di Eloquent, se lo scope lo espone, lo chiama automaticamente. Puoi sfruttarlo per aggiungere macro personalizzate.
Quando nel back office vuoi vedere anche i post non pubblicati, ma sul frontend solo quelli pubblicati.
<?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()); }}
Nel back office escludi lo scope con withoutGlobalScope.
// frontend: solo pubblicati (PublishedScope si applica automaticamente)$posts = Post::latest()->get();// back office: mostra tutti i post$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
Quando aggiungi colonne all’interno di un global scope, usa addSelect invece di select. Con select sovrascriveresti le colonne che il chiamante ha già indicato.
// esempio errato: sovrascrive il select del chiamantepublic function apply(Builder $builder, Model $model): void{ $builder->select('id', 'tenant_id', 'name');}// esempio corretto: si aggiunge alle select esistentipublic function apply(Builder $builder, Model $model): void{ $builder->addSelect('tenant_id');}