← ClaudeAtlas

project-command-handlerlisted

Use when a controller action needs business logic (creating, updating, or deleting anything), or any action beyond rendering a template or redirecting. Documents the command + handler pair pattern, the enforced class shapes, and DomainErrors propagation.
ubermuda/loupe · ★ 2 · AI & Automation · score 65
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