sevendays_backend
(Symfony, PHP 8.3). These are the enforceable form of the
backend coding standards and the shared
AI manifesto. Each rule has a stable ID so it can be cited in
review.
Rule IDs
Rules use category-scoped IDs:BE-<AREA>-NN, where <AREA> is the (stable, uppercase)
section code below and NN is a zero-padded number within that area — for example
BE-ASYNC-01.
- To add a rule, take the next free number in the relevant area (a second async rule is
BE-ASYNC-02), so each section grows independently and the ID always tells you the category. - IDs are stable. Never renumber an existing rule or reuse the ID of a retired one — mark a dropped rule as retired instead.
Language & tooling
- BE-LANG-01 — Add
declare(strict_types=1);at the top of every new PHP file. The codebase is mid-migration, so new code always opts in. - BE-LANG-02 — Target PHP 8.3. Use modern features: backed enums,
readonlyproperties, constructor property promotion, first-class attributes. - BE-LANG-03 — Before proposing a change as done, run the per-file diff gates:
bin/phpstan-diff.sh,bin/phpcs-diff.sh,bin/static.sh. Never claim a change passes CI without running them.
Coding style
- BE-STYLE-01 — Follow PSR-12 plus the Symfony (risky) ruleset. Do not hand-format;
let the fixer decide. Run
vendor/bin/ecs check src --fixbefore finishing. - BE-STYLE-02 — Respect the baked-in house conventions: short array syntax
[], strict comparison===, Yoda style for equality checks, ordered class elements, and no arrow functions. Do not emitfn() =>closures.
Static analysis
- BE-STATIC-01 — New files must pass PHPStan level 4 as a hard floor; aim as high as possible toward the level 8 project goal.
- BE-STATIC-02 — Never lower the PHPStan level on a file that already passes higher,
and never suppress errors with baseline entries or
@phpstan-ignoreto make a check pass. Fix the underlying issue instead.
Structure & modules
- BE-MOD-01 — Keep controllers thin. Business logic lives in services, not controllers.
- BE-MOD-02 — Put a bigger feature in its own module directory under
src/(e.g.src/Raffle), owning its ownconfig/services.yamlandconfig/packages.yaml. Followsrc/IGamingas the reference DDD layout (Domain/Application/Infrastructure). - BE-MOD-03 — Communication between modules is minimal and through a facade. Do not depend on another module’s entities, repositories, or internal services directly.
Multi-project & configuration
- BE-CONFIG-01 — Keep application code project-agnostic. Never branch on the brand:
no
if ($projectCode === '7days'), nomatch (ProjectCodeEnum::UKCC), no per-brand conditionals that fork behavior. A brand check buried in logic is a bug waiting for the other brand. - BE-CONFIG-02 — Drive per-brand differences through config or data, not control
flow. Values belong in
config/project/{project_code}_parameters.yaml(overdefault_parameters.yaml); differing implementations are swapped as services inconfig/project/{project_code}_config.yaml; operational differences are DB-driven. Inject the parameter/binding and let config select it — adding or changing a brand should be a config/data change, not a logic edit. See Projects: 7days & UKCC.
Services & DI
- BE-DI-01 — Use constructor injection and autowiring. Do not pull services from the container manually or add service-locator patterns without reason.
Doctrine, entities & migrations
- BE-DB-01 — There are four separate databases (
draw,user,ticket,event), each with its own connection and entity manager. Put a new entity in the directory matching its database/mapping prefix; thedrawEM is the default. - BE-DB-02 — Never query or join across connections/databases in a single query. Coordinate at the service layer or through a module facade.
- BE-DB-03 — Mapping is attribute-based with the underscore naming strategy.
- BE-DB-04 — Generate and apply migrations with
bin/db_diffandbin/db_migrate(all four namespaces). Do not run bared:m:migrate. - BE-DB-05 — After
bin/db_diff, review the generated migration: confirm it landed in the expected namespace and touches only that DB. Clean up auto-generated boilerplate comments, leftover@todos, and emptydown()scaffolding. - BE-DB-06 — Never edit a committed migration — create a new one. Always provide a
meaningful
getDescription().
API conventions
- BE-API-01 — Use plain Symfony controllers (no API Platform). API controllers
extend
App\Controller\Api\ApiController. - BE-API-02 — Declare routes with
#[Route]and document every endpoint with accurate Nelmio / OpenAPI attributes (#[OA\Get],#[OA\Parameter],#[OA\Response],#[Security]) — they are the published API docs. - BE-API-03 — In new code use the Symfony Serializer with
#[Groups]. JMS Serializer is legacy — only touch it in endpoints that already depend on it; never introduce it in new code. - BE-API-04 — Validate input with the Symfony Validator (constraints on request
DTOs/models in
src/App/Model) or Form types for complex requests. - BE-API-05 — Throw from the
App\Exceptionhierarchy; letApiExceptionSubscriberproduce the standard JSON shape. Do not build error responses ad hoc. - BE-API-06 — Guard endpoints with
#[IsGranted]/ voters. Session auth is the default; JWT is used by the iGaming module. - BE-API-07 — Mark cacheable endpoints with the custom
#[CachedEndpoint]attribute.
Sensitive data
- BE-SEC-01 — Store PII / secrets with the custom
encrypted_dataDoctrine type (App\Security\Crypto). Never store sensitive columns as plaintext, and never place secrets or customer data in a prompt or agent context.
Testing
- BE-TEST-02 — Cover meaningful branches and edge cases (zero, negative, empty, boundary), not just the happy path. Keep unit tests fast and free of I/O; use functional tests for HTTP/DB integration. We do not target 100% coverage — skip trivial glue.
- BE-TEST-03 — Build test state with Zenstruck Foundry factories (
tests/Factory) and Stories (tests/Story), not hand-rolled fixtures. Name test classes*Test.php, data providersprovider*, and tag with@group unit/@group integration.
Async & messaging
- BE-ASYNC-01 — Async currently runs on RabbitMQ (
OldSoundRabbitMqBundle); Symfony Messenger is not in use yet. Keep consumer/handler classes thin and framework-agnostic — put real logic in a service the consumer calls — so the planned move to Messenger is an adapter swap, not a rewrite.
Design principles & hygiene
- BE-DESIGN-01 — Aim to comply with SOLID and the relevant PHP-FIG PSRs; follow HTTP semantics (RFC 9110), RFC 9457 Problem Details for errors, and ISO 8601 / UTC for dates. Prefer composition over inheritance; avoid premature abstraction.
- BE-DESIGN-02 — Remove unnecessary comments — comment the why, not the what; delete commented-out code and generated placeholders.
- BE-DESIGN-03 — Use early returns / guard clauses over deep nesting.
- BE-DESIGN-04 — Leave it nicer than you found it: clear names, small methods, sensible shape. If a change makes a file harder to read, rethink it.
When any of these rules changes, update it here and in the
backend coding standards in the same pull
request, so the AI ruleset never drifts from the human docs.