Zum Hauptinhalt springen

Was sind Scopes?

Eloquent-Scopes sind ein Mechanismus, um Einschränkungen für Queries zusammenzufassen und wiederzuverwenden. Es gibt zwei Arten von Scopes.
ArtZeitpunkt der AnwendungVerwendungszweck
Globaler ScopeImmer automatisch angewendetSoft Deletes, Multi-Tenancy, Sichtbarkeitsfilter
Lokaler ScopeNur bei expliziter NutzungGemeinsame Filter wie „beliebte Beiträge” oder „aktive Nutzer”

Lokale Scopes

Definition

Lokale Scopes definieren Sie, indem Sie einer Modellmethode das Attribut #[Scope] hinzufügen.
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Auf veröffentlichte Beiträge einschränken
     */
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', 'published');
    }

    /**
     * Auf beliebte Beiträge einschränken
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('views', '>', 1000);
    }
}
Das Attribut #[Scope] liegt im Namensraum Illuminate\Database\Eloquent\Attributes und ist native Syntax ab PHP 8.0.

Verwendung

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();

Parameter übergeben

Zusätzliche Parameter definieren Sie ab dem zweiten Argument der Scope-Methode.
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
Beim Aufruf übergeben Sie die Argumente einfach.
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();

Kombination mit orWhere

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();

Globale Scopes

Funktionsweise

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.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
In apply fügen Sie dem Query Builder Einschränkungen hinzu.

Eine globale Scope-Klasse erstellen

Erzeugen Sie ein Gerüst mit dem Befehl make:scope.
php artisan make:scope ActiveScope
Die Datei app/Models/Scopes/ActiveScope.php wird angelegt.
<?php

namespace 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);
    }
}

Auf das Modell anwenden

1

Registrierung über #[ScopedBy] (empfohlen)

In Laravel 13 ist die Registrierung über das Attribut #[ScopedBy] am einfachsten.
<?php

namespace App\Models;

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;

#[ScopedBy([ActiveScope::class])]
class User extends Model
{
    //
}
Mehrere Scopes werden als Array übergeben.
#[ScopedBy([ActiveScope::class, TenantScope::class])]
class User extends Model
{
    //
}
2

Manuelle Registrierung über booted()

Sie können auch die booted-Methode überschreiben und addGlobalScope aufrufen.
<?php

namespace App\Models;

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new ActiveScope);
    }
}
Nach dem Hinzufügen des globalen Scopes wird bei allen Queries wie User::all() automatisch WHERE is_active = 1 gesetzt.

Globaler Scope über eine anonyme Closure

Für einfache Scopes, die keine eigene Klasse rechtfertigen, können Sie Closures verwenden.
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
Um einen per Closure definierten Scope später auszuschließen, verwenden Sie nicht den Klassennamen, sondern den Scope-Namen (als String).

Globale Scopes ausschließen

Manchmal wollen Sie den Scope für eine bestimmte Query deaktivieren.
use App\Models\Scopes\ActiveScope;

// Einen bestimmten Scope ausschließen
User::withoutGlobalScope(ActiveScope::class)->get();

// Einen per Closure definierten Scope ausschließen
User::withoutGlobalScope('active')->get();

// Alle globalen Scopes ausschließen
User::withoutGlobalScopes()->get();

// Mehrere Scopes ausschließen
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// Alle bis auf den angegebenen Scope ausschließen
User::withoutGlobalScopesExcept([TenantScope::class])->get();

Framework-intern: SoftDeletingScope

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.php

class 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(),
            ]);
        });
    }
}
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.

Praktische Anwendungsfälle

Multi-Tenancy: automatische Filterung per Tenant-ID

In SaaS-Anwendungen ist es entscheidend, jede Query automatisch mit einem Tenant-ID-Filter zu versehen.
<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenantId = auth()->user()?->tenant_id) {
            $builder->where('tenant_id', $tenantId);
        }
    }
}
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([TenantScope::class])]
class Post extends Model
{
    //
}
Damit liefert bereits Post::all() nur die Daten des Tenants des eingeloggten Nutzers.

Filter für veröffentlichte/nicht veröffentlichte Inhalte

Im Admin-Bereich wollen Sie auch unveröffentlichte Beiträge sehen, im Frontend nur veröffentlichte.
<?php

namespace 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());
    }
}
Im Admin schließen Sie den Scope mit withoutGlobalScope aus.
// Frontend: nur veröffentlichte Beiträge (PublishedScope automatisch aktiv)
$posts = Post::latest()->get();

// Admin: alle Beiträge anzeigen
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();

addSelect statt select verwenden

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 Aufrufers
public function apply(Builder $builder, Model $model): void
{
    $builder->select('id', 'tenant_id', 'name');
}

// Gut: erweitert das bestehende select
public function apply(Builder $builder, Model $model): void
{
    $builder->addSelect('tenant_id');
}

Nächste Schritte

Eigene Casts in Eloquent

Lernen Sie, wie Sie Attribut-Konvertierungslogik als eigene Casts umsetzen und Value-Object-Muster nutzen.
Zuletzt geändert am 13. Juli 2026