Symfony is my default framework because it is defaults-driven and conventional. It is one of the two major PHP frameworks, alongside Laravel. I have used Laravel, but for this kind of work it adds opinions I do not need. A lot of Laravel's plumbing already sits on Symfony components, while Laravel also brings its own templating engine and database layer. For this project I would rather use Twig and Doctrine directly, because they are established outside one application framework and used across a wider PHP ecosystem.
Symfony publishes patch releases regularly, minor releases every six months, and major releases every two years. Its LTS branches have years of bug-fix and security-fix coverage, and that release governance has been public operating practice for more than a decade. That matters for clients because we regularly meet systems that still receive security patches but have no credible path to the next architectural version.
Frameworks Set The Abstraction Level
In a Symfony slice prompt, I do not have to specify how routing, Twig templates, form types, validation, Doctrine repositories, services, console commands, security voters, and functional tests are wired. Those defaults are already in the framework, so the generated diff is about application behaviour instead of route glue and service registration.
This matters because model output does not reliably preserve last week's correction unless the correction becomes an artifact. If a convention lives only in my head, or only in the shape of nearby files, a new run may follow it, or it may produce a plausible variation. If the convention lives in a skill and the staged diff is checked against that skill, the commit can be blocked until the diff matches the rule.
The Skill Is A Pattern Index
Some of the rules in the /symfony skill are ordinary framework discipline. State-changing forms use CSRF. Filter forms are the exception because they are read-only. Migrations have predictable structure. Dependencies come from a defined project standard. Datetime fields are named created, updated, and submitted, not whatever the agent happens to infer from another codebase. Pagination lives in the path, not as a loose query-string habit.
In controllers, I standardise on injecting RequestStack, so that choice is not remade in every slice. Symfony allows action-level Request arguments as well, but using both patterns means every controller leaves another choice in the prompt, and every controller that mixes both patterns is one more diff I have to read twice.
The skill is organised as a pattern index, so each slice pulls only the narrow convention it needs. A filtered list maps to the filter/search-list pattern. A form maps to the form and CSRF pattern. A repository maps to the repository-security pattern.
Repository Security Is The Centrepiece
The most important local convention is the multi-tenant security layer. I have published the same pattern as mhpdigital/cross-tenant-security-bundle, a Symfony bundle for row-level tenant isolation in Doctrine repositories. In this project, row-level access lives in repositories, not in controllers. Controllers do not pass $user into repository methods. The repository's createQueryBuilder() applies the security filter. Route parameters that resolve entities are fetched through the same security-aware path. Reference tables that are truly open use OpenAccessRepository; business data does not get marked open just because several roles need to read it.
After createQueryBuilder() has added its filter, andWhere() is safe because it adds another condition; where() can replace the existing condition, which makes a one-word choice security-relevant. A diff that swaps andWhere() for where() reads as a style change and removes the filter protecting the query.
The security layer also has a narrow circumvention method. It needs one, because real exceptions exist: password-reset token lookups, unauthenticated public queries, test assertions, background jobs, or other deliberately unrestricted paths. The problem is that LLM-generated patches often contain escape-hatch calls. The usual failure is a false exception classification, based on an assumption about the surrounding workflow that turns out not to be true. Putting more warnings in a planning document has not fixed that reliably; the same wrong bypass can still appear once the slice is being implemented.
The Hook Is The Enforcement Layer
The pre-commit hook takes the staged diff and reviews it against the Symfony rules. In the current version, Claude is the reviewing model.
A broad "review this for security" instruction produces an open-ended review loop, so the checklist is narrow: Symfony deprecations, form-security flows, repository bypasses, the RequestStack convention, hardcoded credentials, XSS, SQL injection, and specific project rules. When JavaScript and controller files are both staged, the hook can require a flow trace from template to JavaScript to HTTP to controller to side effect.
The output contract has three states: NO ISSUES FOUND passes. ISSUES FOUND: blocks the commit. CRITICAL SECURITY ISSUE: blocks the commit and prints the security reason in the hook output.
The Useful Failure Mode
A generated diff calls the bypass method with a comment classifying it as a special case. The checklist forces the narrower classification: password reset, public unauthenticated access, a test assertion, a background job, or something else on the approved list. The blocked commit is regenerated using the normal secured repository path, and the hook catches the problem at commit time, where there is actual code to inspect.
Why This Sits Under The Lifecycle
The previous article described the artifact chain around the work: PLAN.md, SCAFFOLD.md, claude-progress.md, review environments, and audit records, so a handoff can be read without recovering hidden context from a previous run.
If every slice repeats the same framework instructions, the planning documents become noisy. A human still has to decide whether a repository really belongs in OpenAccessRepository, whether a new bypass is justified, and whether the tenancy model itself is correct.
