Skip to content

Commit 07fda0f

Browse files
authored
Merge pull request #328 from binaryfire/feature/use-policy-attribute
feat: Add `#[UsePolicy]` attribute for declarative policy binding
2 parents e99b3e1 + 2794ea2 commit 07fda0f

File tree

7 files changed

+178
-0
lines changed

7 files changed

+178
-0
lines changed

src/auth/src/Access/Gate.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Hypervel\Auth\Access\Events\GateEvaluated;
1414
use Hypervel\Auth\Contracts\Authenticatable;
1515
use Hypervel\Auth\Contracts\Gate as GateContract;
16+
use Hypervel\Database\Eloquent\Attributes\UsePolicy;
1617
use InvalidArgumentException;
1718
use Psr\EventDispatcher\EventDispatcherInterface;
1819
use ReflectionClass;
@@ -496,13 +497,38 @@ public function getPolicyFor(object|string $class)
496497
return $this->resolvePolicy($this->policies[$class]);
497498
}
498499

500+
$policy = $this->getPolicyFromAttribute($class);
501+
502+
if (! is_null($policy)) {
503+
return $this->resolvePolicy($policy);
504+
}
505+
499506
foreach ($this->policies as $expected => $policy) {
500507
if (is_subclass_of($class, $expected)) {
501508
return $this->resolvePolicy($policy);
502509
}
503510
}
504511
}
505512

513+
/**
514+
* Get the policy class from the UsePolicy attribute.
515+
*
516+
* @param class-string $class
517+
* @return null|class-string
518+
*/
519+
protected function getPolicyFromAttribute(string $class): ?string
520+
{
521+
if (! class_exists($class)) {
522+
return null;
523+
}
524+
525+
$attributes = (new ReflectionClass($class))->getAttributes(UsePolicy::class);
526+
527+
return $attributes !== []
528+
? $attributes[0]->newInstance()->class
529+
: null;
530+
}
531+
506532
/**
507533
* Build a policy class instance of the given type.
508534
*
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Attributes;
6+
7+
use Attribute;
8+
9+
/**
10+
* Declare the policy class for a model using an attribute.
11+
*
12+
* When placed on a model class, the Gate will use the specified policy
13+
* class for authorization checks. This takes precedence over policy
14+
* name guessing but not over explicitly registered policies.
15+
*
16+
* @example
17+
* ```php
18+
* #[UsePolicy(PostPolicy::class)]
19+
* class Post extends Model {}
20+
* ```
21+
*/
22+
#[Attribute(Attribute::TARGET_CLASS)]
23+
class UsePolicy
24+
{
25+
/**
26+
* Create a new attribute instance.
27+
*
28+
* @param class-string $class
29+
*/
30+
public function __construct(
31+
public string $class
32+
) {
33+
}
34+
}

tests/Auth/Access/GateTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
use Hypervel\Tests\Auth\Stub\AccessGateTestResource;
3434
use Hypervel\Tests\Auth\Stub\AccessGateTestStaticClass;
3535
use Hypervel\Tests\Auth\Stub\AccessGateTestSubDummy;
36+
use Hypervel\Tests\Auth\Stub\DummyWithoutUsePolicy;
37+
use Hypervel\Tests\Auth\Stub\DummyWithUsePolicy;
38+
use Hypervel\Tests\Auth\Stub\DummyWithUsePolicyPolicy;
39+
use Hypervel\Tests\Auth\Stub\SubDummyWithUsePolicy;
3640
use Hypervel\Tests\TestCase;
3741
use InvalidArgumentException;
3842
use PHPUnit\Framework\Attributes\DataProvider;
@@ -1011,6 +1015,61 @@ public function testClassesCanBeDefinedAsCallbacksUsingAtNotationForGuests()
10111015
$this->assertFalse($gate->check('absent_invokable'));
10121016
}
10131017

1018+
public function testPolicyCanBeResolvedFromUsePolicyAttribute(): void
1019+
{
1020+
$gate = $this->getBasicGate();
1021+
1022+
$this->assertInstanceOf(
1023+
DummyWithUsePolicyPolicy::class,
1024+
$gate->getPolicyFor(DummyWithUsePolicy::class)
1025+
);
1026+
}
1027+
1028+
public function testPolicyFromUsePolicyAttributeWorksWithObjectInstance(): void
1029+
{
1030+
$gate = $this->getBasicGate();
1031+
1032+
$this->assertInstanceOf(
1033+
DummyWithUsePolicyPolicy::class,
1034+
$gate->getPolicyFor(new DummyWithUsePolicy())
1035+
);
1036+
}
1037+
1038+
public function testExplicitPolicyTakesPrecedenceOverUsePolicyAttribute(): void
1039+
{
1040+
$gate = $this->getBasicGate();
1041+
1042+
// Register an explicit policy that should take precedence
1043+
$gate->policy(DummyWithUsePolicy::class, AccessGateTestPolicy::class);
1044+
1045+
$this->assertInstanceOf(
1046+
AccessGateTestPolicy::class,
1047+
$gate->getPolicyFor(DummyWithUsePolicy::class)
1048+
);
1049+
}
1050+
1051+
public function testUsePolicyAttributeTakesPrecedenceOverSubclassFallback(): void
1052+
{
1053+
$gate = $this->getBasicGate();
1054+
1055+
// Register a policy for the parent class
1056+
$gate->policy(DummyWithUsePolicy::class, AccessGateTestPolicy::class);
1057+
1058+
// SubDummyWithUsePolicy extends DummyWithUsePolicy but has its own #[UsePolicy] attribute
1059+
// The attribute should take precedence over the subclass fallback
1060+
$this->assertInstanceOf(
1061+
DummyWithUsePolicyPolicy::class,
1062+
$gate->getPolicyFor(SubDummyWithUsePolicy::class)
1063+
);
1064+
}
1065+
1066+
public function testGetPolicyForReturnsNullForClassWithoutUsePolicyAttribute(): void
1067+
{
1068+
$gate = $this->getBasicGate();
1069+
1070+
$this->assertNull($gate->getPolicyFor(DummyWithoutUsePolicy::class));
1071+
}
1072+
10141073
public function testCanSetDenialResponseInConstructor()
10151074
{
10161075
$gate = $this->getGuestGate();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Auth\Stub;
6+
7+
use Hypervel\Database\Eloquent\Attributes\UsePolicy;
8+
9+
#[UsePolicy(DummyWithUsePolicyPolicy::class)]
10+
class DummyWithUsePolicy
11+
{
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Auth\Stub;
6+
7+
use Hypervel\Auth\Access\HandlesAuthorization;
8+
9+
class DummyWithUsePolicyPolicy
10+
{
11+
use HandlesAuthorization;
12+
13+
public function view($user, DummyWithUsePolicy $dummy): bool
14+
{
15+
return true;
16+
}
17+
18+
public function update($user, DummyWithUsePolicy $dummy): bool
19+
{
20+
return true;
21+
}
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Auth\Stub;
6+
7+
class DummyWithoutUsePolicy
8+
{
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Auth\Stub;
6+
7+
use Hypervel\Database\Eloquent\Attributes\UsePolicy;
8+
9+
/**
10+
* Extends DummyWithUsePolicy but has its own UsePolicy attribute.
11+
* Used to test that the attribute takes precedence over subclass fallback.
12+
*/
13+
#[UsePolicy(DummyWithUsePolicyPolicy::class)]
14+
class SubDummyWithUsePolicy extends DummyWithUsePolicy
15+
{
16+
}

0 commit comments

Comments
 (0)