Skip to content

Conversation

@binaryfire
Copy link
Contributor

Ports Laravel's #[Scope] and #[ScopedBy] attributes to Hypervel, enabling declarative scope registration on models.

Summary

This PR ports two PHP attributes from Laravel that allow models to declare their scopes declaratively rather than imperatively:

  • #[ScopedBy] - Class-level attribute for global scopes (auto-applied to all queries)
  • #[Scope] - Method-level attribute for local scopes (without scope prefix)

Features

ScopedBy Attribute (Global Scopes)

Declare global scopes that are automatically registered when the model boots:

use Hypervel\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy(ActiveScope::class)]
class User extends Model
{
    // ActiveScope is automatically applied to all queries
}

// Multiple scopes via array
#[ScopedBy([TenantScope::class, AuditScope::class])]
class Invoice extends Model { }

// Or multiple attributes (repeatable)
#[ScopedBy(TenantScope::class)]
#[ScopedBy(SoftDeleteScope::class)]
class Order extends Model { }

Trait support - Scopes declared on traits are inherited by models using them:

#[ScopedBy(TenantScope::class)]
trait BelongsToTenant
{
    public function tenant(): BelongsTo { ... }
}

class Invoice extends Model
{
    use BelongsToTenant; // Automatically gets TenantScope
}

Inheritance - Child models inherit scopes from parent classes. Resolution order: parent class -> traits -> class

Scope Attribute (Local Scopes)

Mark methods as local scopes without the traditional scope prefix:

use Hypervel\Database\Eloquent\Attributes\Scope;

class User extends Model
{
    #[Scope]
    protected function verified(Builder $query): void
    {
        $query->where('email_verified_at', '!=', null);
    }

    #[Scope]
    protected function ofType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

// Usage - works on both static calls and query builder
User::verified()->get();
User::query()->verified()->ofType('admin')->get();

This provides a cleaner alternative to the scopeVerified() naming convention.

Changes

New Files

File Description
Attributes/ScopedBy.php Class-level attribute for global scopes
Attributes/Scope.php Method-level attribute for local scopes
Concerns/HasGlobalScopes.php Trait with boot method and attribute resolution
Concerns/HasLocalScopes.php Trait with hasNamedScope() / callNamedScope()

Modified Files

File Changes
Model.php Added traits, override __callStatic() for scope detection
Builder.php Override __call() to use hasNamedScope()
Relations/Pivot.php Added HasGlobalScopes trait
Relations/MorphPivot.php Added HasGlobalScopes trait

Implementation Details

HasGlobalScopes Trait

  • bootHasGlobalScopes() - Called during model boot, registers attribute-declared scopes
  • resolveGlobalScopeAttributes() - Collects #[ScopedBy] from class, traits, and parents
  • addGlobalScopes(array $scopes) - Batch registration helper
  • addGlobalScope() - Extended to support class-string (Laravel compatibility)

HasLocalScopes Trait

  • hasNamedScope(string $scope) - Checks for scope prefix OR #[Scope] attribute
  • callNamedScope(string $scope, array $params) - Invokes the scope method
  • isScopeMethodWithAttribute(string $method) - Reflection check for attribute

Builder Changes

The __call() method now uses hasNamedScope() instead of directly checking for scope prefix, enabling both traditional and attribute-based scopes to work seamlessly.

Testing

  • 19 tests for HasGlobalScopes (attribute resolution, inheritance, traits, Pivot models)
  • 14 tests for HasLocalScopes (scope detection, invocation, parameters, inheritance)

All tests passing with PHPStan and PHP-CS-Fixer checks clean.

Laravel Parity

This implementation follows Laravel's approach with some improvements:

Aspect Laravel Hypervel
ScopedBy inheritance Traits only Parent classes + traits
Scope storage static::$globalScopes GlobalScope::$container (Hyperf)
Class-string support Yes Yes (added to Hyperf's implementation)

Ports Laravel's Scope and ScopedBy attributes to Hypervel, enabling
declarative scope registration on models.

## ScopedBy (class-level attribute)
Declares global scopes that are automatically registered when the model boots:

    #[ScopedBy(ActiveScope::class)]
    #[ScopedBy([TenantScope::class, AuditScope::class])]
    class User extends Model { }

Supports inheritance from parent classes and traits, following the same
pattern as ObservedBy. Order: parent -> traits -> class.

## Scope (method-level attribute)
Marks methods as local scopes without requiring the 'scope' prefix:

    #[Scope]
    protected function verified(Builder $query): void
    {
        $query->where('verified', true);
    }

    // Called as: User::verified() or $query->verified()

## Changes
- Add ScopedBy and Scope attribute classes
- Add HasGlobalScopes trait with boot method and attribute resolution
- Add HasLocalScopes trait with hasNamedScope/callNamedScope methods
- Update Model to use new traits and override __callStatic
- Update Builder __call to check for Scope attribute
- Add HasGlobalScopes to Pivot and MorphPivot classes
@binaryfire binaryfire changed the title feat: Add Scope and ScopedBy attributes for declarative scope registration feat: Add #[Scope] and #[ScopedBy] attributes for declarative scope registration Dec 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant