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 6/5/2026
Install via CLI
openskills install event4u-app/agent-config---
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.
No comments yet. Be the first to comment!