symfony-workflow
When to use
Use this skill for all Symfony-specific code generation and editing tasks, especially when working with:
- Controllers (annotated / attribute-routed)
- Request listeners / event subscribers
- Services and Dependency Injection (
services.yaml)
- Forms and validators
- Doctrine entities, repositories, and migrations
- Security: firewalls, voters, authenticators
- Messenger handlers and transports
- Console commands
- Bundles, compiler passes, and tagged services
- Twig templates and view logic
This skill extends the base php-coder skill and applies Symfony conventions on top of the project's general PHP rules.
When to use the analysis sibling
When the task is understanding how a Symfony app boots, wires its container, routes requests, or fails at runtime — defer to project-analysis-symfony first, then return here for the edit. This skill assumes the kernel/container layout is already known.
Procedure: write Symfony code
→ First apply the php-coder skill for general PHP rules.
Then add these Symfony-specific checks:
- Confirm Symfony —
bin/console exists, composer.json lists symfony/framework-bundle.
- Confirm version —
composer.lock for the framework-bundle major. 5.x → 6.x → 7.x differs in attribute routing, voter signatures, and Messenger DSN shape.
- Inspect app structure — standard, modular (
src/Module/<Name>/), or DDD-style. Do not enforce a layout the project does not use.
- Check config layout —
config/packages/<env>/, services.yaml autowiring vs explicit bindings, config/bundles.php.
- Check test conventions — PHPUnit/Codeception; unit vs integration vs functional split.
Core Symfony principles
- Follow Symfony conventions unless the project explicitly does otherwise.
- Keep controllers thin — delegate to services.
- Rely on autowiring + autoconfigure unless the project has explicit bindings.
- Prefer attributes over annotations on 6.x+; keep annotations only if the codebase still uses them.
- Use service IDs by FQCN —
App\Service\Foo, not custom string IDs.
- Services are private by default; do not flip
public: true to make tests pass.
- Do not bypass the container with
new on classes that have collaborators.
HTTP layer rules
- Controllers:
- extend
AbstractController only when the project does
- accept
Request or a DTO; delegate business logic; return a Response variant
- Use
#[Route] attributes on 6.x+; YAML routes only where the project already does.
- Use
#[MapRequestPayload] / #[MapQueryString] (7.x+) for request DTOs when the project uses them.
- Validate via Symfony Validator on the DTO, not inline in the controller.
- Use
ParamConverter / argument resolvers for entities only when the project uses them.
Validation rules
- Symfony Validator with constraints on DTOs / entities.
- Prefer attribute constraints (
#[Assert\NotBlank], #[Assert\Email]) on 6.x+.
- Render errors from
ConstraintViolationListInterface — never compose error arrays by hand.
- Validation is declarative; do not put domain validation in entity setters.
Service layer and DI rules
- One responsibility per service; constructor injection.
- Interfaces when there are multiple implementations or the boundary is mocked.
- Tagged services for collecting implementations —
#[AutoconfigureTag] or YAML tags, never an injected array of FQCNs.
- Decorators via
#[AsDecorator] (6.1+); respect priority.
- Compiler passes only when wiring cannot be expressed via attributes/YAML.
- Do not call
Container::get in application code.
Routing rules
- Follow the existing organization — attributes on controllers, or YAML in
config/routes/.
- Route names:
<resource>_<action> (user_show, invoice_list).
#[IsGranted], #[RateLimit] at the route level, not inside the controller body.
requirements: for path parameter constraints; do not validate in the controller.
Response rules
- Match the project's response style: Twig,
JsonResponse, API Platform, or redirects with flash.
- For APIs: consistent status codes;
ConstraintViolationList → application/problem+json; DTOs / serializer groups, not raw entities.
- Do not return entities directly unless the project consistently does that.
Messenger and async work
- Messenger for async/deferred work; one message class per intent.
- Handlers:
MessageHandlerInterface (5.x) or #[AsMessageHandler] (6.x+).
- Route via
framework.messenger.routing in config/packages/messenger.yaml.
- Configure
failure_transport explicitly — without it, failed messages disappear.
- Pass IDs, not entities; the consumer re-fetches.
Events and subscribers
#[AsEventListener] (6.1+) or EventSubscriberInterface — match the project's convention.
- Past-tense event names (
UserRegistered, OrderPaid); one side-effect per subscriber.
- Respect priority on
kernel.request / kernel.response — wrong priority is a frequent bug source.
Security, voters, authorization
- One firewall per surface in
config/packages/security.yaml (main, API, admin).
- Voters for object-level permissions; never role checks in templates or controllers.
#[IsGranted] on actions; $this->isGranted() only when the result drives downstream logic.
- Stateless APIs: token-based authenticator, not form-login.
Config and environment
- Read via
ParameterBagInterface or #[Autowire(param: ...)] — never $_ENV directly.
- New env vars in
.env (+ .env.test); production values in deployment config.
- Bundle config under
config/packages/<bundle>.yaml; env overrides under config/packages/<env>/.
Doctrine and persistence
- Doctrine ORM unless the project uses DBAL/raw SQL by convention.
- Repositories for non-trivial queries; no inline QueryBuilder in controllers/services.
- N+1 awareness: fetch joins via
addSelect or EAGER when always needed.
- Transactions via
EntityManager::wrapInTransaction() for multi-write atomicity.
- Lifecycle hooks (
PreFlush, PostUpdate) — no domain logic there unless the project already does.
Migrations
- Generate via
doctrine:migrations:diff; review before commit.
- Reversible — implement
up() and down().
- One concern per migration; destructive prod changes split into expand → migrate → contract.
Twig
- Templates are dumb — presentation only; pre-computed view models from the controller/service.
- Reuse via
{% extends %} / {% include %} / macros.
- Auto-escape on;
|raw only when content is provably safe.
Bundles and compiler passes
- Bundles are for reusable, redistributable code — not "another folder".
- Compiler passes only when wiring cannot be expressed via attributes/YAML.
Console commands
#[AsCommand] (6.x+); one command class per intent; constructor injection.
- Long-running:
--limit, --time-limit, graceful SIGTERM shutdown.
- Output via
OutputInterface — never echo.
Output format
- Symfony code following framework conventions and project architecture.
- All related files (controller, service, DTO, repository, test, config) as needed.
- Schema changes — migration file plus updated entity/mapping.
Do NOT
- Business logic in controllers, entities, listeners, or Twig.
- Bypass the container with
new on classes with collaborators.
$_ENV / $_SERVER direct access — go through the parameter bag.
- Return Doctrine entities from an API endpoint — use DTOs or serializer groups.
- Silently swallow Messenger failures — route to a failure transport.
- Flip services
public: true to make tests pass — use the test container.
- Pass entities through Messenger — pass IDs.
- Mix attribute and YAML routing for the same controller surface.
Gotcha
- Autowiring fails silently when two implementations exist without explicit binding — read the error, don't just flip
public: true.
#[IsGranted] is a no-op if the controller is not a service (autoconfigure handles it by default).
- Messenger
failure_transport is opt-in; without it, failures vanish.
- Compiled container changes need
cache:clear in prod before debugging "config not applied".
- Symfony 7.x removed deprecated APIs — verify
composer.lock before assuming 6.x patterns work.
When NOT to use — components without the framework
A DEPENDENCY PROVES A LIBRARY IS AVAILABLE. ONLY THE ENTRY POINT AND THE
ROUTER PROVE WHICH APPLICATION SHAPE IS RUNNING.
`symfony/*` PACKAGES WITH NO SKELETON MARKER IS A THIRD STATE, AND THIS SKILL
DOES NOT APPLY TO IT.
Symfony ships its ORM, container, collections and HTTP layer as
independently installable packages, usable with no framework present — a
published distribution model, not one consumer's arrangement. An application
built that way has a custom entry point and a custom router. Routing it here
offers a CLI that does not exist, a request-validation primitive that is not
wired, and a routes file that was never there: every suggestion confidently
wrong, and the reason visible only from the entry point.
The probe set, and its cost. A fixed set of filesystem existence checks plus
one manifest read — no directory walk, no content scan, and never re-derived per
session. Any ONE marker present means the framework is real:
| Probe |
Meaning |
bin/console |
the console entry point the skeleton writes |
config/bundles.php |
the bundle registry |
config/services.yaml |
the DI service config |
symfony/* in composer.json with none of those markers is
components-without-the-framework. Say so and route away from this skill rather
than answering as though the framework were there.
Implemented deterministically in src/install/detect_php_shape.ts
(detectPhpShape), whose PROBE_PATHS is this table and whose verdict names
what a wrong route would have offered.
1---2name: symfony-workflow3description: Writes Symfony PHP — DI container, bundles, Doctrine, Messenger, Security voters, console commands. For Laravel / Eloquent / Artisan use `laravel`. For framework-free PHP use `php-coder`.4---56# symfony-workflow78## When to use910Use this skill for all Symfony-specific code generation and editing tasks, especially when working with:1112- Controllers (annotated / attribute-routed)13- Request listeners / event subscribers14- Services and Dependency Injection (`services.yaml`)15- Forms and validators16- Doctrine entities, repositories, and migrations17- Security: firewalls, voters, authenticators18- Messenger handlers and transports19- Console commands20- Bundles, compiler passes, and tagged services21- Twig templates and view logic2223This skill extends the base `php-coder` skill and applies Symfony conventions on top of the project's general PHP rules.2425## When to use the analysis sibling2627When the task is **understanding** how a Symfony app boots, wires its container, routes requests, or fails at runtime — defer to `project-analysis-symfony` first, then return here for the edit. This skill assumes the kernel/container layout is already known.2829## Procedure: write Symfony code3031→ **First apply the `php-coder` skill** for general PHP rules.3233Then add these **Symfony-specific** checks:34351. **Confirm Symfony** — `bin/console` exists, `composer.json` lists `symfony/framework-bundle`.362. **Confirm version** — `composer.lock` for the framework-bundle major. 5.x → 6.x → 7.x differs in attribute routing, voter signatures, and Messenger DSN shape.373. **Inspect app structure** — standard, modular (`src/Module/<Name>/`), or DDD-style. Do not enforce a layout the project does not use.384. **Check config layout** — `config/packages/<env>/`, `services.yaml` autowiring vs explicit bindings, `config/bundles.php`.395. **Check test conventions** — PHPUnit/Codeception; unit vs integration vs functional split.4041## Core Symfony principles4243- Follow Symfony conventions unless the project explicitly does otherwise.44- Keep controllers thin — delegate to services.45- Rely on autowiring + autoconfigure unless the project has explicit bindings.46- Prefer attributes over annotations on 6.x+; keep annotations only if the codebase still uses them.47- Use service IDs by FQCN — `App\Service\Foo`, not custom string IDs.48- Services are private by default; do not flip `public: true` to make tests pass.49- Do not bypass the container with `new` on classes that have collaborators.5051## HTTP layer rules5253- Controllers:54 - extend `AbstractController` only when the project does55 - accept `Request` or a DTO; delegate business logic; return a `Response` variant56- Use `#[Route]` attributes on 6.x+; YAML routes only where the project already does.57- Use `#[MapRequestPayload]` / `#[MapQueryString]` (7.x+) for request DTOs when the project uses them.58- Validate via Symfony Validator on the DTO, not inline in the controller.59- Use `ParamConverter` / argument resolvers for entities only when the project uses them.6061## Validation rules6263- Symfony Validator with constraints on DTOs / entities.64- Prefer attribute constraints (`#[Assert\NotBlank]`, `#[Assert\Email]`) on 6.x+.65- Render errors from `ConstraintViolationListInterface` — never compose error arrays by hand.66- Validation is declarative; do not put domain validation in entity setters.6768## Service layer and DI rules6970- One responsibility per service; constructor injection.71- Interfaces when there are multiple implementations or the boundary is mocked.72- Tagged services for collecting implementations — `#[AutoconfigureTag]` or YAML tags, never an injected array of FQCNs.73- Decorators via `#[AsDecorator]` (6.1+); respect priority.74- Compiler passes only when wiring cannot be expressed via attributes/YAML.75- Do not call `Container::get` in application code.7677## Routing rules7879- Follow the existing organization — attributes on controllers, or YAML in `config/routes/`.80- Route names: `<resource>_<action>` (`user_show`, `invoice_list`).81- `#[IsGranted]`, `#[RateLimit]` at the route level, not inside the controller body.82- `requirements:` for path parameter constraints; do not validate in the controller.8384## Response rules8586- Match the project's response style: Twig, `JsonResponse`, API Platform, or redirects with flash.87- For APIs: consistent status codes; `ConstraintViolationList` → `application/problem+json`; DTOs / serializer groups, not raw entities.88- Do not return entities directly unless the project consistently does that.8990## Messenger and async work9192- Messenger for async/deferred work; one message class per intent.93- Handlers: `MessageHandlerInterface` (5.x) or `#[AsMessageHandler]` (6.x+).94- Route via `framework.messenger.routing` in `config/packages/messenger.yaml`.95- Configure `failure_transport` explicitly — without it, failed messages disappear.96- Pass IDs, not entities; the consumer re-fetches.9798## Events and subscribers99100- `#[AsEventListener]` (6.1+) or `EventSubscriberInterface` — match the project's convention.101- Past-tense event names (`UserRegistered`, `OrderPaid`); one side-effect per subscriber.102- Respect priority on `kernel.request` / `kernel.response` — wrong priority is a frequent bug source.103104## Security, voters, authorization105106- One firewall per surface in `config/packages/security.yaml` (main, API, admin).107- Voters for object-level permissions; never role checks in templates or controllers.108- `#[IsGranted]` on actions; `$this->isGranted()` only when the result drives downstream logic.109- Stateless APIs: token-based authenticator, not form-login.110111## Config and environment112113- Read via `ParameterBagInterface` or `#[Autowire(param: ...)]` — never `$_ENV` directly.114- New env vars in `.env` (+ `.env.test`); production values in deployment config.115- Bundle config under `config/packages/<bundle>.yaml`; env overrides under `config/packages/<env>/`.116117## Doctrine and persistence118119- Doctrine ORM unless the project uses DBAL/raw SQL by convention.120- Repositories for non-trivial queries; no inline QueryBuilder in controllers/services.121- N+1 awareness: fetch joins via `addSelect` or `EAGER` when always needed.122- Transactions via `EntityManager::wrapInTransaction()` for multi-write atomicity.123- Lifecycle hooks (`PreFlush`, `PostUpdate`) — no domain logic there unless the project already does.124125## Migrations126127- Generate via `doctrine:migrations:diff`; review before commit.128- Reversible — implement `up()` and `down()`.129- One concern per migration; destructive prod changes split into expand → migrate → contract.130131## Twig132133- Templates are dumb — presentation only; pre-computed view models from the controller/service.134- Reuse via `{% extends %}` / `{% include %}` / macros.135- Auto-escape on; `|raw` only when content is provably safe.136137## Bundles and compiler passes138139- Bundles are for reusable, redistributable code — not "another folder".140- Compiler passes only when wiring cannot be expressed via attributes/YAML.141142## Console commands143144- `#[AsCommand]` (6.x+); one command class per intent; constructor injection.145- Long-running: `--limit`, `--time-limit`, graceful `SIGTERM` shutdown.146- Output via `OutputInterface` — never `echo`.147148## Output format1491501. Symfony code following framework conventions and project architecture.1512. All related files (controller, service, DTO, repository, test, config) as needed.1523. Schema changes — migration file plus updated entity/mapping.153154## Do NOT155156- Business logic in controllers, entities, listeners, or Twig.157- Bypass the container with `new` on classes with collaborators.158- `$_ENV` / `$_SERVER` direct access — go through the parameter bag.159- Return Doctrine entities from an API endpoint — use DTOs or serializer groups.160- Silently swallow Messenger failures — route to a failure transport.161- Flip services `public: true` to make tests pass — use the test container.162- Pass entities through Messenger — pass IDs.163- Mix attribute and YAML routing for the same controller surface.164165## Gotcha166167- Autowiring fails silently when two implementations exist without explicit binding — read the error, don't just flip `public: true`.168- `#[IsGranted]` is a no-op if the controller is not a service (autoconfigure handles it by default).169- Messenger `failure_transport` is opt-in; without it, failures vanish.170- Compiled container changes need `cache:clear` in `prod` before debugging "config not applied".171- Symfony 7.x removed deprecated APIs — verify `composer.lock` before assuming 6.x patterns work.172173## When NOT to use — components without the framework174175```176A DEPENDENCY PROVES A LIBRARY IS AVAILABLE. ONLY THE ENTRY POINT AND THE177ROUTER PROVE WHICH APPLICATION SHAPE IS RUNNING.178`symfony/*` PACKAGES WITH NO SKELETON MARKER IS A THIRD STATE, AND THIS SKILL179DOES NOT APPLY TO IT.180```181182Symfony ships its ORM, container, collections and HTTP layer as183independently installable packages, usable with no framework present — a184published distribution model, not one consumer's arrangement. An application185built that way has a **custom entry point and a custom router**. Routing it here186offers a CLI that does not exist, a request-validation primitive that is not187wired, and a routes file that was never there: every suggestion confidently188wrong, and the reason visible only from the entry point.189190**The probe set, and its cost.** A fixed set of filesystem existence checks plus191one manifest read — no directory walk, no content scan, and never re-derived per192session. Any ONE marker present means the framework is real:193194| Probe | Meaning |195|---|---|196| `bin/console` | the console entry point the skeleton writes |197| `config/bundles.php` | the bundle registry |198| `config/services.yaml` | the DI service config |199200`symfony/*` in `composer.json` with **none** of those markers is201*components-without-the-framework*. Say so and route away from this skill rather202than answering as though the framework were there.203204Implemented deterministically in `src/install/detect_php_shape.ts`205(`detectPhpShape`), whose `PROBE_PATHS` is this table and whose verdict names206what a wrong route would have offered.