> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent-Scopes

> Erläutert die Funktionsweise von lokalen und globalen Scopes. Anhand interner Implementierungen wie SoftDeletingScope werden praktische Anwendungsfälle wie Multi-Tenancy oder Sichtbarkeitsfilter vorgestellt.

## Was sind Scopes?

Eloquent-Scopes sind ein Mechanismus, um Einschränkungen für Queries zusammenzufassen und wiederzuverwenden. Es gibt zwei Arten von Scopes.

| Art                | Zeitpunkt der Anwendung      | Verwendungszweck                                               |
| ------------------ | ---------------------------- | -------------------------------------------------------------- |
| **Globaler Scope** | Immer automatisch angewendet | Soft Deletes, Multi-Tenancy, Sichtbarkeitsfilter               |
| **Lokaler Scope**  | Nur bei expliziter Nutzung   | Gemeinsame 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 theme={null}
<?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);
    }
}
```

<Info>
  Das Attribut `#[Scope]` liegt im Namensraum `Illuminate\Database\Eloquent\Attributes` und ist native Syntax ab PHP 8.0.
</Info>

### Verwendung

Definierte Scopes lassen sich als Methoden aufrufen und verketten.

```php theme={null}
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.

```php theme={null}
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
```

Beim Aufruf übergeben Sie die Argumente einfach.

```php theme={null}
$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.

```php theme={null}
// 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`.

```php theme={null}
// 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`.

```bash theme={null}
php artisan make:scope ActiveScope
```

Die Datei `app/Models/Scopes/ActiveScope.php` wird angelegt.

```php theme={null}
<?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

<Steps>
  <Step title="Registrierung über #[ScopedBy] (empfohlen)">
    In Laravel 13 ist die Registrierung über das Attribut `#[ScopedBy]` am einfachsten.

    ```php theme={null}
    <?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.

    ```php theme={null}
    #[ScopedBy([ActiveScope::class, TenantScope::class])]
    class User extends Model
    {
        //
    }
    ```
  </Step>

  <Step title="Manuelle Registrierung über booted()">
    Sie können auch die `booted`-Methode überschreiben und `addGlobalScope` aufrufen.

    ```php theme={null}
    <?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);
        }
    }
    ```
  </Step>
</Steps>

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.

```php theme={null}
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
```

<Warning>
  Um einen per Closure definierten Scope später auszuschließen, verwenden Sie nicht den Klassennamen, sondern den Scope-Namen (als String).
</Warning>

### Globale Scopes ausschließen

Manchmal wollen Sie den Scope für eine bestimmte Query deaktivieren.

```php theme={null}
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`.

```php theme={null}
// 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(),
            ]);
        });
    }
}
```

<Accordion title="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.

  ```php theme={null}
  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.

  ```php theme={null}
  protected function addOnlyTrashed(Builder $builder)
  {
      $builder->macro('onlyTrashed', function (Builder $builder) {
          $model = $builder->getModel();

          $builder->withoutGlobalScope($this)->whereNotNull(
              $model->getQualifiedDeletedAtColumn()
          );

          return $builder;
      });
  }
  ```
</Accordion>

<Tip>
  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.
</Tip>

## 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 theme={null}
<?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);
        }
    }
}
```

```php theme={null}
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 theme={null}
<?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.

```php theme={null}
// 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

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

  ```php theme={null}
  // 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');
  }
  ```
</Warning>

## Nächste Schritte

<Card title="Eigene Casts in Eloquent" icon="wand-magic-sparkles" href="/de/advanced/eloquent-casts">
  Lernen Sie, wie Sie Attribut-Konvertierungslogik als eigene Casts umsetzen und Value-Object-Muster nutzen.
</Card>


## Related topics

- [PHP-Attribute](/de/advanced/php-attributes.md)
- [Deferred Service Provider](/de/advanced/deferred-provider.md)
- [Eloquent Bootable Traits](/de/advanced/eloquent-bootable-traits.md)
- [Laravel Socialite (Social Authentication)](/de/socialite.md)
- [Socialite für Discord](/de/packages/socialite-discord.md)
