LaravelMiddleTechnical

Что такое scopes в Laravel (local vs global) в Eloquent?

Local scope — метод scopeXxx() на модели, вызывается явно в цепочке запроса; global scope — применяется к каждому запросу автоматически (через addGlobalScope) и отключается через withoutGlobalScope().

Scopes в Eloquent: local и global

Scopes позволяют инкапсулировать повторяющиеся условия запросов внутри модели, делая код в контроллерах и сервисах чище и читаемее.

Local Scopes — переиспользуемые фильтры по требованию

Метод называется scope + ИмяScope (camelCase), принимает Builder и дополнительные параметры. Вызывается без префикса scope в цепочке запроса.

class Post extends Model
{
    // Простой scope
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('status', 'published');
    }

    // Scope с параметром
    public function scopeOfType(Builder $query, string $type): Builder
    {
        return $query->where('type', $type);
    }

    // Scope с несколькими условиями
    public function scopeRecent(Builder $query, int $days = 7): Builder
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
}

// Использование
$posts = Post::published()->get();
$posts = Post::published()->ofType('article')->recent(14)->get();
$posts = Post::ofType('video')->orderBy('views', 'desc')->get();

Global Scopes — автоматические фильтры для всех запросов

Применяются к каждому запросу модели автоматически. Реализуются через интерфейс Scope или анонимную функцию. Soft Deletes — встроенный пример глобального scope.

// Отдельный класс scope
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('active', true);
    }
}

// Регистрация в модели
class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new ActiveScope());

        // Или анонимно с именем
        static::addGlobalScope('verified', function (Builder $builder) {
            $builder->whereNotNull('email_verified_at');
        });
    }
}

// Теперь User::all() автоматически добавляет WHERE active = 1
$users = User::all(); // SELECT * FROM users WHERE active = 1 AND email_verified_at IS NOT NULL

// Отключение глобального scope
$allUsers = User::withoutGlobalScope(ActiveScope::class)->get();
$allUsers = User::withoutGlobalScope('verified')->get();
$allUsers = User::withoutGlobalScopes()->get(); // отключить все

Soft Deletes как встроенный global scope

use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes; // добавляет GlobalScope WHERE deleted_at IS NULL
}

Post::all();                     // только не удалённые
Post::withTrashed()->get();      // все, включая удалённые
Post::onlyTrashed()->get();      // только удалённые
$post->restore();                // восстановить
$post->forceDelete();            // удалить физически

Подводные камни

  • Global scope ломает JOIN-запросы. Если глобальный scope добавляет WHERE на столбец без алиаса таблицы, при JOIN с другой таблицей, имеющей такой же столбец, возникнет ambiguous column error. Всегда квалифицируйте: $builder->where('users.active', true).
  • withoutGlobalScope нужно вызывать явно. Если где-то нужны все записи — не забудьте отключить scope, иначе получите неожиданно отфильтрованные данные.
  • Local scope не возвращает Builder — тихая ошибка. Если забыть return $query->..., метод вернёт null, цепочка сломается с неинформативным сообщением.
  • Имя local scope не может совпадать с методом Eloquent. Например, scopeWith вызовет конфликт с методом with() eager loading.
  • Global scope и migrations. Если вы запускаете seed/migration и в модели зарегистрирован scope, он применится и там — это может привести к неожиданным результатам при начальном наполнении данными.
  • Порядок применения global scopes. Scopes применяются в порядке регистрации. Если один scope изменяет JOIN, а другой добавляет условие на ту же таблицу, порядок может иметь значение.
  • Анонимные scopes требуют строкового ключа для отключения. withoutGlobalScope('verified') работает только если при регистрации передано то же имя 'verified'. Без имени отключить анонимный scope нельзя точечно.

Common mistakes

  • Сводить eloquent scopes к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Laravel 13 поверх PHP: request проходит через HTTP kernel, middleware, routing, controller/action и response pipeline.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет eloquent scopes через конкретную точку lifecycle в Laravel.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics

Что такое scopes в Laravel (local vs global) в Eloquent? | Talanto