# Coding Guide

## Conventions

- `declare(strict_types=1);` in **every** PHP file, no exceptions.
- Namespace root `Tfb\`, PSR-4, folder = namespace (`src/Services/UserService.php` → `Tfb\Services\UserService`).
- English code (classes, methods, variables, comments); Persian only in **user-facing strings** (bot replies, installer UI).
- `final class` by default. Only remove `final` if you have a real subclassing need.
- Typed properties/params/returns everywhere; avoid `mixed` except at DB-row boundaries (`fromRow(array $row)`).
- Constructor property promotion + `readonly` for value objects (Models) and simple dependencies (Repositories/Services).

## Where things live (do not violate this)

| If you're writing... | It goes in... | Never in... |
|---|---|---|
| SQL (any query) | `src/Repositories/*Repository.php` | Services, Controllers, Middleware |
| Business rule spanning >1 repo call, or needing validation/cache | `src/Services/*Service.php` | Controllers |
| Telegram command/update handling | `src/Controllers/**` | Middleware, Router |
| Cross-cutting webhook policy (auth, rate limit, ban, ...) | `src/Middleware/*Middleware.php` (implements `WebhookMiddlewareInterface`) | Router, Controllers |
| A typed view of one DB row | `src/Models/*.php` (`fromRow`/`toArray`, no logic) | anywhere else |
| A stateless helper or fixed constant list | `src/Support/*.php` | duplicated inline in multiple classes |
| Anything installer-only | `src/Install/*.php` | shared with runtime bot code |

## How to add a new feature safely (example: a new admin command)

1. Does it need new data? Add/extend a **Model** + **Repository** method first (never write ad-hoc SQL in the controller).
2. Does it need business rules (validation, multi-step orchestration)? Add/extend a **Service** that uses the repository.
3. Add a thin handler method on the right **Controller** (or a new one under `src/Controllers/Admin` or `System`) that only: parses the update, calls the service, sends a reply.
4. Wire the command into `Tfb\Bot\UpdateRouter` (`$this->commandHandlers['yourcommand'] = ...`).
5. If it's admin-only, gate it with `AdminAuthService::isAdmin()` — same pattern as `/admin` in `SystemController`.
6. Add a CLI smoke/unit test exercising the new repository/service method (see `tests/phase04_domain_smoke.php` for the pattern: pure checks first, DB-backed checks behind a `config/app.php` existence check).
7. Run `php -l` on every changed/new file, then run the smoke tests.
8. Update `docs/PROJECT_STATUS.md` and `docs/CHANGELOG.md` if the change is phase-significant.

## Comment policy

Write comments that transfer knowledge a reader can't get from the
code itself:
- **Business rules** ("why does re-answering update instead of insert?")
- **Race-safety assumptions** ("collapses onto one row via UNIQUE key, no read-modify-write race")
- **Cache/transaction/invalidation notes** ("invalidates immediately so the next read isn't stale")
- **Security-sensitive decisions** ("config admins.ids is authoritative, not the DB role column, so a compromised row can't self-promote")

Do **not** comment:
- Obvious getters (`public function id(): int { return $this->id; }`)
- Anything the method/variable name already says clearly

Every non-trivial class has a class-level docblock stating: what it's
responsible for, what layer it belongs to, and any important
dependency/boundary notes. Follow the existing style in
`src/Services/SessionService.php` or `src/Repositories/TestSessionRepository.php`
as the reference examples.

## Admin Telegram screens: escaping gotcha (Phase 6)

`Tfb\Controllers\Admin\AdminReplier` has **two** families of send
methods and picking the wrong one produces a real, easy-to-miss bug
(it happened once already — see `docs/CHANGELOG.md` Phase 6 entry):

- `reply()` / `replyWithKeyboard()` — **always HTML-escapes** the
  whole string. Use these for any message that is plain text with no
  intentional `<b>`/`<code>` formatting, even if it happens to
  interpolate admin-typed content.
- `replyHtml()` / `replyHtmlWithKeyboard()` — sends the string
  **as-is** with `parse_mode=HTML`. Use these ONLY when you need
  literal `<b>`/`<code>` tags to actually format as bold/monospace.
  Every dynamic (admin-typed) fragment you interpolate into that
  string **must** be escaped individually first via
  `Tfb\Bot\MessageBuilder::escapeHtml()` — the method itself does not
  escape anything for you.

When adding a new admin screen: if you're not building literal HTML
tags, use `reply()`. If you are, use `replyHtml()` and escape every
non-static fragment.

## Performance patch protocol

Measure the same executable webhook path before and after. Separate PHP/DB/log overhead from Telegram RTT, record API/query counts, preserve idempotency, and do not ship an optimization without improved measurements and regression proof.

## Testing expectations

- No PHPUnit dependency (project rule: zero new Composer runtime deps). Tests are plain CLI PHP scripts under `tests/`, using a `check($label, $condition, $failures)` pattern (see `tests/phase3_unit.php`, `tests/phase04_domain_smoke.php`).
- Every test file must be runnable with `php tests/<file>.php` and exit non-zero on failure.
- Prefer real assertions over "it didn't crash" — check actual return values, actual DB state.
- If a test needs a real DB and one isn't available, it must **skip explicitly and print why**, never silently report success.
- Run `php -l` on every file you touch before calling anything done.

## Do / Don't

**Do:**
- Keep methods short and named for exactly what they do (`findInProgressByUserAndTest`, not `getData`).
- Use existing constants (`Tfb\Support\AnalyticsEvents`, `StudentStates`, etc.) instead of raw strings.
- Add batch/graph repository methods when a caller would otherwise loop+query (N+1).
- Keep transactions short — wrap only the statements that must be atomic.

**Don't:**
- Don't add a Composer dependency without an extremely strong reason (project must run on bare shared hosting).
- Don't put `SELECT *` — err, actually the existing repos do use `SELECT *` for simplicity on small tables; if a table/query becomes hot or wide, switch to explicit columns (document why in a comment).
- Don't reintroduce `PDO::ATTR_PERSISTENT` or long-lived connections — one PDO per request, lazily opened.
- Don't build SQL by string concatenation with variables — always prepared statement placeholders.
- Don't log secrets (tokens, passwords, webhook secret) — see `Tfb\Logging\FileLogger` redaction list before adding new sensitive context keys.
