Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/auth/src/Access/Gate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -504,13 +505,38 @@ 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);
}
}
}

/**
* 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.
*
Expand Down
34 changes: 34 additions & 0 deletions src/core/src/Database/Eloquent/Attributes/UsePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Attributes;

use Attribute;

/**
* Declare the policy class for a model using an attribute.
*
* When placed on a model class, the Gate will use the specified policy
* class for authorization checks. This takes precedence over policy
* name guessing but not over explicitly registered policies.
*
* @example
* ```php
* #[UsePolicy(PostPolicy::class)]
* class Post extends Model {}
* ```
*/
#[Attribute(Attribute::TARGET_CLASS)]
class UsePolicy
{
/**
* Create a new attribute instance.
*
* @param class-string $class
*/
public function __construct(
public string $class
) {
}
}
59 changes: 59 additions & 0 deletions tests/Auth/Access/GateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
use Hypervel\Tests\Auth\Stub\AccessGateTestResource;
use Hypervel\Tests\Auth\Stub\AccessGateTestStaticClass;
use Hypervel\Tests\Auth\Stub\AccessGateTestSubDummy;
use Hypervel\Tests\Auth\Stub\DummyWithoutUsePolicy;
use Hypervel\Tests\Auth\Stub\DummyWithUsePolicy;
use Hypervel\Tests\Auth\Stub\DummyWithUsePolicyPolicy;
use Hypervel\Tests\Auth\Stub\SubDummyWithUsePolicy;
use Hypervel\Tests\TestCase;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -1011,6 +1015,61 @@ public function testClassesCanBeDefinedAsCallbacksUsingAtNotationForGuests()
$this->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();
Expand Down
12 changes: 12 additions & 0 deletions tests/Auth/Stub/DummyWithUsePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Auth\Stub;

use Hypervel\Database\Eloquent\Attributes\UsePolicy;

#[UsePolicy(DummyWithUsePolicyPolicy::class)]
class DummyWithUsePolicy
{
}
22 changes: 22 additions & 0 deletions tests/Auth/Stub/DummyWithUsePolicyPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Auth\Stub;

use Hypervel\Auth\Access\HandlesAuthorization;

class DummyWithUsePolicyPolicy
{
use HandlesAuthorization;

public function view($user, DummyWithUsePolicy $dummy): bool
{
return true;
}

public function update($user, DummyWithUsePolicy $dummy): bool
{
return true;
}
}
9 changes: 9 additions & 0 deletions tests/Auth/Stub/DummyWithoutUsePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Auth\Stub;

class DummyWithoutUsePolicy
{
}
16 changes: 16 additions & 0 deletions tests/Auth/Stub/SubDummyWithUsePolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Auth\Stub;

use Hypervel\Database\Eloquent\Attributes\UsePolicy;

/**
* Extends DummyWithUsePolicy but has its own UsePolicy attribute.
* Used to test that the attribute takes precedence over subclass fallback.
*/
#[UsePolicy(DummyWithUsePolicyPolicy::class)]
class SubDummyWithUsePolicy extends DummyWithUsePolicy
{
}