# Coding Standards — A&B Hub

## PHP style

- **PSR-12**, enforced via `composer cs-check` (PHP_CodeSniffer,
  configured in `composer.json`'s `require-dev`).
- `declare(strict_types=1);` at the top of every PHP file, no
  exceptions — this catches an entire category of bugs (a string
  silently coerced where an int was expected) at the point they
  happen instead of three function calls later.
- One class per file, filename matches class name exactly (PSR-4).
- Final classes by default (`final class LeadController`) unless a
  class is specifically designed to be extended — `BaseModel` and
  `BaseController` are `abstract`, everything that extends them is
  `final` unless there's a concrete reason otherwise.
- Type-hint everything: parameters, return types, property types. A
  method signature should never require reading its body to know what
  it accepts or returns.
- Prefer `match` over `switch` for anything that returns a value —
  it's exhaustive-by-default (throws on an unmatched case instead of
  silently falling through) and has no `break;` to forget.

## Naming

| Thing | Convention | Example |
|---|---|---|
| Class | `PascalCase` | `LeadController`, `BaseModel` |
| Method / function / variable | `camelCase` | `findByStage()`, `$estimatedValue` |
| Database table | `snake_case`, plural | `leads`, `customer_contacts` |
| Database column | `snake_case` | `business_line_id`, `created_at` |
| Config key | `snake_case`, dot-notation access | `config('security.login_rate_limit.max_attempts')` |
| Route permission slug | `module.action` | `leads_customers.create` |
| Route middleware with a parameter | `name:param` | `permission:leads_customers.view`, `throttle:strict` |

## Where things go (quick reference)

- Business logic that spans more than one model → `app/services`, not
  a fat controller method and not a static helper on the model.
- A one-off, stateless utility with no business meaning → `app/helpers`.
- Anything that needs to run before/after a route's controller for
  *every* route it's attached to (not just one) → `app/middleware`.
- A new database table → add it to `database/schema.sql` in the
  relevant section (or a new `-- SECTION N` block), then run
  `php database/build_migrations.php` to regenerate every file under
  `database/migrations/` from it. Never hand-edit a generated migration
  file directly — the next regeneration would silently overwrite it.

## Validation

Every piece of user input that reaches a database write goes through
`App\Core\Validator` first — never trust `$request->input(...)` to be
used directly in an `INSERT`/`UPDATE`. Validation rules live with the
controller action that needs them (or a small private method on that
controller if the same rules are reused across create/update), not in
a separate "form request" class — for a codebase this size, that
indirection costs more readability than it buys.

```php
$data = $this->validateOrFail($request, [
    'email' => 'required|email|max:191|unique:users,email',
    'first_name' => 'required|string|max:100',
]);
```

## Output escaping

`e($value)` (in `app/helpers/functions.php`) is the **only** approved
way to print user-supplied or database-sourced text into HTML. Never
`echo $value` directly, never `<?= $value ?>` without wrapping it —
grep the codebase for bare `<?=` outside of `e(...)`, `asset(...)`,
`base_url(...)`, `csrf_field()`/`csrf_token()` calls (which are
themselves either already-escaped or generate known-safe markup) as
part of any code review.

## Security rules (non-negotiable)

1. **SQL**: PDO prepared statements only, `ATTR_EMULATE_PREPARES =>
   false`. No string-concatenated SQL, anywhere, ever — including
   "just this one report query."
2. **Passwords**: `password_hash()` with the algorithm/cost from
   `config('security.password')` (Argon2id if the extension is
   available, bcrypt cost 12 otherwise). Never store, log, or include
   a plaintext password in an exception message — see how
   `AuthService::attempt()` and `rehashIfNeeded()` pass the plaintext
   password only as far as it needs to go and never persist it
   anywhere but the resulting hash.
3. **CSRF**: every `POST`/`PUT`/`PATCH`/`DELETE` route carries the
   `csrf` middleware. Forms use `csrf_field()`; JS `fetch()` calls read
   the token from `<meta name="csrf-token">` (rendered by
   `Csrf::metaTag()` in the layout) and send it as an `X-CSRF-Token`
   header.
4. **XSS**: see "Output escaping" above. Additionally, every
   `Response::send()` call sets `X-Content-Type-Options: nosniff`,
   `X-Frame-Options: DENY`, and a `Referrer-Policy` — this happens once,
   centrally, so no individual controller can forget it.
5. **Sessions**: `HttpOnly`, `SameSite=Lax`, `Secure` outside local
   dev, `session.use_strict_mode` on, session ID regenerated on every
   login (`Session::regenerate()`, called from `Auth::login()`) to
   defeat session fixation.
6. **Rate limiting**: login attempts are rate-limited per
   email-or-IP (`config('security.login_rate_limit')`); any other
   endpoint that needs it gets the `throttle:*` middleware.
7. **File uploads**: real MIME sniffing (`finfo`) against an allow-
   list — never trust the client-supplied `Content-Type` or the
   filename's extension. Stored under `/uploads` with a random
   filename; the original name is kept only as *data* (the
   `attachments.original_name` column), never used to build a
   filesystem path.
8. **Secrets**: `.env` is git-ignored, never committed, and blocked
   from direct HTTP access by both the root `.htaccess` (deny-all) and
   `public/.htaccess`'s explicit `FilesMatch` rule. `APP_KEY` is
   generated per-environment via `bin/generate_key.php`, never reused
   across local/staging/production.

## Testing (foundation for Phase 2+)

`composer.json` already declares `phpunit/phpunit` under
`require-dev` and a `tests/` PSR-4 autoload root
(`Tests\\` → `tests/`), so the very first feature module built in
Phase 2 can add `tests/Unit/...` and `tests/Feature/...` without any
setup step first. No tests were written in Phase 1 itself, since there
is no business logic yet to test beyond what was already exercised
directly against a live database during development (see
`docs/ARCHITECTURE.md` for what that covered).
