Skip to content

Commit 940b933

Browse files
committed
WIP Added new AbilityType::Access
1 parent 5a1c4f7 commit 940b933

File tree

5 files changed

+197
-24
lines changed

5 files changed

+197
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7+
## [1.1.1] - 2025-11-08
8+
- Added new `AbilityType::Access`. Introduces backend-only ability type for controlling access to entry-level artifacts (Apps, Dashboards, Reports, Tiles, KPIs, Resources, Dialogs).
9+
- Enhanced `Ui5Registry` accessors for abilities and settings
10+
711
## [1.1.0] - 2025-11-07
812
- Added tests for Ui5Registry
913
- Switched to PHP 8.3 language level

src/Enums/AbilityType.php

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,115 @@
44

55
use InvalidArgumentException;
66

7+
/**
8+
* Defines the four fundamental categories of Abilities within LaravelUi5.
9+
* Each type expresses a distinct semantic permission scope that governs
10+
* what a user may see, do, or enter across frontend and backend layers.
11+
*
12+
* Integer-backed for compact storage and stable comparison.
13+
*
14+
* ---------------------------------------------------------------------
15+
* Use → Access to frontend views, dialogs, or routes
16+
* Act → Execution of backend operations or actions
17+
* See → Visibility control for UI elements
18+
* Access → Permission to enter system-level artifacts (apps, dashboards,
19+
* reports, cards, tiles, KPIs, dialogs, resources)
20+
* ---------------------------------------------------------------------
21+
*
22+
* @example
23+
* if ($ability->type === AbilityType::Act) {
24+
* // Perform business logic for action-level permission
25+
* }
26+
*/
727
enum AbilityType: int
828
{
29+
/**
30+
* Frontend-level access to views, dialogs, or routes declared in manifest.json
31+
*/
932
case Use = 0;
33+
34+
/**
35+
* Permission to execute a backend operation or service action.
36+
*
37+
* Implementors that want to secure a frontend operation should
38+
* work via the `AbilityType::See` instead.
39+
*/
1040
case Act = 1;
41+
42+
/**
43+
* Controls visibility of UI elements such as buttons, dialogs,
44+
* or sections. Typically used in the frontend to bind `visible`
45+
* or `enabled` states to user abilities.
46+
*/
1147
case See = 2;
1248

49+
/**
50+
* Backend-level access to entry-point artifacts without manifest anchors
51+
*/
52+
case Access = 3;
53+
54+
/**
55+
* Returns the canonical lowercase label used in manifests and caches.
56+
*
57+
* @return string
58+
*/
1359
public function label(): string
1460
{
1561
return match ($this) {
1662
self::Use => 'use',
1763
self::Act => 'act',
1864
self::See => 'see',
65+
self::Access => 'access',
1966
};
2067
}
2168

22-
public function isAct(): bool
23-
{
24-
return $this === self::Act;
25-
}
26-
69+
/**
70+
* Creates an AbilityType from its canonical lowercase label.
71+
*
72+
* @param string $label
73+
* @return self
74+
*/
2775
public static function fromLabel(string $label): self
2876
{
2977
return match ($label) {
3078
'use' => self::Use,
3179
'act' => self::Act,
3280
'see' => self::See,
81+
'access' => self::Access,
3382
default => throw new InvalidArgumentException("Unknown AbilityType label: $label"),
3483
};
3584
}
85+
86+
/**
87+
* Returns true if this AbilityType represents an action-level permission.
88+
*
89+
* @return bool
90+
*/
91+
public function isAct(): bool
92+
{
93+
return $this === self::Act;
94+
}
95+
96+
/**
97+
* Returns true if this AbilityType represents an access-level permission.
98+
*
99+
* @return bool
100+
*/
101+
public function isAccess(): bool
102+
{
103+
return $this === self::Access;
104+
}
105+
106+
/**
107+
* Determines whether this AbilityType should be declared
108+
* inside the manifest.json (frontend domain) rather than
109+
* in backend PHP annotations.
110+
*/
111+
public function shouldBeInManifest(): bool
112+
{
113+
return match ($this) {
114+
self::Use, self::See => true,
115+
default => false,
116+
};
117+
}
36118
}

src/Ui5/Contracts/Ui5RegistryInterface.php

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace LaravelUi5\Core\Ui5\Contracts;
44

