@@ -92,6 +92,227 @@ controller behaviors. Roles receive features.
9292Components can be controllers, controller actions, models, model methods, CLI
9393tasks, or WebSocket tasks.
9494
95+ Use controller class names as the canonical component key. Route-style
96+ controller aliases such as ` project-user ` are accepted during dispatcher
97+ security checks for compatibility, but class constants are easier to refactor.
98+
99+ Use dash-case for action keys because these names match URLs:
100+
101+ ``` php
102+ \App\Modules\Api\Controllers\ProjectController::class => [
103+ 'find',
104+ 'find-with',
105+ 'archive-project',
106+ ],
107+ ```
108+
109+ Existing camelCase action config remains valid. PhalconKit normalizes dispatcher
110+ actions and ACL registrations so ` findWith ` and ` find-with ` refer to the same
111+ permission action. New docs and generated examples should prefer dash-case.
112+
113+ ## Controller Attributes
114+
115+ Controllers may declare permissions with PHP attributes. Attributes are additive:
116+ they compile into the same ` permissions ` structure shown above, so existing
117+ config-driven features, roles, and inheritance remain valid.
118+
119+ ``` php
120+ use Phalcon\Http\ResponseInterface;
121+ use PhalconKit\Mvc\Controller\Attributes\AllowRoles;
122+ use PhalconKit\Mvc\Controller\Attributes\AttachBehavior;
123+ use PhalconKit\Mvc\Controller\Attributes\PermissionFeature;
124+ use PhalconKit\Mvc\Controller\Behavior\Query\Conditions\RemoveDefaultPermissionCondition;
125+
126+ #[PermissionFeature('project.view', actions: ['find', 'find-with'])]
127+ #[AllowRoles('admin', actions: '*')]
128+ final class ProjectController extends AbstractController
129+ {
130+ #[PermissionFeature('project.write')]
131+ #[AllowRoles(['admin', 'manager'])]
132+ #[AttachBehavior(RemoveDefaultPermissionCondition::class, roles: 'admin')]
133+ public function archiveProjectAction(): ResponseInterface
134+ {
135+ // ...
136+ }
137+ }
138+ ```
139+
140+ Method-level attributes without ` actions ` use the method name after removing the
141+ ` Action ` suffix. In the example above, ` archiveProjectAction() ` maps to
142+ ` archive-project ` . Class-level attributes without ` actions ` apply to ` * ` .
143+
144+ ` PermissionFeature ` declares which actions belong to a feature; roles still
145+ receive that feature through config:
146+
147+ ``` php
148+ 'permissions' => [
149+ 'roles' => [
150+ 'manager' => [
151+ 'features' => ['project.write'],
152+ ],
153+ ],
154+ ],
155+ ```
156+
157+ ` AllowRoles ` grants controller actions directly to roles when a small app or a
158+ local workflow does not need a reusable feature. ` AttachBehavior ` can target
159+ roles, features, or both. When neither ` roles ` nor ` features ` is provided, the
160+ behavior attaches for the ` everyone ` context role.
161+
162+ ### Config-First Feature Example
163+
164+ Use this style when permissions are owned centrally and several controllers,
165+ models, tasks, or WebSocket actions share the same feature.
166+
167+ ``` php
168+ 'permissions' => [
169+ 'features' => [
170+ 'project.view' => [
171+ 'components' => [
172+ \App\Modules\Api\Controllers\ProjectController::class => [
173+ 'find',
174+ 'find-with',
175+ 'find-first',
176+ 'find-first-with',
177+ ],
178+ \App\Models\Project::class => ['find'],
179+ ],
180+ ],
181+ 'project.manage' => [
182+ 'components' => [
183+ \App\Modules\Api\Controllers\ProjectController::class => ['*'],
184+ \App\Models\Project::class => ['*'],
185+ ],
186+ 'behaviors' => [
187+ \App\Modules\Api\Controllers\ProjectController::class => [
188+ RemoveDefaultPermissionCondition::class,
189+ ],
190+ ],
191+ ],
192+ ],
193+ 'roles' => [
194+ 'admin' => [
195+ 'features' => ['project.manage'],
196+ ],
197+ 'researcher' => [
198+ 'features' => ['project.view'],
199+ ],
200+ ],
201+ ],
202+ ```
203+
204+ ### Attribute-First Feature Example
205+
206+ Use this style when the controller owns its own action surface but roles should
207+ still receive reusable features through config.
208+
209+ ``` php
210+ #[PermissionFeature('project.view', actions: [
211+ 'find',
212+ 'find-with',
213+ 'find-first',
214+ 'find-first-with',
215+ ])]
216+ #[PermissionFeature('project.manage', actions: '*')]
217+ #[AttachBehavior(RemoveDefaultPermissionCondition::class, features: 'project.manage')]
218+ final class ProjectController extends AbstractController
219+ {
220+ #[PermissionFeature('project.manage')]
221+ public function archiveProjectAction(): ResponseInterface
222+ {
223+ // archive-project
224+ }
225+ }
226+ ```
227+
228+ The remaining config only assigns features to roles:
229+
230+ ``` php
231+ 'permissions' => [
232+ 'roles' => [
233+ 'admin' => [
234+ 'features' => ['project.manage'],
235+ ],
236+ 'researcher' => [
237+ 'features' => ['project.view'],
238+ ],
239+ ],
240+ ],
241+ ```
242+
243+ ### Direct Role Attribute Example
244+
245+ Use direct role attributes for small controllers or local actions that are not
246+ worth turning into global feature names.
247+
248+ ``` php
249+ final class ProjectController extends AbstractController
250+ {
251+ #[AllowRoles(['admin', 'manager'])]
252+ public function archiveProjectAction(): ResponseInterface
253+ {
254+ // archive-project
255+ }
256+
257+ #[AllowRoles('admin', actions: ['restore', 'force-delete'])]
258+ public function restoreAction(): ResponseInterface
259+ {
260+ // restore
261+ }
262+ }
263+ ```
264+
265+ Direct role attributes do not require matching ` features ` config. The role still
266+ has to be present in the current identity's ACL role list.
267+
268+ ### Behavior Example In Both Styles
269+
270+ Action-scoped behavior through config:
271+
272+ ``` php
273+ 'permissions' => [
274+ 'roles' => [
275+ 'admin' => [
276+ 'behaviorActions' => [
277+ \App\Modules\Api\Controllers\ProjectController::class => [
278+ 'archive-project' => [
279+ RemoveDefaultPermissionCondition::class,
280+ ],
281+ ],
282+ ],
283+ ],
284+ ],
285+ ],
286+ ```
287+
288+ The same behavior beside the action:
289+
290+ ``` php
291+ final class ProjectController extends AbstractController
292+ {
293+ #[AttachBehavior(RemoveDefaultPermissionCondition::class, roles: 'admin')]
294+ public function archiveProjectAction(): ResponseInterface
295+ {
296+ // archive-project
297+ }
298+ }
299+ ```
300+
301+ The existing ` behaviors ` key is still supported for non-action-scoped behavior
302+ attachment.
303+
304+ Controller attributes use PHP's built-in Reflection API. Normal PHP 8.5 builds
305+ include Reflection; no extra Composer package or optional extension is required.
306+ Only the active controller class is inspected, and PhalconKit caches the result
307+ inside the process. Config-only applications can disable attribute scanning with
308+ ` ACL_ATTRIBUTES=false ` or config:
309+
310+ ``` php
311+ 'acl' => [
312+ 'attributes' => false,
313+ ],
314+ ```
315+
95316## Row-Level Conditions
96317
97318Feature-level access answers whether a role may use a component. Row-level
0 commit comments