> ## Documentation Index
> Fetch the complete documentation index at: https://docs.internal.sevencanyon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coding standards

> PHP and Symfony coding conventions

Standards for our Symfony (PHP) backend.

## Language & tooling

* **PHP 8.3** (`composer.json` requires `^8.3`). Use modern language features: native
  backed enums, readonly properties, constructor promotion, first-class attributes.
* **Add `declare(strict_types=1);`** at the top of every new file. The codebase is
  mid-migration (not all legacy files have it yet), so new code should always opt in.
* Quality gates run in CI; the per-file diff scripts let you check only what you changed:
  * `bin/phpstan-diff.sh` — PHPStan on changed files
  * `bin/phpcs-diff.sh` — code style on changed files
  * `bin/static.sh` — combined static analysis

## Coding style

* Follow **PSR-12** plus the Symfony (risky) ruleset.

* Style is enforced by **PHP-CS-Fixer** (`.php-cs-fixer.dist.php`) and
  **Easy Coding Standard (ECS)**. Auto-fix before committing:

  ```bash theme={null}
  vendor/bin/ecs check src --fix
  ```

* House conventions baked into the fixer config: short array syntax (`[]`), ordered class
  elements, strict comparison (`===`), Yoda style for equality checks, and **no arrow
  functions**. Don't hand-format — let the fixer decide.

## Static analysis

* We run **PHPStan** (with `phpstan-symfony` + bleeding-edge rules) on every PR;
  config is `phpstan.neon`.
* **Newer files must pass at PHPStan level 4 as a hard minimum.** Level 8 is the project
  goal — the config targets it — so aim as high as you can and raise the level on a file as
  it's cleaned up. Never *lower* the bar on a file that already passes higher.
* Migrations, the Kernel, and third-party bundles are excluded from analysis.

## Project structure & conventions

* Keep controllers thin; put business logic in services.
* TODO: namespacing and naming conventions.

### Modules

Bigger features should live in their own module directory under `src/` (for example
`src/Raffle`), with the module owning its own configuration rather than dumping everything
into the global `config/`:

```
src/
  Raffle/
    config/
      services.yaml      # services for this module
      packages.yaml      # package config for this module
    Controller/
    Entity/
    Service/
    ...
```

Wire these module configs into the application's main config (e.g. `imports` or the
`config/services.yaml`) so each module stays self-contained.

**Communication between modules should be minimal.** When one module needs another,
prefer going through a well-defined **facade** (a small public-facing service that exposes
only what other modules need) rather than reaching into the module's internals. Keep the
rest of a module's classes internal to that module.

* Don't depend on another module's entities, repositories, or internal services directly —
  go through its facade where one makes sense.
* A facade is the seam we can keep stable while a module's internals change.

The codebase already follows this shape: alongside `src/App` there are top-level modules
such as `src/IGaming`, `src/Rng`, and `src/Brdge`, each with its own
`config/services.yaml`. `src/IGaming` is a reference for a DDD module — it separates
`Domain` / `Application` / `Infrastructure`. New modules should follow this layout.

## Services & dependency injection

* Prefer constructor injection and autowiring.
* TODO: conventions for service naming, tags, and configuration.

## Doctrine: connections, entities & migrations

We run **four separate databases**, each with its own DBAL connection and ORM entity
manager:

| Connection / EM | Entities live in                                                               | Mapping prefix                          |
| --------------- | ------------------------------------------------------------------------------ | --------------------------------------- |
| `draw`          | `src/App/Entity/Draw` (+ Sonata Media on this EM)                              | `App\Entity\Draw`                       |
| `user`          | `src/App/Entity/User`, plus `CheckoutIDV` and `RecurringBilling` user entities | `App\Entity\User` (and module prefixes) |
| `ticket`        | `src/App/Entity/Ticket`                                                        | `App\Entity\Ticket`                     |
| `event`         | `src/App/Entity/Event`                                                         | `App\Entity\Event`                      |

