Skip to content

Commit 7b90225

Browse files
authored
feat: capability can apply to entire permission class (#36)
* feat: update stubs to follow patterns * feat: capability can apply to entire permission class
1 parent 3517aca commit 7b90225

File tree

6 files changed

+86
-12
lines changed

6 files changed

+86
-12
lines changed

src/Attributes/Capability.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@
99
/**
1010
* Assigns a permission to a capability during sync.
1111
*
12-
* Multiple Capability attributes can be applied to assign a permission to multiple capabilities.
12+
* Can be applied at the class level to assign all permissions in the class to a capability,
13+
* or at the constant level for individual permissions. Multiple attributes can be applied
14+
* to assign a permission to multiple capabilities.
1315
*
14-
* @example
16+
* @example Class-level (all permissions inherit):
17+
* #[Capability('user-management')]
18+
* class UserPermissions
19+
* {
20+
* public const VIEW = 'user:view';
21+
* public const EDIT = 'user:edit';
22+
* }
23+
* @example Constant-level:
1524
* #[Capability('user-management')]
1625
* public const VIEW = 'user:view';
17-
*
26+
* @example Multiple capabilities:
1827
* #[Capability('user-management')]
1928
* #[Capability('reporting')]
2029
* public const EXPORT = 'user:export';
2130
*/
22-
#[Attribute(Attribute::TARGET_CLASS_CONSTANT | Attribute::IS_REPEATABLE)]
31+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_CLASS_CONSTANT | Attribute::IS_REPEATABLE)]
2332
final readonly class Capability
2433
{
2534
public function __construct(

src/CodeFirst/DefinitionDiscoverer.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ private function extractPermissionsFromClass(ReflectionClass $class): array
155155
$classGuard = $this->getClassGuard($class);
156156
$classLabel = $this->getClassAttribute($class, Label::class)?->value;
157157
$classDescription = $this->getClassAttribute($class, Description::class)?->value;
158+
$classCapabilities = $this->getClassCapabilityNames($class);
158159

159160
foreach ($class->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC) as $constant) {
160161
$value = $constant->getValue();
@@ -166,14 +167,15 @@ private function extractPermissionsFromClass(ReflectionClass $class): array
166167
$labelAttr = $this->getConstantAttribute($constant, Label::class);
167168
$descAttr = $this->getConstantAttribute($constant, Description::class);
168169
$contextAttr = $this->getConstantAttribute($constant, Context::class);
170+
$constantCapabilities = $this->getCapabilityNames($constant);
169171

170172
$definitions[] = PermissionDefinition::fromAttributes([
171173
'name' => $value,
172174
'guard' => $classGuard,
173175
'label' => $labelAttr !== null ? $labelAttr->value : $classLabel,
174176
'description' => $descAttr !== null ? $descAttr->value : $classDescription,
175177
'context' => $contextAttr?->modelClass,
176-
'capabilities' => $this->getCapabilityNames($constant),
178+
'capabilities' => array_unique(array_merge($classCapabilities, $constantCapabilities)),
177179
'source_class' => $class->getName(),
178180
'source_constant' => $constant->getName(),
179181
]);
@@ -321,4 +323,20 @@ private function getCapabilityNames(ReflectionClassConstant $constant): array
321323
$attributes
322324
);
323325
}
326+
327+
/**
328+
* Get all capability names from a class's Capability attributes.
329+
*
330+
* @param ReflectionClass<object> $class
331+
* @return array<string>
332+
*/
333+
private function getClassCapabilityNames(ReflectionClass $class): array
334+
{
335+
$attributes = $class->getAttributes(CapabilityAttribute::class);
336+
337+
return array_map(
338+
fn ($attr) => $attr->newInstance()->name,
339+
$attributes
340+
);
341+
}
324342
}

stubs/capability.stub

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use OffloadProject\Mandate\Attributes\Label;
1111
#[Guard('{{ guard }}')]
1212
final class {{ class }}
1313
{
14-
#[Label('{{ capability }} Management')]
14+
#[Label('Manage {{ capability }}')]
1515
#[Description('Manage {{ capability }} resources')]
16-
public const MANAGE = '{{ capability }}-management';
16+
public const MANAGE = 'manage-{{ capability }}';
1717
}

stubs/permission.stub

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ use OffloadProject\Mandate\Attributes\Label;
1111
#[Guard('{{ guard }}')]
1212
final class {{ class }}
1313
{
14+
#[Label('View any {{ resource }}')]
15+
#[Description('View any {{ resource }} records')]
16+
public const VIEW_ANY = '{{ resource }}:view_any';
17+
1418
#[Label('View {{ resource }}')]
15-
#[Description('View {{ resource }} records')]
16-
public const VIEW = '{{ resource }}.view';
19+
#[Description('View {{ resource }} record')]
20+
public const VIEW = '{{ resource }}:view';
1721

1822
#[Label('Create {{ resource }}')]
1923
#[Description('Create new {{ resource }} records')]
20-
public const CREATE = '{{ resource }}.create';
24+
public const CREATE = '{{ resource }}:create';
2125

2226
#[Label('Update {{ resource }}')]
2327
#[Description('Update existing {{ resource }} records')]
24-
public const UPDATE = '{{ resource }}.update';
28+
public const UPDATE = '{{ resource }}:update';
2529

2630
#[Label('Delete {{ resource }}')]
2731
#[Description('Delete {{ resource }} records')]
28-
public const DELETE = '{{ resource }}.delete';
32+
public const DELETE = '{{ resource }}:delete';
2933
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OffloadProject\Mandate\Tests\Fixtures\CodeFirst;
6+
7+
use OffloadProject\Mandate\Attributes\Capability;
8+
use OffloadProject\Mandate\Attributes\Guard;
9+
use OffloadProject\Mandate\Attributes\Label;
10+
11+
#[Guard('web')]
12+
#[Capability('user-management')]
13+
class UserPermissions
14+
{
15+
#[Label('View Users')]
16+
public const VIEW = 'user:view';
17+
18+
#[Label('Edit Users')]
19+
public const EDIT = 'user:edit';
20+
21+
#[Label('Delete Users')]
22+
#[Capability('admin-only')]
23+
public const DELETE = 'user:delete';
24+
}

tests/Unit/CodeFirst/DefinitionDiscovererTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@
6464

6565
expect($permissions)->toBeEmpty();
6666
});
67+
68+
it('extracts capabilities from class-level attribute', function () {
69+
$permissions = $this->discoverer->discoverPermissions($this->fixturesPath);
70+
71+
$viewPermission = $permissions->first(fn (PermissionDefinition $p) => $p->name === 'user:view');
72+
$editPermission = $permissions->first(fn (PermissionDefinition $p) => $p->name === 'user:edit');
73+
74+
expect($viewPermission->capabilities)->toBe(['user-management']);
75+
expect($editPermission->capabilities)->toBe(['user-management']);
76+
});
77+
78+
it('merges class-level and constant-level capabilities', function () {
79+
$permissions = $this->discoverer->discoverPermissions($this->fixturesPath);
80+
81+
$deletePermission = $permissions->first(fn (PermissionDefinition $p) => $p->name === 'user:delete');
82+
83+
expect($deletePermission->capabilities)->toContain('user-management', 'admin-only');
84+
expect(count($deletePermission->capabilities))->toBe(2);
85+
});
6786
});
6887

6988
describe('discoverRoles', function () {

0 commit comments

Comments
 (0)