Erläutert die Funktionsweise von lokalen und globalen Scopes. Anhand interner Implementierungen wie SoftDeletingScope werden praktische Anwendungsfälle wie Multi-Tenancy oder Sichtbarkeitsfilter vorgestellt.
Definierte Scopes lassen sich als Methoden aufrufen und verketten.
use App\Models\Post;// Nur veröffentlichte Beiträge holen$posts = Post::published()->get();// Veröffentlichte und beliebte Beiträge in absteigender Reihenfolge holen$posts = Post::published()->popular()->latest()->get();
Beim Verknüpfen von Scopes mit orWhere kann eine logische Gruppierung nötig werden.
// Mit Closure (sicher, aber ausführlicher)$users = User::popular()->orWhere(function (Builder $query) { $query->active();})->get();// Die Higher-Order-Methode ermöglicht eine schlankere Schreibweise$users = User::popular()->orWhere->active()->get();
Ein globaler Scope ist eine Klasse, die das Interface Illuminate\Database\Eloquent\Scope implementiert. Dieses Interface verlangt nur die Methode apply.
// Interface-Definition im Framework// src/Illuminate/Database/Eloquent/Scope.phpinterface Scope{ public function apply(Builder $builder, Model $model);}
In apply fügen Sie dem Query Builder Einschränkungen hinzu.
Erzeugen Sie ein Gerüst mit dem Befehl make:scope.
php artisan make:scope ActiveScope
Die Datei app/Models/Scopes/ActiveScope.php wird angelegt.
<?phpnamespace App\Models\Scopes;use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Scope;class ActiveScope implements Scope{ /** * Wendet den Scope auf den Query Builder an */ public function apply(Builder $builder, Model $model): void { $builder->where('is_active', true); }}
Manchmal wollen Sie den Scope für eine bestimmte Query deaktivieren.
use App\Models\Scopes\ActiveScope;// Einen bestimmten Scope ausschließenUser::withoutGlobalScope(ActiveScope::class)->get();// Einen per Closure definierten Scope ausschließenUser::withoutGlobalScope('active')->get();// Alle globalen Scopes ausschließenUser::withoutGlobalScopes()->get();// Mehrere Scopes ausschließenUser::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();// Alle bis auf den angegebenen Scope ausschließenUser::withoutGlobalScopesExcept([TenantScope::class])->get();
Wenn Sie sich anschauen, wie der Standard-Trait SoftDeletes einen globalen Scope einsetzt, sehen Sie die Implementierungsmuster.SoftDeletingScope implementiert das Interface Scope.
// src/Illuminate/Database/Eloquent/SoftDeletingScope.phpclass SoftDeletingScope implements Scope{ protected $extensions = [ 'Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed', ]; /** * Wendet den Scope auf den Query Builder an * Fügt die Einschränkung hinzu, dass nur Datensätze mit deleted_at IS NULL geladen werden */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); } /** * Erweitert den Query Builder um Makros * Ermöglicht Methoden wie withTrashed() / onlyTrashed() usw. */ public function extend(Builder $builder) { foreach ($this->extensions as $extension) { $this->{"add{$extension}"}($builder); } // delete() überschreiben, damit stattdessen deleted_at aktualisiert wird $builder->onDelete(function (Builder $builder) { $column = $this->getDeletedAtColumn($builder); return $builder->update([ $column => $builder->getModel()->freshTimestampString(), ]); }); }}
Implementierung von withTrashed() ansehen
withTrashed() ruft intern withoutGlobalScope($this) auf. Das heißt, es schließt den SoftDeletingScope selbst aus, sodass auch gelöschte Datensätze zurückgegeben werden.
protected function addWithTrashed(Builder $builder){ $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { if (! $withTrashed) { return $builder->withoutTrashed(); } return $builder->withoutGlobalScope($this); });}
onlyTrashed() schließt ebenfalls den Scope aus und fügt zusätzlich whereNotNull('deleted_at') hinzu.
protected function addOnlyTrashed(Builder $builder){ $builder->macro('onlyTrashed', function (Builder $builder) { $model = $builder->getModel(); $builder->withoutGlobalScope($this)->whereNotNull( $model->getQualifiedDeletedAtColumn() ); return $builder; });}
Das Interface Scope verlangt keine extend-Methode, aber der Eloquent-Builder ruft sie automatisch auf, sofern der Scope sie besitzt. Nutzen Sie das, um eigene Makros hinzuzufügen.
Wenn Sie in einem globalen Scope Spalten ergänzen, verwenden Sie addSelect und nicht select. Mit select überschreiben Sie die vom Aufrufer bereits festgelegten Spalten.
// Schlecht: überschreibt das select des Aufruferspublic function apply(Builder $builder, Model $model): void{ $builder->select('id', 'tenant_id', 'name');}// Gut: erweitert das bestehende selectpublic function apply(Builder $builder, Model $model): void{ $builder->addSelect('tenant_id');}