Connection details come from `%*_database_*%` parameters and all use `pdo_mysql`,
`utf8mb4`, and a `schema_filter` of `~^(?!local_)~` (so `local_*` tables are ignored).
Custom DBAL types (`encrypted_data`, `json`, `json_array`, `json_std`) and DQL functions
(`FIRST`, `GROUP_CONCAT`, `DAYOFWEEK`, `DATE`, `JSON_CONTAINS`) are registered in
`config/packages/doctrine.yaml`.

### Entity conventions

* Put a new entity in the directory for the database it belongs to, matching the mapping
  prefix above — that determines which entity manager and connection it uses.
* Mapping is **attribute-based** with the underscore naming strategy.
* An entity belongs to exactly one connection. **Do not query across connections in a
  single query / join across databases** — coordinate at the service layer (or via a
  module facade) instead.
* When injecting an entity manager or repository, make sure you get the one for the right
  connection (the `draw` EM is the default).

### Migrations

Each database has its own migration configuration under
`config/doctrine_migrations/<db>.yaml` and its own namespace
(`DoctrineMigrations\Draw`, `\User`, `\Ticket`, `\Event`). Don't run the bare
`d:m:migrate`; use the scripts so every connection is handled:

```bash theme={null}
bin/db_diff      # generate migrations for all four DBs (one per namespace)
bin/db_migrate   # apply pending migrations across all four DBs
```

After `bin/db_diff`, **review the generated migration** for the right database/namespace
before committing — the diff is generated per connection, so confirm it landed in the
expected namespace and only touches that DB's schema.

* Never edit a committed migration; create a new one.
* **Clean up generated migrations:** remove the auto-generated boilerplate Doctrine leaves
  behind — the `// this up()/down() migration is auto-generated...` comments, leftover
  `@todo` placeholders, and any empty `down()` scaffolding — and keep only the SQL you
  actually mean to run.
* **Always provide a meaningful `getDescription()`** so the migration is self-documenting
  in the migrations list.
* Migrations use a custom factory (`App\Doctrine\Migrations\DoctrineAwareMigrationFactory`),
  so migrations can be services and use dependency injection where needed.

## API conventions

We use **plain Symfony controllers** (no API Platform). API controllers extend the shared
`App\Controller\Api\ApiController` base, which wraps serialization and JSON responses.

* **Routing & docs:** declare routes with the `#[Route]` attribute and document endpoints
  with **Nelmio / OpenAPI attributes** (`#[OA\Get]`, `#[OA\Parameter]`, `#[OA\Response]`,
  `#[Security]`). Keep the OpenAPI annotations accurate — they are the published API docs.
* **Serialization:** new modules use the **Symfony Serializer** (with serialization
  groups / `#[Groups]` to control exposed fields). **JMS Serializer is legacy** — only use
  it in existing endpoints that already depend on it, and only when there's a concrete
  reason to (e.g. matching the existing behaviour of that endpoint). Don't reach for JMS in
  new code.
* **Validation:** validate input with the Symfony Validator (constraints on request
  DTOs/models in `src/App/Model`) or Symfony Form types for complex requests.
* **Errors:** throw from the `App\Exception` hierarchy (`ApiException`, `ApiFormException`,
  `ApiDetailedException`, …); `ApiExceptionSubscriber` turns them into the standard JSON
  shape — `{"message": "...", "apiCode": "...", "quantity": ...}` (or `{"errors": {...}}`
  for form errors). Don't build error responses ad hoc.
* **Auth:** session-based authentication is the default; OAuth (HWIOAuthBundle) for social
  login; JWT (`Authorization: Bearer …`) is used by the iGaming module. Guard endpoints
  with `#[IsGranted]` / voters.
* **Caching:** mark cacheable endpoints with the custom `#[CachedEndpoint]` attribute
  (`ttl`, `includeUserContext`, `warmup`).

## Encryption of sensitive data

