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`.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add event4u-app/agent-config --skill symfony-workflow --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Symfony Workflow?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/event4u-app-symfony-workflow-agent-config)More formats (shields.io, HTML) on the badges page.
---
model_tier: medium
name: symfony-workflow
description: "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`."
domain: engineering
workspaces:
- engineering
packs:
- symfony
trust:
level: professional
install:
default: false
removable: true
---
# 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:
1. **Confirm Symfony** — `bin/console` exists, `composer.json` lists `symfony/framework-bundle`.
2. **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.
3. **Inspect app structure** — standard, modular (`src/Module/<Name>/`), or DDD-style. Do not enforce a layout the project does not use.
4. **Check config layout** — `config/packages/<env>/`, `services.yaml` autowiring vs explicit bindings, `config/bundles.php`.
5. **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
1. Symfony code following framework conventions and project architecture.
2. All related files (controller, service, DTO, repository, test, config) as needed.
3. 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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!