-
-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Add #[Scope] and #[ScopedBy] attributes for declarative scope registration
#327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Hypervel\Database\Eloquent\Attributes; | ||
|
|
||
| use Attribute; | ||
|
|
||
| /** | ||
| * Mark a method as a local query scope. | ||
| * | ||
| * Methods with this attribute can be called as scopes without | ||
| * the traditional 'scope' prefix convention: | ||
| * | ||
| * #[Scope] | ||
| * protected function active(Builder $query): void | ||
| * { | ||
| * $query->where('active', true); | ||
| * } | ||
| * | ||
| * // Called as: User::active() or $query->active() | ||
| */ | ||
| #[Attribute(Attribute::TARGET_METHOD)] | ||
| class Scope | ||
| { | ||
| public function __construct() | ||
| { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Hypervel\Database\Eloquent\Attributes; | ||
|
|
||
| use Attribute; | ||
|
|
||
| /** | ||
| * Declare global scopes to be automatically applied to the model. | ||
| * | ||
| * Can be applied to model classes or traits. Supports both single scope | ||
| * class and arrays of scope classes. Repeatable for multiple declarations. | ||
| */ | ||
| #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] | ||
| class ScopedBy | ||
| { | ||
| /** | ||
| * @param class-string|class-string[] $classes | ||
| */ | ||
| public function __construct( | ||
| public array|string $classes, | ||
| ) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
src/core/src/Database/Eloquent/Concerns/HasGlobalScopes.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Hypervel\Database\Eloquent\Concerns; | ||
|
|
||
| use Closure; | ||
| use Hyperf\Collection\Collection; | ||
| use Hyperf\Database\Model\GlobalScope; | ||
| use Hyperf\Database\Model\Model as HyperfModel; | ||
| use Hyperf\Database\Model\Scope; | ||
| use Hypervel\Database\Eloquent\Attributes\ScopedBy; | ||
| use InvalidArgumentException; | ||
| use ReflectionAttribute; | ||
| use ReflectionClass; | ||
|
|
||
| /** | ||
| * Extends Hyperf's global scope functionality with attribute-based registration. | ||
| * | ||
| * This trait adds support for the #[ScopedBy] attribute, allowing models | ||
| * to declare their global scopes declaratively on the class or traits. | ||
| */ | ||
| trait HasGlobalScopes | ||
| { | ||
| /** | ||
| * Boot the has global scopes trait for a model. | ||
| * | ||
| * Automatically registers any global scopes declared via the ScopedBy attribute. | ||
| */ | ||
| public static function bootHasGlobalScopes(): void | ||
| { | ||
| $scopes = static::resolveGlobalScopeAttributes(); | ||
|
|
||
| if (! empty($scopes)) { | ||
| static::addGlobalScopes($scopes); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the global scope class names from the ScopedBy attributes. | ||
| * | ||
| * Collects ScopedBy attributes from parent classes, traits, and the | ||
| * current class itself, merging them together. The order is: | ||
| * parent class scopes -> trait scopes -> class scopes. | ||
| * | ||
| * @return array<int, class-string<Scope>> | ||
| */ | ||
| public static function resolveGlobalScopeAttributes(): array | ||
| { | ||
| $reflectionClass = new ReflectionClass(static::class); | ||
|
|
||
| $parentClass = get_parent_class(static::class); | ||
| $hasParentWithMethod = $parentClass | ||
| && $parentClass !== HyperfModel::class | ||
| && method_exists($parentClass, 'resolveGlobalScopeAttributes'); | ||
|
|
||
| // Collect attributes from traits, then from the class itself | ||
| $attributes = new Collection(); | ||
|
|
||
| foreach ($reflectionClass->getTraits() as $trait) { | ||
| foreach ($trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { | ||
| $attributes->push($attribute); | ||
| } | ||
| } | ||
|
|
||
| foreach ($reflectionClass->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { | ||
| $attributes->push($attribute); | ||
| } | ||
|
|
||
| // Process all collected attributes | ||
| $scopes = $attributes | ||
| ->map(fn (ReflectionAttribute $attribute) => $attribute->getArguments()) | ||
| ->flatten(); | ||
|
|
||
| // Prepend parent's scopes if applicable | ||
| return $scopes | ||
| ->when($hasParentWithMethod, function (Collection $attrs) use ($parentClass) { | ||
| /** @var class-string $parentClass */ | ||
| return (new Collection($parentClass::resolveGlobalScopeAttributes())) | ||
| ->merge($attrs); | ||
| }) | ||
| ->all(); | ||
| } | ||
|
|
||
| /** | ||
| * Register multiple global scopes on the model. | ||
| * | ||
| * @param array<int|string, class-string<Scope>|Closure|Scope> $scopes | ||
| */ | ||
| public static function addGlobalScopes(array $scopes): void | ||
| { | ||
| foreach ($scopes as $key => $scope) { | ||
| if (is_string($key)) { | ||
| static::addGlobalScope($key, $scope); | ||
| } else { | ||
| static::addGlobalScope($scope); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register a new global scope on the model. | ||
| * | ||
| * Extends Hyperf's implementation to support scope class-strings. | ||
| * | ||
| * @param Closure|Scope|string $scope | ||
| * @return mixed | ||
| * | ||
| * @throws InvalidArgumentException | ||
| */ | ||
| public static function addGlobalScope($scope, ?Closure $implementation = null) | ||
| { | ||
| if (is_string($scope) && $implementation !== null) { | ||
| return GlobalScope::$container[static::class][$scope] = $implementation; | ||
| } | ||
|
|
||
| if ($scope instanceof Closure) { | ||
| return GlobalScope::$container[static::class][spl_object_hash($scope)] = $scope; | ||
| } | ||
|
|
||
| if ($scope instanceof Scope) { | ||
| return GlobalScope::$container[static::class][get_class($scope)] = $scope; | ||
| } | ||
|
|
||
| // Support class-string for Scope classes (Laravel compatibility) | ||
| if (class_exists($scope) && is_subclass_of($scope, Scope::class)) { | ||
| return GlobalScope::$container[static::class][$scope] = new $scope(); | ||
| } | ||
|
|
||
| throw new InvalidArgumentException( | ||
| 'Global scope must be an instance of Closure or Scope, or a class-string of a Scope implementation.' | ||
| ); | ||
| } | ||
| } |
56 changes: 56 additions & 0 deletions
56
src/core/src/Database/Eloquent/Concerns/HasLocalScopes.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Hypervel\Database\Eloquent\Concerns; | ||
|
|
||
| use Hypervel\Database\Eloquent\Attributes\Scope; | ||
| use ReflectionMethod; | ||
|
|
||
| /** | ||
| * Adds support for the #[Scope] attribute on model methods. | ||
| * | ||
| * This trait allows methods to be marked as local scopes without | ||
| * requiring the traditional 'scope' prefix naming convention. | ||
| */ | ||
| trait HasLocalScopes | ||
| { | ||
| /** | ||
| * Determine if the model has a named scope. | ||
| * | ||
| * Checks for both traditional scope prefix (scopeActive) and | ||
| * methods marked with the #[Scope] attribute. | ||
| */ | ||
| public function hasNamedScope(string $scope): bool | ||
| { | ||
| return method_exists($this, 'scope' . ucfirst($scope)) | ||
| || static::isScopeMethodWithAttribute($scope); | ||
| } | ||
|
|
||
| /** | ||
| * Apply the given named scope if possible. | ||
| * | ||
| * @param array<int, mixed> $parameters | ||
| */ | ||
| public function callNamedScope(string $scope, array $parameters = []): mixed | ||
| { | ||
| if (static::isScopeMethodWithAttribute($scope)) { | ||
| return $this->{$scope}(...$parameters); | ||
| } | ||
|
|
||
| return $this->{'scope' . ucfirst($scope)}(...$parameters); | ||
| } | ||
|
|
||
| /** | ||
| * Determine if the given method has a #[Scope] attribute. | ||
| */ | ||
| protected static function isScopeMethodWithAttribute(string $method): bool | ||
| { | ||
| if (! method_exists(static::class, $method)) { | ||
| return false; | ||
| } | ||
|
|
||
| return (new ReflectionMethod(static::class, $method)) | ||
| ->getAttributes(Scope::class) !== []; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.