5+
use LaravelUi5\Core\Enums\AbilityType;
6+
use LaravelUi5\Core\Enums\ArtifactType;
7+
58
/**
69
* The `Ui5Registry` is the central coordination and introspection service
710
* of the LaravelUi5 ecosystem. It provides a unified API to discover, inspect,
@@ -72,18 +75,52 @@ public function all(): array;
7275
public function roles(): array;
7376

7477
/**
75-
* Returns all abilities declared across all modules via #[Ability] attributes.
78+
* Returns all registered abilities, grouped by namespace and ability type.
7679
*
77-
* @return array<string, array>
80+
* The result reflects the normalized internal structure:
81+
* `$abilities[$namespace][$type->label()][$abilityName] = Ability`.
82+
*
83+
* - When `$namespace` is provided, abilities are limited to that artifact
84+
* namespace (e.g. "io.pragmatiqu.offers").
85+
* - When `$type` is provided, only abilities of that `AbilityType`
86+
* (e.g. `AbilityType::Act`) are returned.
87+
* - When both are null, all abilities across all namespaces and types
88+
* are returned.
89+
*
90+
* Example:
91+
* ```php
92+
* $registry->abilities('io.pragmatiqu.reports', AbilityType::Act);
93+
* // → [ 'toggleLock' => Ability, 'exportPdf' => Ability, ... ]
94+
* ```
95+
*
96+
* @param string|null $namespace Optional artifact namespace to filter by.
97+
* @param ArtifactType|null $type Optional ability type to filter by.
98+
* @return array
7899
*/
79-
public function abilities(): array;
100+
public function abilities(?string $namespace = null, ?ArtifactType $type = null): array;
80101

81102
/**
82-
* Returns all settings declared across all artifacts via #[Setting] attributes.
103+
* Returns all settings declared via #[Setting] attributes,
104+
* grouped by artifact namespace.
83105
*
84-
* @return array<string, array>
106+
* - When `$namespace` is provided, only settings belonging to
107+
* that namespace are returned.
108+
* - When `$namespace` is null, all settings across all registered
109+
* artifacts are returned.
110+
*
111+
* The result reflects the normalized internal structure:
112+
* `$settings[$namespace][$settingName] = Setting`.
113+
*
114+
* Example:
115+
* ```php
116+
* $registry->settings('io.pragmatiqu.dashboard');
117+
* // → [ 'refreshInterval' => Setting, 'theme' => Setting, ... ]
118+
* ```
119+
*
120+
* @param string|null $namespace Optional artifact namespace to filter by.
121+
* @return array
85122
*/
86-
public function settings(): array;
123+
public function settings(?string $namespace = null): array;
87124

88125
/**
89126
* Returns all semantic objects declared via #[SemanticObject] attributes.

src/Ui5/Ui5Registry.php

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
use LaravelUi5\Core\Ui5\Contracts\ReportActionInterface;
1414
use LaravelUi5\Core\Ui5\Contracts\SluggableInterface;
1515
use LaravelUi5\Core\Ui5\Contracts\Ui5ActionInterface;
16+
use LaravelUi5\Core\Ui5\Contracts\Ui5AppInterface;
1617
use LaravelUi5\Core\Ui5\Contracts\Ui5ArtifactInterface;
18+
use LaravelUi5\Core\Ui5\Contracts\Ui5CardInterface;
19+
use LaravelUi5\Core\Ui5\Contracts\Ui5DashboardInterface;
20+
use LaravelUi5\Core\Ui5\Contracts\Ui5DialogInterface;
21+
use LaravelUi5\Core\Ui5\Contracts\Ui5KpiInterface;
1722
use LaravelUi5\Core\Ui5\Contracts\Ui5ModuleInterface;
1823
use LaravelUi5\Core\Ui5\Contracts\Ui5RegistryInterface;
1924
use LaravelUi5\Core\Ui5\Contracts\Ui5ReportInterface;
25+
use LaravelUi5\Core\Ui5\Contracts\Ui5ResourceInterface;
26+
use LaravelUi5\Core\Ui5\Contracts\Ui5TileInterface;
2027
use LogicException;
2128
use ReflectionClass;
2229
use ReflectionException;
@@ -183,30 +190,47 @@ protected function discoverAbilities(Ui5ArtifactInterface|ReportActionInterface
183190
/** @var Ability $ability */
184191
$ability = $attributes[0]->newInstance();
185192

