diff --git a/src/auth/src/Access/Gate.php b/src/auth/src/Access/Gate.php index 540b00122..ce939ce48 100644 --- a/src/auth/src/Access/Gate.php +++ b/src/auth/src/Access/Gate.php @@ -13,6 +13,7 @@ use Hypervel\Auth\Access\Events\GateEvaluated; use Hypervel\Auth\Contracts\Authenticatable; use Hypervel\Auth\Contracts\Gate as GateContract; +use Hypervel\Database\Eloquent\Attributes\UsePolicy; use InvalidArgumentException; use Psr\EventDispatcher\EventDispatcherInterface; use ReflectionClass; @@ -504,6 +505,12 @@ public function getPolicyFor(object|string $class) return $this->resolvePolicy($this->policies[$class]); } + $policy = $this->getPolicyFromAttribute($class); + + if (! is_null($policy)) { + return $this->resolvePolicy($policy); + } + foreach ($this->policies as $expected => $policy) { if (is_subclass_of($class, $expected)) { return $this->resolvePolicy($policy); @@ -511,6 +518,25 @@ public function getPolicyFor(object|string $class) } } + /** + * Get the policy class from the UsePolicy attribute. + * + * @param class-string $class + * @return null|class-string + */ + protected function getPolicyFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UsePolicy::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } + /** * Build a policy class instance of the given type. * diff --git a/src/core/src/Database/Eloquent/Attributes/UsePolicy.php b/src/core/src/Database/Eloquent/Attributes/UsePolicy.php new file mode 100644 index 000000000..9139fcf79 --- /dev/null +++ b/src/core/src/Database/Eloquent/Attributes/UsePolicy.php @@ -0,0 +1,34 @@ +assertFalse($gate->check('absent_invokable')); } + public function testPolicyCanBeResolvedFromUsePolicyAttribute(): void + { + $gate = $this->getBasicGate(); + + $this->assertInstanceOf( + DummyWithUsePolicyPolicy::class, + $gate->getPolicyFor(DummyWithUsePolicy::class) + ); + } + + public function testPolicyFromUsePolicyAttributeWorksWithObjectInstance(): void + { + $gate = $this->getBasicGate(); + + $this->assertInstanceOf( + DummyWithUsePolicyPolicy::class, + $gate->getPolicyFor(new DummyWithUsePolicy()) + ); + } + + public function testExplicitPolicyTakesPrecedenceOverUsePolicyAttribute(): void + { + $gate = $this->getBasicGate(); + + // Register an explicit policy that should take precedence + $gate->policy(DummyWithUsePolicy::class, AccessGateTestPolicy::class); + + $this->assertInstanceOf( + AccessGateTestPolicy::class, + $gate->getPolicyFor(DummyWithUsePolicy::class) + ); + } + + public function testUsePolicyAttributeTakesPrecedenceOverSubclassFallback(): void + { + $gate = $this->getBasicGate(); + + // Register a policy for the parent class + $gate->policy(DummyWithUsePolicy::class, AccessGateTestPolicy::class); + + // SubDummyWithUsePolicy extends DummyWithUsePolicy but has its own #[UsePolicy] attribute + // The attribute should take precedence over the subclass fallback + $this->assertInstanceOf( + DummyWithUsePolicyPolicy::class, + $gate->getPolicyFor(SubDummyWithUsePolicy::class) + ); + } + + public function testGetPolicyForReturnsNullForClassWithoutUsePolicyAttribute(): void + { + $gate = $this->getBasicGate(); + + $this->assertNull($gate->getPolicyFor(DummyWithoutUsePolicy::class)); + } + public function testCanSetDenialResponseInConstructor() { $gate = $this->getGuestGate(); diff --git a/tests/Auth/Stub/DummyWithUsePolicy.php b/tests/Auth/Stub/DummyWithUsePolicy.php new file mode 100644 index 000000000..4a8c5462e --- /dev/null +++ b/tests/Auth/Stub/DummyWithUsePolicy.php @@ -0,0 +1,12 @@ +