In one line
I built the guardrails - custom static-analysis rules, layer enforcement, a 1,200+ test suite, a one-command environment, and an AI workflow that encodes the architecture - so that a five-developer team stays on a clean design they'd never worked in, without a senior policing every pull request.
The problem
Designing a clean architecture is the easy half. The hard half is keeping a five-developer team on it - on a stack (Mezzio, Doctrine, Nuxt 4, a generated SDK) none of them had worked in before, while migrating features out of four tangled legacy systems whose every shortcut is right there to copy.
Without a system around the code, three things go wrong fast: decisions lose their memory, "quality" becomes whatever a reviewer happens to notice, and the architecture erodes one shortcut at a time. So I built the guardrails before the work scaled out - to make the right way the path of least resistance, not a thing someone has to police.
Standards that enforce themselves
I refused to rely on humans remembering rules. The rules are executable.
- Static analysis at the gate. PHPStan (level 5) and Psalm run in CI and in pre-commit hooks. ECS and Rector handle style and PHP modernization automatically, so nobody argues about formatting in review.
- Custom architecture rules. Beyond the off-the-shelf checks, I wrote
custom PHPStan and Rector rules that enforce this platform's conventions
- handlers must be complete, classes
finalby default, endpoints must carry the API annotations (including theoperationIdthe SDK pipeline depends on). The architecture polices itself instead of depending on me catching violations in review.
- handlers must be complete, classes
- Layer boundaries, enforced. Deptrac asserts the dependency direction so
a Repository can never reach up into a Handler. The modular structure I designed
can't quietly collapse:
# Deptrac: dependencies may only point downward through the layers ruleset: Handler: [Service] Service: [Repository] Repository: ~ # depends on nothing above it - A linted API contract. Spectral validates the OpenAPI spec against a custom ruleset before it can generate a client.
- A quality gate on top. Conventional-commit enforcement, a pre-push test run, and SonarQube ingesting the PHPStan/Psalm reports - so every change clears the same bar regardless of who reviewed it. In practice, 100% of commits pass through these gates; the build is the source of truth for "is this up to standard?", not a person's memory.
A test suite the team can trust
QA on the project is manual, so the automated suite is the regression safety net - it has to catch what a person otherwise would. On the backend, tests are written with Pest: unit tests for services, handlers, input filters and DTOs; integration smoke tests for the DI container, Doctrine metadata, and route wiring; and ACL tests covering every non-public route, so a permissions regression fails the build rather than shipping. The frontend is covered by Vitest unit tests (stores, composables, components) and Playwright end-to-end tests for the workflows that matter most. Well over a thousand backend test cases run as part of normal delivery, with a unit/integration split tuned for fast feedback.
One command to a running stack
The old onboarding ritual was a manual, multi-tool local setup that taught a new developer four legacy architectures before they could run anything. I replaced it with a DDEV environment that brings the whole stack up - nginx-fpm, Redis, Swagger UI, the right PHP extensions, Xdebug - in essentially one command, with automated certificate handling for the corporate proxy environment. (More on the environment shift in From XAMPP to DDEV.)
I also built a scaffolding CLI that generates a new module's boilerplate to the project's conventions - entity, repository, service, DTO, handler, and test stubs - so a new domain starts correct instead of copy-pasted from whatever was nearest. The fastest way to keep code consistent is to make the consistent version the easiest one to produce.
Teaching the machine the conventions
The most forward-looking piece: I encoded the architecture into an
AI-augmented migration workflow. Repo-local agent skills - migrate-feature,
api-endpoint-scaffold, create-service, register-route, quality-check,
code-review, publish - carry the project's own rules and drive a fixed
seven-phase flow:
1. Analyze legacy code → migration doc
2. Backend: entity, repository, service, DTO, handlers, tests
3. Regenerate the typed SDK from the OpenAPI spec
4. Middleware: register proxy routes
5. Frontend: Pinia stores, Nuxt pages, Vuetify components, tests
6. Verify: cross-layer testing + legacy-parity check
7. Publish: quality checks, PR, ticket transition
The point isn't "we use AI." It's that the conventions I designed are captured where the work happens, so an assisted migration comes out shaped like the rest of the platform instead of freestyling - and a developer reviews each phase before the next begins. In planning, that workflow was the difference between a multi-year manual estimate and a timeline roughly half as long.
What changed
The architecture is now documented in ADRs (why that habit compounds is in Architecture Decision Records), the standards are automated rather than tribal, and a five-developer team ships confidently on a stack they'd never touched - because the system catches the mistakes that used to need a senior reviewer to catch.
The throughline across everything I build: I'd rather spend a day on the rule or the tool that prevents a whole class of problems than a week cleaning up after it.