Skip to content
Back to skills

Dotkernel Handler Naming

ASecurity

Use when naming or reviewing a PSR-15 request handler class, a route URL, a route name, or an authorization key in any Dotkernel project — creating a new endpoint or page, renaming one, adding a route to a RoutesDelegator, wiring a handler into a ConfigProvider, or checking that a handler follows the Dotkernel naming pattern. Also use when a route returns 403 or 404 unexpectedly, when choosing between PATCH and POST, or when deciding whether a handler needs a Form suffix.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentgophpapifrontend

Works with

  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add dotkernel/dotboost --skill dotkernel-handler-naming --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dotkernel Handler Naming?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dotkernel Handler Naming
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dotkernel-dotkernel-handler-naming/badge)](https://www.skillsdirectory.com/skills/dotkernel-dotkernel-handler-naming)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dotkernel-handler-naming
description: >
  Use when naming or reviewing a PSR-15 request handler class, a route URL, a route name, or an
  authorization key in any Dotkernel project — creating a new endpoint or page, renaming
  one, adding a route to a RoutesDelegator, wiring a handler into a ConfigProvider, or checking
  that a handler follows the Dotkernel naming pattern. Also use when a route returns 403 or 404
  unexpectedly, when choosing between PATCH and POST, or when deciding whether a handler needs
  a Form suffix.
---

# Handler, route and authorization naming

```text
Applies to
  Projects:  - Dotkernel API
             - Dotkernel Admin
             - Dotkernel Light
  Packages:  - none
```

The naming pattern exists so that any developer can tell what a file does from its name alone.
It has **two dialects**, and the word order differs between them — pick by variant
(`dotkernel-project-variants`). Frontend has no handlers: it routes to controllers with an
`{action}` segment, so none of this applies there.

## Dialect A — API (REST, no forms)

```text
{HttpVerb}{Resource}[{SubResource}][{Action}]{Resource|Collection}Handler
```

Resource first, then the optional action. Suffix with `ResourceHandler` (a single entity) or
`CollectionHandler` (a paginated list). Non-CRUD actions that return a message rather than a
resource end in `…{Action}Handler`. No CRUD word in the class name — the HTTP verb says it.
**Updates use PATCH, never PUT.**

| Class | Route |
| --- | --- |
| `GetUserCollectionHandler` | `GET /user` |
| `PostUserResourceHandler` | `POST /user` |
| `GetUserResourceHandler` | `GET /user/{id}` |
| `PatchUserResourceHandler` | `PATCH /user/{id}` |
| `DeleteUserResourceHandler` | `DELETE /user/{id}` |
| `PatchUserActivateHandler` | `PATCH /user/{id}/activate` |
| `PostUserAvatarResourceHandler` | `POST /user/{id}/avatar` |
| `GetUserRoleCollectionHandler` | `GET /user/role` |

## Dialect B — Admin (templated, form-driven)

```text
{HttpVerb}{Action}{Resource}[{SubResource}][Form]Handler
```

Action first, then the resource — the reverse of the API. A `Form` suffix means the handler
**renders a form** that submits to another handler. The pair is always a `Get…FormHandler` that
renders and a `Post…Handler` that acts — deletes included, which get a confirmation form.

| Class | Route | Route name |
| --- | --- | --- |
| `GetCreateAdminFormHandler` | `GET /admin/create-admin` | `admin::create-admin-form` |
| `PostCreateAdminHandler` | `POST /admin/create-admin` | `admin::create-admin` |
| `GetEditAdminFormHandler` | `GET /admin/edit-admin/{id}` | `admin::edit-admin-form` |
| `PostEditAdminHandler` | `POST /admin/edit-admin/{id}` | `admin::edit-admin` |
| `GetDeleteAdminFormHandler` | `GET /admin/delete-admin/{id}` | `admin::delete-admin-form` |
| `PostDeleteAdminHandler` | `POST /admin/delete-admin/{id}` | `admin::delete-admin` |
| `GetListAdminHandler` | `GET /admin/list-admin` | `admin::list-admin` |
| `PostEditUserAvatarHandler` | `POST /user/edit-user-avatar/{id}` | `user::edit-user-avatar` |
| `GetListUserHandler` | `GET /user/list-user` | `user::list-user` |

Action words are `Create`, `Edit`, `Delete`, `List` and `View`, plus explicit verbs
(`ChangeAccountPassword`, `Logout`, `Store`). Admin uses **POST for updates** (browser form
submit), never PATCH, and puts the action in the URL.

Light is a starter with two handlers, `GetIndexViewHandler` and `GetPageViewHandler`; the second
registers one `GET` route per template listed in config, named `<prefix>::<template>`. Its names
put the resource first. When you add to Light, match those two files rather than either dialect.

## Naming a new one — the one-minute method

1. Which HTTP verb? → prefix.
2. API: resource, then sub-resource, then a non-CRUD action. Admin: action, then resource.
3. Admin only: does it render a form to be submitted later? → append `Form`.
4. API only: one thing or a list? → `Resource` / `Collection`.
5. Append `Handler`.

## URL conventions

API:

- Lowercase, `kebab-case` for multi-word segments: `/user/account/reset-password`.
- Singular resource segments: `/user`, not `/users` — collection-ness comes from verb and path.
- Item identifiers are UUIDs, matched by the `REGEXP_UUID` constant in Core's `App`
  `ConfigProvider`, whose placeholder is `{id}`. Never declare an unconstrained placeholder —
  the UUID pattern is what keeps fixed segments like `/user/role` from colliding with
  `/user/{id}`.
- Sub-resources nest: `/user/{id}/avatar`.

Admin: `/<module>/<action>-<resource>[/{id}]`, grouped by module prefix — `/admin/edit-admin/{id}`.

## Route names

```text
<module>::<action>-<resource>
```

| | API | Admin |
| --- | --- | --- |
| Actions | `list`, `view`, `create`, `update`, `delete`, plus `activate`, `recover`… | `list`, `view`, `create`, `edit`, `delete`, plus `store`, `change`… |
| Form-rendering route | n/a | add `-form`: `admin::edit-admin-form` |
| Example | `PatchUserResourceHandler` → `user::update-user` | `PostEditUserHandler` → `user::edit-user` |

## Route name = authorization key — but the default differs

- **API** — RBAC permissions in `authorization.global.php`. **A route name absent there is
  denied for every role**, the usual cause of "my new route returns Forbidden".
- **Admin** — roles map to permissions (`authenticated`, `unauthenticated`, …) in
  `authorization.global.php`; route names map to those permissions in
  `authorization-guards.global.php`. The guards use `POLICY_ALLOW`, so **a route name absent from
  the guards is open to everyone, guests included**. Forgetting the entry never shows up as a
  403 — it shows up as an unprotected page.

## RoutesDelegator style

Group by path prefix; order routes alphabetically inside a group.

```php
$routeCollector->group('/book')
    ->get('', GetBookCollectionHandler::class, 'book::list-book')
    ->post('', PostBookResourceHandler::class, 'book::create-book');

$routeCollector->group('/book/' . $id)
    ->delete('', DeleteBookResourceHandler::class, 'book::delete-book')
    ->get('', GetBookResourceHandler::class, 'book::view-book')
    ->patch('', PatchBookResourceHandler::class, 'book::update-book');

return $callback();
```

## Handler body conventions (all variants)

Handlers hold **no business logic**: validate → call service → shape response. Extend the
project's abstract handler if it has one, take dependencies through `#[Inject]` where the project
uses `dot-dependency-injection`, throw the project's typed exceptions for errors, and never
`echo` or hand-build a response object.

## Checklist for a new route

- [ ] Class name follows the dialect for **this** project.
- [ ] File under `src/<Module>/src/Handler/<Resource>[/<Sub>]/`.
- [ ] Extends the repo's abstract handler; deps via `#[Inject]`.
- [ ] Registered in `ConfigProvider` under `factories` (and `delegators` in the API).
- [ ] Route in `RoutesDelegator` named for this dialect.
- [ ] Authorization entry added — API: every role that should reach it; Admin: a guard rule,
      without which the route is public.
- [ ] Presentation registered — HAL metadata (API) or template (templated project).
- [ ] Documented, if this project publishes OpenAPI.
- [ ] Test covering success and the denied path.
- [ ] The repo's route-listing command shows it as expected.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.