186-
if ($ability->type === AbilityType::Use) {
193+
if ($ability->type->shouldBeInManifest()) {
187194
throw new LogicException(sprintf(
188-
'Invalid ability declaration: [%s] uses type [Use], which is reserved for UI visibility. Move this definition to your manifest.json file.',
189-
$ability->name
195+
'AbilityType::%s for ability %s cannot be declared in backend artifacts (%s). Move this definition to your manifest.json file.',
196+
$ability->name,
197+
$ability->type->name,
198+
get_class($artifact)
190199
));
191200
}
192201

193-
if ($ability->type === AbilityType::Act && !($artifact instanceof Ui5ActionInterface || $artifact instanceof ReportActionInterface)) {
202+
if ($ability->type->isAct() && !($artifact instanceof Ui5ActionInterface || $artifact instanceof ReportActionInterface)) {
194203
throw new LogicException(sprintf(
195-
'Ability [%s] of type [Act] must be declared on an executable artifact, found on [%s].',
204+
'AbilityType::Act for ability %s must be declared on an executable artifact, found on (%s).',
196205
$ability->name,
197206
get_class($artifact)
198207
));
199208
}
200209

201-
if (array_key_exists($ability->name, $this->abilities[$namespace] ?? [])) {
210+
if ($ability->type->isAccess() && !(
211+
$artifact instanceof Ui5AppInterface
212+
|| $artifact instanceof Ui5CardInterface
213+
|| $artifact instanceof Ui5ReportInterface
214+
|| $artifact instanceof Ui5TileInterface
215+
|| $artifact instanceof Ui5KpiInterface
216+
|| $artifact instanceof Ui5DashboardInterface
217+
|| $artifact instanceof Ui5ResourceInterface
218+
|| $artifact instanceof Ui5DialogInterface
219+
)) {
220+
throw new LogicException(
221+
sprintf('AbilityType::Access is only valid for entry-level artifacts (%s)', get_class($artifact))
222+
);
223+
}
224+
225+
if (array_key_exists($ability->name, $this->abilities[$namespace][$ability->type->label()] ?? [])) {
202226
throw new LogicException(sprintf(
203227
'Duplicate ability [%s] found on [%s].',
204228
$ability->name,
205229
get_class($artifact)
206230
));
207231
}
208232

209-
$this->abilities[$namespace][$ability->name] = [
233+
$this->abilities[$namespace][$ability->type->label()][$ability->name] = [
210234
'type' => $ability->type->name,
211235
'role' => $ability->role,
212236
'note' => $ability->note,
@@ -429,14 +453,37 @@ public function roles(): array
429453
return $this->roles;
430454
}
431455

432-
public function abilities(): array
456+
public function abilities(?string $namespace = null, ?ArtifactType $type = null): array
433457
{
434-
return $this->abilities;
458+
if ($namespace === null) {
459+
if ($type === null) {
460+
return $this->abilities;
461+
}
462+
463+
$result = [];
464+
foreach ($this->abilities as $ns => $types) {
465+
if (isset($types[$type->label()])) {
466+
$result[$ns] = $types[$type->label()];
467+
}
468+
}
469+
470+
return $result;
471+
}
472+
473+
if ($type === null) {
474+
return $this->abilities[$namespace] ?? [];
475+
}
476+
477+
return $this->abilities[$namespace][$type->label()] ?? [];
435478
}
436479

437-
public function settings(): array
480+
public function settings(?string $namespace = null): array
438481
{
439-
return $this->settings;
482+
if (null === $namespace) {
483+
return $this->settings;
484+
}
485+
486+
return $this->settings[$namespace] ?? [];
440487
}
441488

442489
public function objects(): array

tests/Unit/Ui5Registry/AbilitiesTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use LaravelUi5\Core\Enums\AbilityType;
34
use LaravelUi5\Core\Ui5\Ui5Registry;
45
use Tests\Fixture\Hello\Hello;
56

@@ -12,8 +13,10 @@
1213
->toBeArray()
1314
->toHaveKey(Hello::NAMESPACE)
1415
->and($abilities[Hello::NAMESPACE])
16+
->toHaveKey(AbilityType::Act->label())
17+
->and($abilities[Hello::NAMESPACE][AbilityType::Act->label()])
1518
->toHaveKey(Hello::ACTION_NAME)
16-
->and($abilities[Hello::NAMESPACE][Hello::ACTION_NAME])
19+
->and($abilities[Hello::NAMESPACE][AbilityType::Act->label()][Hello::ACTION_NAME])
1720
->toMatchArray([
1821
'type' => 'Act',
1922
'role' => 'Admin',
@@ -30,7 +33,7 @@
3033
'modules' => [
3134
'foo' => \Tests\Fixture\Hello\Errors\Ability\UseAbility\Module::class,
3235
]
33-
]))->toThrow(LogicException::class, 'reserved for UI visibility');
36+
]))->toThrow(LogicException::class, 'cannot be declared in backend artifacts');
3437
});
3538

3639
it('throws when Ability type ACT declared on non-action artifact', function () {

0 commit comments

Comments
 (0)