project-command-handlerlisted
Install: claude install-skill ubermuda/loupe
# Command + Handler Pattern
Any controller action that does more than render a template or redirect must use
a command + handler pair. The controller injects the handler and calls it
synchronously as a callable. Symfony Messenger is not involved. Do not put
business logic in a controller. Do not route a command through Messenger unless
async dispatch is a stated requirement.
## Scope: every entry point
The pattern covers every entry point, not only controllers. Console commands
(`Command/Console/`) and messenger handlers are thin shells too. They invoke a
`Command/` + `Handler` pair. They never call a business-logic service directly.
Name domain pairs verb-first, for example `RunTrialSweepCommand`. Give the
domain pair a name that differs from the console command class and the messenger
handler class, so the three never collide.
`PurgeExpiredExportsCommand` injects `ExpiredExportPurger` directly, a known
pre-existing violation. Migrate it when you touch it. Do not copy it.
## File layout
Both classes live in the module's `Command/` directory:
```
src/Module/Project/Command/CreateIssueCommand.php
src/Module/Project/Command/CreateIssueHandler.php
```
Name the pair after the action: `<Action><Entity>Command` / `<Action><Entity>Handler`.
## The command carries data only
Write a `final readonly class` with public promoted constructor properties. It
holds no logic, and no public method other than `__construct()`.
```php
final readonly class CreateIssueCommand
{
publ