diff --git a/src/core/src/Database/Eloquent/Attributes/Scope.php b/src/core/src/Database/Eloquent/Attributes/Scope.php new file mode 100644 index 00000000..b1eded8e --- /dev/null +++ b/src/core/src/Database/Eloquent/Attributes/Scope.php @@ -0,0 +1,29 @@ +where('active', true); + * } + * + * // Called as: User::active() or $query->active() + */ +#[Attribute(Attribute::TARGET_METHOD)] +class Scope +{ + public function __construct() + { + } +} diff --git a/src/core/src/Database/Eloquent/Attributes/ScopedBy.php b/src/core/src/Database/Eloquent/Attributes/ScopedBy.php new file mode 100644 index 00000000..8413fbc2 --- /dev/null +++ b/src/core/src/Database/Eloquent/Attributes/ScopedBy.php @@ -0,0 +1,25 @@ + $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if ($method === 'macro') { + $this->localMacros[$parameters[0]] = $parameters[1]; + + return; + } + + if ($method === 'mixin') { + return static::registerMixin($parameters[0], $parameters[1] ?? true); + } + + if ($this->hasMacro($method)) { + array_unshift($parameters, $this); + + return $this->localMacros[$method](...$parameters); + } + + if (static::hasGlobalMacro($method)) { + $macro = static::$macros[$method]; + + if ($macro instanceof Closure) { + return call_user_func_array($macro->bindTo($this, static::class), $parameters); + } + + return call_user_func_array($macro, $parameters); + } + + // Check for named scopes (both 'scope' prefix and #[Scope] attribute) + if ($this->hasNamedScope($method)) { + return $this->callNamedScope($method, $parameters); + } + + if (in_array($method, $this->passthru)) { + return $this->toBase()->{$method}(...$parameters); + } + + $this->query->{$method}(...$parameters); + + return $this; + } + + /** + * Determine if the given model has a named scope. + */ + public function hasNamedScope(string $scope): bool + { + return $this->model && $this->model->hasNamedScope($scope); + } + + /** + * Call the given named scope on the model. + * + * @param array $parameters + */ + protected function callNamedScope(string $scope, array $parameters = []): mixed + { + return $this->callScope(function (...$params) use ($scope) { + return $this->model->callNamedScope($scope, $params); + }, $parameters); + } + /** * @return \Hypervel\Support\LazyCollection */ diff --git a/src/core/src/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/core/src/Database/Eloquent/Concerns/HasGlobalScopes.php new file mode 100644 index 00000000..59125bda --- /dev/null +++ b/src/core/src/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -0,0 +1,134 @@ + trait scopes -> class scopes. + * + * @return array> + */ + 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|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.' + ); + } +} diff --git a/src/core/src/Database/Eloquent/Concerns/HasLocalScopes.php b/src/core/src/Database/Eloquent/Concerns/HasLocalScopes.php new file mode 100644 index 00000000..dc1570ca --- /dev/null +++ b/src/core/src/Database/Eloquent/Concerns/HasLocalScopes.php @@ -0,0 +1,56 @@ + $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) !== []; + } +} diff --git a/src/core/src/Database/Eloquent/Model.php b/src/core/src/Database/Eloquent/Model.php index de09102d..2fa4b38d 100644 --- a/src/core/src/Database/Eloquent/Model.php +++ b/src/core/src/Database/Eloquent/Model.php @@ -10,6 +10,8 @@ use Hypervel\Context\Context; use Hypervel\Database\Eloquent\Concerns\HasAttributes; use Hypervel\Database\Eloquent\Concerns\HasCallbacks; +use Hypervel\Database\Eloquent\Concerns\HasGlobalScopes; +use Hypervel\Database\Eloquent\Concerns\HasLocalScopes; use Hypervel\Database\Eloquent\Concerns\HasObservers; use Hypervel\Database\Eloquent\Concerns\HasRelations; use Hypervel\Database\Eloquent\Concerns\HasRelationships; @@ -68,9 +70,11 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChann { use HasAttributes; use HasCallbacks; + use HasGlobalScopes; + use HasLocalScopes; + use HasObservers; use HasRelations; use HasRelationships; - use HasObservers; protected ?string $connection = null; @@ -231,6 +235,25 @@ public function replicateQuietly(?array $except = null): static return static::withoutEvents(fn () => $this->replicate($except)); } + /** + * Handle dynamic static method calls into the model. + * + * Checks for methods marked with the #[Scope] attribute before + * falling back to the default behavior. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public static function __callStatic($method, $parameters) + { + if (static::isScopeMethodWithAttribute($method)) { + return static::query()->{$method}(...$parameters); + } + + return (new static())->{$method}(...$parameters); + } + protected static function getWithoutEventContextKey(): string { return '__database.model.without_events.' . static::class; diff --git a/src/core/src/Database/Eloquent/Relations/MorphPivot.php b/src/core/src/Database/Eloquent/Relations/MorphPivot.php index c806817d..ac4a89b1 100644 --- a/src/core/src/Database/Eloquent/Relations/MorphPivot.php +++ b/src/core/src/Database/Eloquent/Relations/MorphPivot.php @@ -7,12 +7,15 @@ use Hyperf\DbConnection\Model\Relations\MorphPivot as BaseMorphPivot; use Hypervel\Database\Eloquent\Concerns\HasAttributes; use Hypervel\Database\Eloquent\Concerns\HasCallbacks; +use Hypervel\Database\Eloquent\Concerns\HasGlobalScopes; use Hypervel\Database\Eloquent\Concerns\HasObservers; +use Psr\EventDispatcher\StoppableEventInterface; class MorphPivot extends BaseMorphPivot { use HasAttributes; use HasCallbacks; + use HasGlobalScopes; use HasObservers; /** @@ -29,8 +32,10 @@ public function delete(): mixed } // For composite key pivots, manually fire events around the raw delete - if ($this->fireModelEvent('deleting') === false) { - return 0; + if ($event = $this->fireModelEvent('deleting')) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + return 0; + } } $query = $this->getDeleteQuery(); diff --git a/src/core/src/Database/Eloquent/Relations/Pivot.php b/src/core/src/Database/Eloquent/Relations/Pivot.php index 3112b64e..47d9468a 100644 --- a/src/core/src/Database/Eloquent/Relations/Pivot.php +++ b/src/core/src/Database/Eloquent/Relations/Pivot.php @@ -7,12 +7,15 @@ use Hyperf\DbConnection\Model\Relations\Pivot as BasePivot; use Hypervel\Database\Eloquent\Concerns\HasAttributes; use Hypervel\Database\Eloquent\Concerns\HasCallbacks; +use Hypervel\Database\Eloquent\Concerns\HasGlobalScopes; use Hypervel\Database\Eloquent\Concerns\HasObservers; +use Psr\EventDispatcher\StoppableEventInterface; class Pivot extends BasePivot { use HasAttributes; use HasCallbacks; + use HasGlobalScopes; use HasObservers; /** @@ -28,8 +31,10 @@ public function delete(): mixed } // For composite key pivots, manually fire events around the raw delete - if ($this->fireModelEvent('deleting') === false) { - return 0; + if ($event = $this->fireModelEvent('deleting')) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + return 0; + } } $result = $this->getDeleteQuery()->delete(); diff --git a/tests/Core/Database/Eloquent/Concerns/HasGlobalScopesTest.php b/tests/Core/Database/Eloquent/Concerns/HasGlobalScopesTest.php new file mode 100644 index 00000000..2931c181 --- /dev/null +++ b/tests/Core/Database/Eloquent/Concerns/HasGlobalScopesTest.php @@ -0,0 +1,411 @@ +assertSame([], $result); + } + + public function testResolveGlobalScopeAttributesReturnsSingleScope(): void + { + $result = ModelWithSingleScope::resolveGlobalScopeAttributes(); + + $this->assertSame([ActiveScope::class], $result); + } + + public function testResolveGlobalScopeAttributesReturnsMultipleScopesFromArray(): void + { + $result = ModelWithMultipleScopesInArray::resolveGlobalScopeAttributes(); + + $this->assertSame([ActiveScope::class, TenantScope::class], $result); + } + + public function testResolveGlobalScopeAttributesReturnsMultipleScopesFromRepeatableAttribute(): void + { + $result = ModelWithRepeatableScopedBy::resolveGlobalScopeAttributes(); + + $this->assertSame([ActiveScope::class, TenantScope::class], $result); + } + + public function testResolveGlobalScopeAttributesInheritsFromParentClass(): void + { + $result = ChildModelWithOwnScope::resolveGlobalScopeAttributes(); + + // Parent's scope comes first, then child's + $this->assertSame([ParentScope::class, ChildScope::class], $result); + } + + public function testResolveGlobalScopeAttributesInheritsFromParentWhenChildHasNoAttributes(): void + { + $result = ChildModelWithoutOwnScope::resolveGlobalScopeAttributes(); + + $this->assertSame([ParentScope::class], $result); + } + + public function testResolveGlobalScopeAttributesInheritsFromGrandparent(): void + { + $result = GrandchildModelWithScope::resolveGlobalScopeAttributes(); + + // Should have grandparent's, parent's, and own scope + $this->assertSame([ParentScope::class, MiddleScope::class, GrandchildScope::class], $result); + } + + public function testResolveGlobalScopeAttributesDoesNotInheritFromModelBaseClass(): void + { + // Models that directly extend Model should not try to resolve + // parent attributes since Model itself has no ScopedBy attribute + $result = ModelWithSingleScope::resolveGlobalScopeAttributes(); + + $this->assertSame([ActiveScope::class], $result); + } + + public function testResolveGlobalScopeAttributesCollectsFromTrait(): void + { + $result = ModelUsingTraitWithScope::resolveGlobalScopeAttributes(); + + $this->assertSame([TraitScope::class], $result); + } + + public function testResolveGlobalScopeAttributesCollectsMultipleScopesFromTrait(): void + { + $result = ModelUsingTraitWithMultipleScopes::resolveGlobalScopeAttributes(); + + $this->assertSame([TraitFirstScope::class, TraitSecondScope::class], $result); + } + + public function testResolveGlobalScopeAttributesCollectsFromMultipleTraits(): void + { + $result = ModelUsingMultipleTraitsWithScopes::resolveGlobalScopeAttributes(); + + // Both traits' scopes should be collected + $this->assertSame([TraitScope::class, AnotherTraitScope::class], $result); + } + + public function testResolveGlobalScopeAttributesMergesTraitAndClassScopes(): void + { + $result = ModelWithTraitAndOwnScope::resolveGlobalScopeAttributes(); + + // Trait scopes come first, then class scopes + $this->assertSame([TraitScope::class, ActiveScope::class], $result); + } + + public function testResolveGlobalScopeAttributesMergesParentTraitAndChildScopes(): void + { + $result = ChildModelWithTraitParent::resolveGlobalScopeAttributes(); + + // Parent's trait scope -> child's class scope + $this->assertSame([TraitScope::class, ChildScope::class], $result); + } + + public function testResolveGlobalScopeAttributesCorrectOrderWithParentTraitsAndChild(): void + { + $result = ChildModelWithAllScopeSources::resolveGlobalScopeAttributes(); + + // Order: parent class -> parent trait -> child trait -> child class + // ParentModelWithScope has ParentScope + // ChildModelWithAllScopeSources uses TraitWithScope (TraitScope) and has ChildScope + $this->assertSame([ParentScope::class, TraitScope::class, ChildScope::class], $result); + } + + public function testAddGlobalScopesRegistersMultipleScopes(): void + { + ModelWithoutScopedBy::addGlobalScopes([ + ActiveScope::class, + TenantScope::class, + ]); + + $this->assertTrue(ModelWithoutScopedBy::hasGlobalScope(ActiveScope::class)); + $this->assertTrue(ModelWithoutScopedBy::hasGlobalScope(TenantScope::class)); + } + + public function testAddGlobalScopeSupportsClassString(): void + { + ModelWithoutScopedBy::addGlobalScope(ActiveScope::class); + + $this->assertTrue(ModelWithoutScopedBy::hasGlobalScope(ActiveScope::class)); + $this->assertInstanceOf(ActiveScope::class, ModelWithoutScopedBy::getGlobalScope(ActiveScope::class)); + } + + public function testPivotModelSupportsScopedByAttribute(): void + { + $result = PivotWithScope::resolveGlobalScopeAttributes(); + + $this->assertSame([PivotScope::class], $result); + } + + public function testPivotModelInheritsScopesFromParent(): void + { + $result = ChildPivotWithScope::resolveGlobalScopeAttributes(); + + // Parent's scope comes first, then child's + $this->assertSame([PivotScope::class, ChildPivotScope::class], $result); + } + + public function testMorphPivotModelSupportsScopedByAttribute(): void + { + $result = MorphPivotWithScope::resolveGlobalScopeAttributes(); + + $this->assertSame([MorphPivotScope::class], $result); + } +} + +// Test scope classes +class ActiveScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + $builder->where('active', true); + } +} + +class TenantScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + $builder->where('tenant_id', 1); + } +} + +class ParentScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class ChildScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class MiddleScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class GrandchildScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class TraitScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class TraitFirstScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class TraitSecondScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class AnotherTraitScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class PivotScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class ChildPivotScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +class MorphPivotScope implements Scope +{ + public function apply(Builder $builder, HyperfModel $model): void + { + } +} + +// Test model classes +class ModelWithoutScopedBy extends Model +{ + use HasGlobalScopes; + + protected ?string $table = 'test_models'; +} + +#[ScopedBy(ActiveScope::class)] +class ModelWithSingleScope extends Model +{ + protected ?string $table = 'test_models'; +} + +#[ScopedBy([ActiveScope::class, TenantScope::class])] +class ModelWithMultipleScopesInArray extends Model +{ + protected ?string $table = 'test_models'; +} + +#[ScopedBy(ActiveScope::class)] +#[ScopedBy(TenantScope::class)] +class ModelWithRepeatableScopedBy extends Model +{ + protected ?string $table = 'test_models'; +} + +// Inheritance test models +#[ScopedBy(ParentScope::class)] +class ParentModelWithScope extends Model +{ + protected ?string $table = 'test_models'; +} + +#[ScopedBy(ChildScope::class)] +class ChildModelWithOwnScope extends ParentModelWithScope +{ +} + +class ChildModelWithoutOwnScope extends ParentModelWithScope +{ +} + +#[ScopedBy(MiddleScope::class)] +class MiddleModelWithScope extends ParentModelWithScope +{ +} + +#[ScopedBy(GrandchildScope::class)] +class GrandchildModelWithScope extends MiddleModelWithScope +{ +} + +// Traits with ScopedBy attributes +#[ScopedBy(TraitScope::class)] +trait TraitWithScope +{ +} + +#[ScopedBy([TraitFirstScope::class, TraitSecondScope::class])] +trait TraitWithMultipleScopes +{ +} + +#[ScopedBy(AnotherTraitScope::class)] +trait AnotherTraitWithScope +{ +} + +// Models using traits with scopes +class ModelUsingTraitWithScope extends Model +{ + use TraitWithScope; + + protected ?string $table = 'test_models'; +} + +class ModelUsingTraitWithMultipleScopes extends Model +{ + use TraitWithMultipleScopes; + + protected ?string $table = 'test_models'; +} + +class ModelUsingMultipleTraitsWithScopes extends Model +{ + use TraitWithScope; + use AnotherTraitWithScope; + + protected ?string $table = 'test_models'; +} + +#[ScopedBy(ActiveScope::class)] +class ModelWithTraitAndOwnScope extends Model +{ + use TraitWithScope; + + protected ?string $table = 'test_models'; +} + +// Parent model that uses a trait with scope +class ParentModelUsingTrait extends Model +{ + use TraitWithScope; + + protected ?string $table = 'test_models'; +} + +#[ScopedBy(ChildScope::class)] +class ChildModelWithTraitParent extends ParentModelUsingTrait +{ +} + +// Child model with parent class scope, own trait, and own scope +#[ScopedBy(ChildScope::class)] +class ChildModelWithAllScopeSources extends ParentModelWithScope +{ + use TraitWithScope; +} + +// Pivot test models +#[ScopedBy(PivotScope::class)] +class PivotWithScope extends Pivot +{ + protected ?string $table = 'test_pivots'; +} + +#[ScopedBy(ChildPivotScope::class)] +class ChildPivotWithScope extends PivotWithScope +{ +} + +#[ScopedBy(MorphPivotScope::class)] +class MorphPivotWithScope extends MorphPivot +{ + protected ?string $table = 'test_morph_pivots'; +} diff --git a/tests/Core/Database/Eloquent/Concerns/HasLocalScopesTest.php b/tests/Core/Database/Eloquent/Concerns/HasLocalScopesTest.php new file mode 100644 index 00000000..c80aa54b --- /dev/null +++ b/tests/Core/Database/Eloquent/Concerns/HasLocalScopesTest.php @@ -0,0 +1,233 @@ +assertTrue($model->hasNamedScope('active')); + } + + public function testHasNamedScopeReturnsTrueForScopeAttribute(): void + { + $model = new ModelWithScopeAttribute(); + + $this->assertTrue($model->hasNamedScope('verified')); + } + + public function testHasNamedScopeReturnsFalseForNonExistentScope(): void + { + $model = new ModelWithTraditionalScope(); + + $this->assertFalse($model->hasNamedScope('nonExistent')); + } + + public function testHasNamedScopeReturnsFalseForRegularMethodWithoutAttribute(): void + { + $model = new ModelWithRegularMethod(); + + $this->assertFalse($model->hasNamedScope('regularMethod')); + } + + public function testCallNamedScopeCallsTraditionalScopeMethod(): void + { + $model = new ModelWithTraditionalScope(); + $builder = $this->createMock(Builder::class); + + $result = $model->callNamedScope('active', [$builder]); + + $this->assertSame($builder, $result); + } + + public function testCallNamedScopeCallsScopeAttributeMethod(): void + { + $model = new ModelWithScopeAttribute(); + $builder = $this->createMock(Builder::class); + + $result = $model->callNamedScope('verified', [$builder]); + + $this->assertSame($builder, $result); + } + + public function testCallNamedScopePassesParameters(): void + { + $model = new ModelWithParameterizedScope(); + $builder = $this->createMock(Builder::class); + + $result = $model->callNamedScope('ofType', [$builder, 'premium']); + + $this->assertSame('premium', $result); + } + + public function testIsScopeMethodWithAttributeReturnsTrueForAttributedMethod(): void + { + $result = ModelWithScopeAttribute::isScopeMethodWithAttributePublic('verified'); + + $this->assertTrue($result); + } + + public function testIsScopeMethodWithAttributeReturnsFalseForTraditionalScope(): void + { + $result = ModelWithTraditionalScope::isScopeMethodWithAttributePublic('scopeActive'); + + $this->assertFalse($result); + } + + public function testIsScopeMethodWithAttributeReturnsFalseForNonExistentMethod(): void + { + $result = ModelWithScopeAttribute::isScopeMethodWithAttributePublic('nonExistent'); + + $this->assertFalse($result); + } + + public function testIsScopeMethodWithAttributeReturnsFalseForMethodWithoutAttribute(): void + { + $result = ModelWithRegularMethod::isScopeMethodWithAttributePublic('regularMethod'); + + $this->assertFalse($result); + } + + public function testModelHasBothTraditionalAndAttributeScopes(): void + { + $model = new ModelWithBothScopeTypes(); + + $this->assertTrue($model->hasNamedScope('active')); + $this->assertTrue($model->hasNamedScope('verified')); + } + + public function testInheritedScopeAttributeIsRecognized(): void + { + $model = new ChildModelWithInheritedScope(); + + $this->assertTrue($model->hasNamedScope('parentScope')); + } + + public function testChildCanOverrideScopeFromParent(): void + { + $model = new ChildModelWithOverriddenScope(); + $builder = $this->createMock(Builder::class); + + // Should call the child's version which returns 'child' + $result = $model->callNamedScope('sharedScope', [$builder]); + + $this->assertSame('child', $result); + } +} + +// Test models +class ModelWithTraditionalScope extends Model +{ + protected ?string $table = 'test_models'; + + public function scopeActive(Builder $builder): Builder + { + return $builder; + } + + public static function isScopeMethodWithAttributePublic(string $method): bool + { + return static::isScopeMethodWithAttribute($method); + } +} + +class ModelWithScopeAttribute extends Model +{ + protected ?string $table = 'test_models'; + + #[Scope] + protected function verified(Builder $builder): Builder + { + return $builder; + } + + public static function isScopeMethodWithAttributePublic(string $method): bool + { + return static::isScopeMethodWithAttribute($method); + } +} + +class ModelWithParameterizedScope extends Model +{ + protected ?string $table = 'test_models'; + + #[Scope] + protected function ofType(Builder $builder, string $type): string + { + return $type; + } +} + +class ModelWithRegularMethod extends Model +{ + protected ?string $table = 'test_models'; + + public function regularMethod(): string + { + return 'regular'; + } + + public static function isScopeMethodWithAttributePublic(string $method): bool + { + return static::isScopeMethodWithAttribute($method); + } +} + +class ModelWithBothScopeTypes extends Model +{ + protected ?string $table = 'test_models'; + + public function scopeActive(Builder $builder): Builder + { + return $builder; + } + + #[Scope] + protected function verified(Builder $builder): Builder + { + return $builder; + } +} + +class ParentModelWithScopeAttribute extends Model +{ + protected ?string $table = 'test_models'; + + #[Scope] + protected function parentScope(Builder $builder): Builder + { + return $builder; + } + + #[Scope] + protected function sharedScope(Builder $builder): string + { + return 'parent'; + } +} + +class ChildModelWithInheritedScope extends ParentModelWithScopeAttribute +{ +} + +class ChildModelWithOverriddenScope extends ParentModelWithScopeAttribute +{ + #[Scope] + protected function sharedScope(Builder $builder): string + { + return 'child'; + } +} diff --git a/tests/Core/Database/Eloquent/Concerns/HasObserversTest.php b/tests/Core/Database/Eloquent/Concerns/HasObserversTest.php index 206545f8..1e6a203a 100644 --- a/tests/Core/Database/Eloquent/Concerns/HasObserversTest.php +++ b/tests/Core/Database/Eloquent/Concerns/HasObserversTest.php @@ -174,7 +174,7 @@ public function testResolveObserveAttributesMergesTraitAndClassObservers(): void public function testResolveObserveAttributesMergesParentTraitAndChildObservers(): void { - $result = ChildModelWithTraitParent::resolveObserveAttributes(); + $result = ChildModelWithObserverTraitParent::resolveObserveAttributes(); // Parent's trait observer -> child's class observer $this->assertSame([TraitObserver::class, ChildObserver::class], $result); @@ -409,7 +409,7 @@ class ModelWithTraitAndOwnObserver extends Model } // Parent model that uses a trait with observer -class ParentModelUsingTrait extends Model +class ParentModelUsingObserverTrait extends Model { use TraitWithObserver; @@ -417,7 +417,7 @@ class ParentModelUsingTrait extends Model } #[ObservedBy(ChildObserver::class)] -class ChildModelWithTraitParent extends ParentModelUsingTrait +class ChildModelWithObserverTraitParent extends ParentModelUsingObserverTrait { }