* Store PII / secrets with the custom `encrypted_data` Doctrine type, which encrypts on
  write and decrypts on read via the `App\Security\Crypto` service. Use it for sensitive
  columns instead of storing plaintext.

## Testing

Use **PHPUnit** for unit and functional tests.

<Warning>
  **Unit tests are required for any code that calculates something or contains
  non-trivial logic** — pricing, scoring, date/time math, business rules, state
  machines, parsers, and similar. A PR that adds or changes such logic without tests
  should not be merged.
</Warning>

We are **not** aiming for 100% coverage. Skip tests for trivial glue code (plain
getters/setters, simple DTOs, thin controllers that only delegate). When in doubt, ask:
"if this breaks silently, would we notice?" If not, test it.

* Cover the meaningful branches and edge cases (zero, negative, empty, boundary values),
  not just the happy path.
* Keep unit tests fast and free of I/O; use functional tests for HTTP/database integration.

**Conventions:**

* Tests live in `tests/` (module-scoped suites also exist, e.g. `src/IGaming/tests`,
  `src/Rng/tests`). Name test classes `*Test.php` and data-provider methods `provider*`.
* Tag tests with `@group unit` / `@group integration`.
* Build test state with **Zenstruck Foundry** factories (`tests/Factory`) and
  **Stories** (`tests/Story`) rather than hand-rolled fixtures.
* The **DAMA Doctrine Test Bundle** wraps each test in a transaction for isolation — don't
  rely on leftover data between tests.

<Note>
  Historically the suite was weak ("there are no working tests" in the old README). The
  testing requirement above is a deliberate change of direction — new logic ships with
  tests from now on.
</Note>

## Async & messaging

* Asynchronous work currently runs through **RabbitMQ** via `OldSoundRabbitMqBundle`
  (producers publish, consumer classes handle messages). We do **not** use Symfony
  Messenger yet.
* Run consumers with the `bin/run_consumers_*` scripts; scheduled work runs through the
  cron runner (`php bin/console app:cron:run`).
* For high-volume work, prefer batch consumers over one-message-at-a-time processing.

<Note>
  **Direction of travel:** we plan to migrate async messaging to **Symfony Messenger**.
  For new async work, keep handlers thin and framework-agnostic (put the real logic in a
  service the consumer just calls) so the move to Messenger is a thin adapter swap rather
  than a rewrite.
</Note>

## Design principles & good practices

Aim to comply with **SOLID** principles and established standards rather than reinventing
conventions. These are guidelines, not dogma — favor readable, maintainable code.

* **[SOLID](https://en.wikipedia.org/wiki/SOLID)** — single responsibility, open/closed,
  Liskov substitution, interface segregation, dependency inversion.
* **[PHP-FIG PSRs](https://www.php-fig.org/psr/)** — follow the relevant standards,
  especially [PSR-1](https://www.php-fig.org/psr/psr-1/) / [PSR-12](https://www.php-fig.org/psr/psr-12/)
  (coding style), [PSR-4](https://www.php-fig.org/psr/psr-4/) (autoloading), and
  [PSR-3](https://www.php-fig.org/psr/psr-3/) (logging).
* **HTTP semantics** — follow [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html)
  for methods and status codes, and return errors as
  [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html).
* **Dates & times** — serialize and store using
  [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html); store in UTC.
* Prefer composition over inheritance, keep functions small and single-purpose, and avoid
  premature abstraction (YAGNI / DRY in moderation).

### Code hygiene

* **Remove unnecessary comments.** Code should be self-explanatory; comment the *why*, not
  the *what*. Delete commented-out code and auto-generated placeholder comments.
* **Use early returns / guard clauses** instead of deep nesting — handle edge cases up
  front and let the happy path read top to bottom.
* **Leave it nicer than you found it.** Aim for clean, readable code: clear names, small
  methods, and a sensible shape. If a change makes a file harder to read, rethink it.
