Poznajemy loklane zasięgi dynamiczne, coś dużo łatwiejszego niż jego nazwa. Do dzieła.

Ok, oto przykład zasięgu dynamicznego:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     */
    public function scopeOfType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

Dynamiczny, czyli przyjmuje jakiś argument. Tylko lokalny może być dynamiczny. Ten argument przekazujemy w nawiasach:

$admins = User::ofType('admin')->get();

Tak byśmy użyli dynamicznego. A statyczny?

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     */
    public function scopeOfAdminType(Builder $query): void
    {
        $query->where('type', 'admin');
    }
}

Ten jest statyczny, tylko buildera przyjmuje. Tak byśmy go użyli:

$admins = User::ofAdminType()->get();

I oczywiście tylko lokalne mogą być statyczne, globalne chodzą pod każdym query i nic im przekazać się nie da.

To kilka przykładów z fajnego repo na githubie, tych samych funkcji jako statyczne i dynamiczne zasięgi lokalne:

public function scopePopular(Builder $query): Builder
    {
        return $query->withCount('reviews')
            ->orderBy('reviews_count', 'desc');
    }

    public function scopeHighestRated(Builder $query): Builder
    {
        return $query->withAvg('reviews', 'rating')
            ->orderBy('reviews_avg_rating', 'desc');
    }

Teraz wersja dynamiczna:

public function scopePopular(Builder $query, $from = null, $to = null): Builder|QueryBuilder
    {
        return $query->withCount([
            'reviews' => fn(Builder $q) => $this->dateRangeFilter($q, $from, $to)
        ])
            ->orderBy('reviews_count', 'desc');
    }

I wersja dynamiczna drugiego:

public function scopeHighestRated(Builder $query, $from = null, $to = null): Builder|QueryBuilder
    {
        return $query->withAvg([
            'reviews' => fn(Builder $q) => $this->dateRangeFilter($q, $from, $to)
        ], 'rating')
            ->orderBy('reviews_avg_rating', 'desc');
    }

private function dateRangeFilter(Builder $query, $from = null, $to = null)
    {
        return $query->withRecentReviews(fn($date) => $date->subWeek());
        if ($from && !$to) {
            $query->where('created_at', '>=', $from);
        } elseif (!$from && $to) {
            $query->where('created_at', '<=', $to);
        } elseif ($from && $to) {
            $query->whereBetween('created_at', [$from, $to]);
        }
    }

Bardziej chodzi mi o pokazanie idei czym jest dynamiczny zasięg lokalny, bo te przykłady mogą być odrobinę zbyt trudne, plus musimy przyjąć, że autor gdzieś stworzył inny zasięg lokalny withRecentReviews, do którego odwołuje się w metodzie pomocniczej dateRangeFilter, do której odwołuje się w swoim zasięgu.

Bardziej to pokazuję po to, aby uświadomić, że „tak można”, bo bardzo często nie chodzi o to, aby przedstawić komuś techniczne możliwości (możesz stworzyć klasę, metodę, metodę statyczną bla bla bla) ani kazać komuś zrozumieć jakiś kod (masz tu bryłę kodu i powiedz mi co jak działa i dlaczego) tylko chodzi o to, aby przedstawić pewne możliwości konceptualnego podejścia do rozwiązywania problemów.

Zresztą, niedługo poznamy bardziej zaawansowane rzeczy w Laravelu i takie scopes też będziemy sobie spokojnie pisali.