mpyw/laravel-local-class-scope
composer require mpyw/laravel-local-class-scope in your Laravel 11/12 project.scoped() macro for Eloquent query builders—no manual configuration needed.ActiveScope) implementing Illuminate\Database\Eloquent\Scope with an apply() method.
Use it locally:
User::scoped(ActiveScope::class)->get();
Scope and Builder for context.ScopesMacroServiceProvider to understand macro registration.Reusable Global Scopes as Local Scopes:
Convert a global scope (registered via protected $with = [...]) into a local scope dynamically:
// Global scope (e.g., in User model)
protected static $with = [ActiveScope::class];
// Local usage
User::scoped(ActiveScope::class)->get(); // Applies only to this query
Parameterized Scopes: Pass arguments to scopes via constructor:
class AgeScope implements Scope {
public function __construct(int $minAge, int $maxAge) {}
public function apply(Builder $query, Model $model) {
$query->whereBetween('age', [$minAge, $maxAge]);
}
}
// Usage
User::scoped(new AgeScope(18, 30))->get();
Chaining Scopes: Combine multiple scopes in a single query:
User::scoped(ActiveScope::class)
->scoped(new AgeScope(18, 30))
->get();
Dynamic Scope Resolution: Use dependency injection for scopes (e.g., via Laravel’s container):
class TenantScope implements Scope {
public function __construct(private TenantResolver $resolver) {}
public function apply(Builder $query, Model $model) {
$query->where('tenant_id', $this->resolver->getId());
}
}
// Usage
User::scoped(new TenantScope(app(TenantResolver::class)))->get();
$scope = $this->createMock(Scope::class);
$scope->method('apply')->willReturnCallback(function ($query) {
$query->where('test', true);
});
User::scoped($scope)->get(); // Test with mocked scope
@mixin in PHPStorm for better autocompletion with dynamic scopes:
/** @mixin \Illuminate\Database\Eloquent\Builder */
class User extends Model { ... }
Constructor Injection:
Scopes passed as strings (e.g., User::scoped('App\Scopes\ActiveScope')) cannot receive constructor arguments. Always use new for parameterized scopes:
// ❌ Fails (no args)
User::scoped('App\Scopes\AgeScope');
// ✅ Works
User::scoped(new AgeScope(18, 30));
Model Binding:
The Model parameter in apply() may be null if the scope is applied to a query builder without a model (e.g., DB::table('users')->scoped(...)). Handle this gracefully:
public function apply(Builder $query, ?Model $model): void {
if ($model) {
$query->where('user_id', $model->id);
}
}
Macro Overrides:
The scoped() macro replaces the default behavior. If you later need the original scope() method (e.g., for global scopes), you’ll need to restore it or use a different method name.
PHP 8.2+ Requirement:
Older Laravel versions (pre-11) or PHP <8.2 will fail. Use ^11.0 || ^12.0 as specified.
DB::enableQueryLog();
User::scoped(ActiveScope::class)->get();
dd(DB::getQueryLog());
tap() to debug:
User::scoped(ActiveScope::class)
->tap(fn($query) => dd($query->toSql()))
->get();
Naming Conventions:
Append Scope to class names (e.g., ActiveScope) for clarity and consistency with Laravel’s global scopes.
Extending Scopes: Create base scope classes to share logic:
abstract class BaseSoftDeletesScope implements Scope {
public function apply(Builder $query, Model $model): void {
$query->whereNull('deleted_at');
}
}
Local vs. Global: Use local scopes for query-specific filters (e.g., admin dashboard queries) and global scopes for model-wide defaults (e.g., soft deletes).
Type Safety: Use PHP 8.1+ attributes to enforce scope contracts:
#[Attribute(Attribute::TARGET_CLASS)]
class ScopeAttribute {}
// Then validate scopes in tests or via static analysis.
Performance: For complex scopes, consider compiling them into raw SQL or using query hints to reduce overhead:
public function apply(Builder $query, Model $model): void {
$query->whereRaw('...')->hint('SKIP_SCOPE_ANALYSIS');
}
How can I help you explore Laravel packages today?