# Backend context

> Read `ai/shared.md` first. This file only covers backend-specific rules.

## Project Context

**Stack:** PHP 8.x, Slim 4, PHP-DI/Slim-Bridge, PHPUnit (dev), Composer.
Persistence: PostgreSQL.
**Architecture:** Domain-Driven Design. Code is organized by domain entity
under `backend/app/` (PSR-4 `App\\`) into Entities, DTOs, Repositories,
Use Cases, and Fakes (in-memory repos for tests). The entity list is
intentionally omitted here - update this section as entities land.

## Code patterns

- Look at similar entities for reference before writing anything new
- Entities: constructor with properties, getters
- DTOs: simple data containers for creation (e.g. `CreateElementDto`)
- Repositories: interfaces that define data access
  - Do not write unit tests for concrete repository implementations
    (e.g. `Postgres*Repository`). They are exercised by e2e tests.
    Use cases are tested with fake repositories.
  - Do not write reflection-only contract tests for repository interface
    method names, parameter counts, parameter names, or type hints. PHP
    interfaces already enforce those signatures; test behavior instead.
  - Repository methods that find records by a foreign key should accept
    the related entity, not a raw id (e.g. `findBySet(Set $set)`, not
    `findBySetId(int $setId)`).
- Use cases: business logic with Request objects
  - When throwing exceptions, add `@throws` docblock
- Fakes: in-memory implementations for testing
  - Live under `tests/Fakes/`
  - Find/lookup methods must return a new instance of the entity, not
    the stored reference
- Tests: follow `tests/Unit/[Entity]/UseCases/` layout
  - In `setUp`, only use fake repositories for entities under test -
    construct dependency objects directly with `new` (e.g.
    `new Set(...)`) instead of creating them through their fake
    repositories

## Migrations

- This project is not in production. By default, schema changes should update
  the relevant create-table migration so migrations describe the current desired
  schema from scratch.
- Do not add alter-table or data-backfill migrations unless the user explicitly
  asks for production-style migration safety.
- Put seed data in seeders, not migrations.

## PHP rules

- Imports: always put `use` statements at the top of the file, never use
  inline imports (e.g. `\App\Foo\Bar::class`)
- Closures: never use arrow functions (`fn () =>`) - always use regular
  anonymous functions (`function () { return ...; }`)

## Pre-commit

Run `phpcs` on worked-on directories before committing (phpcs ruleset
to be added to the repo; the Nix flake already provides
`php-codesniffer`). Run `./vendor/bin/phpunit tests` and confirm green.

## LLM anti-patterns

Constructs LLMs default to that this project forbids. Name the trap
explicitly so you catch yourself before writing it.

- Arrow function:
  - Forbidden: `fn ($node) => $node->getId()`
  - Required: `function ($node) { return $node->getId(); }`
- Inline FQCN type:
  - Forbidden: `function f(): \Psr\Http\Message\ResponseInterface`
  - Required: import `ResponseInterface`, then return `ResponseInterface`
- Inline `::class`:
  - Forbidden: `Container::get(\App\Foo\Bar::class)`
  - Required: import `Bar`, then call `Container::get(Bar::class)`
- Default param:
  - Forbidden: `function f(int $count = 10)`
  - Required: `function f(int $count)`
- Default in fake:
  - Forbidden: `public function create(Dto $dto, bool $strict = true)`
  - Required: no default; every caller passes a value
- Lookup returns stored ref:
  - Forbidden: `return $this->items[$id] ?? null;`
  - Required: rebuild a new instance with the stored fields
- FK lookup by id:
  - Forbidden: `findBySetId(int $setId)`
  - Required: `findBySet(Set $set)`
- Short variable name:
  - Forbidden: one-letter or abbreviated names (`$t`, `$n`, `$res`)
  - Required: explicit names (`$text`, `$node`, `$response`)
- Em dash:
  - Forbidden: comment or docblock containing an em dash character
  - Required: use `-`

When generating code, scan the diff for these patterns before writing it
to disk. If you catch one mid-write, rewrite that line.
