# 13. Standardy kodu

Reguły są krótkie i mają jeden cel: po trzech miesiącach przerwy da się wrócić do dowolnego pliku i zrozumieć go bez czytania reszty systemu.

## Nazewnictwo

| Element | Konwencja | Przykład |
|---|---|---|
| Klasy | PascalCase, po angielsku | `AppointmentService` |
| Metody i zmienne | camelCase | `findByPhone()`, `$clientId` |
| Stałe | UPPER_SNAKE | `STATUS_COMPLETED` |
| Tabele | liczba mnoga, snake_case | `appointment_services` |
| Kolumny | snake_case | `starts_at`, `is_active` |
| Pliki klas | nazwa klasy | `AppointmentService.php` |
| Widoki | kebab-case | `client-card.php` |
| Trasy | po polsku, kebab-case | `/klientki/nowa` |
| Klucze tłumaczeń | kropkowane | `clients.deleted_success` |

Kod po angielsku, interfejs po polsku. Mieszanie języków w nazwach klas i zmiennych kończy się `$klientkaRepository->findWszystkie()`.

## Struktura klasy

```php
<?php
declare(strict_types=1);

namespace Salonio\Modules\Appointments;

use Salonio\Support\Db;
use Salonio\Support\Clock;

final class AppointmentService
{
    public function __construct(
        private AppointmentRepository $repository,
        private SmsScheduler $sms,
    ) {}

    public function create(AppointmentData $data): int
    {
        $this->assertNoConflict($data->userId, $data->startsAt, $data->endsAt);
        // ...
    }
}
```

Zasady:

- `declare(strict_types=1)` w każdym pliku.
- Typy parametrów i zwracane, zawsze.
- `final` domyślnie, dziedziczenie tylko tam, gdzie jest przemyślane.
- Zależności przez konstruktor, nie przez statyczne wywołania. Wyjątek: `Auth`, `Clock`, `Tenant` i `Log`, dostępne statycznie, bo wołane wszędzie.
- Metoda dłuższa niż 40 linii to sygnał do podziału, nie zakaz.
- Bez `else` po `return` w warunku odcinającym.

## Baza danych

```php
// dobrze
$sql = 'SELECT * FROM clients WHERE salon_id = :salon_id AND phone = :phone';
$client = $this->db->one($sql, ['salon_id' => Tenant::id(), 'phone' => $phone]);

// źle, zawsze i bez wyjątków
$sql = "SELECT * FROM clients WHERE phone = '$phone'";
```

- Każde zapytanie parametryzowane.
- Każde zapytanie do tabeli z `salon_id` zawiera warunek na `salon_id`.
- Sortowanie po kolumnie z żądania idzie przez białą listę dozwolonych kolumn.
- Operacje zmieniające kilka tabel w transakcji.
- Zapytania w repozytorium, nie w kontrolerze i nie w widoku.
- `SELECT *` tylko przy pobieraniu jednego rekordu. Na listach wymieniamy kolumny.

## Widoki

```php
<div class="client-card">
    <h2><?= e($client->fullName()) ?></h2>
    <?php if (Auth::can('finance.view')): ?>
        <p class="amount"><?= Money::format($client->totalSpent) ?></p>
    <?php endif; ?>
</div>
```

- Każde wyjście przez `e()`.
- Bez zapytań do bazy w widoku.
- Bez logiki biznesowej. Warunek na uprawnienie i pętla po danych to maksimum.
- Powtarzalne fragmenty do `partials/`.

## Obsługa błędów

```php
// błąd przewidziany, użytkownik ma się dowiedzieć co zrobić
throw new ValidationException('Ten termin jest już zajęty.');

// błąd nieprzewidziany, użytkownik ma zobaczyć stronę błędu, my mamy zobaczyć ślad
throw new \RuntimeException('Nie udało się zapisać wizyty: ' . $e->getMessage(), 0, $e);
```

- Komunikaty dla użytkownika po polsku, konkretne, bez kodów.
- "Nie udało się zapisać" to zły komunikat. "Ten termin jest już zajęty przez inną wizytę" to dobry.
- Nigdy nie pokazujemy użytkownikowi treści wyjątku z bazy.
- Każdy złapany wyjątek albo jest obsłużony, albo rzucony dalej. Puste `catch` jest zakazane.

## Logowanie

```php
Log::info('sms.sent', ['message_id' => $id, 'salon_id' => Tenant::id(), 'parts' => 2]);
Log::error('stripe.webhook_failed', ['event' => $eventId, 'error' => $e->getMessage()]);
```

- Format: nazwa zdarzenia z kropką i tablica kontekstu.
- Do logu nigdy nie trafiają: hasła, klucze API, pełne treści SMS z danymi osobowymi, numery telefonów w całości.
- Poziomy: `debug` tylko lokalnie, `info` dla zdarzeń biznesowych, `warning` dla rzeczy do sprawdzenia, `error` dla awarii.
- Plik na dzień, kasowany po 90 dniach.

## JavaScript

- Bez frameworka. Kalendarz, podpowiedzi klientek i przeciąganie to jedyne miejsca z większą ilością kodu.
- Jeden plik na moduł, ładowany tylko tam, gdzie potrzebny.
- Bez `onclick` w HTML, nasłuchiwanie w pliku skryptu, zgodnie z polityką CSP.
- Żądania przez `fetch`, zawsze z tokenem CSRF w nagłówku.
- Kod działa bez JavaScriptu wszędzie, gdzie to możliwe. Formularz wysłany bez skryptu ma zadziałać.

## CSS

- Jeden plik główny plus pliki modułów.
- Zmienne CSS na kolory, odstępy i cienie, w jednym miejscu na górze.
- Nazwy klas po angielsku, w konwencji blok i element: `.appointment-card`, `.appointment-card__time`.
- Bez `!important`, poza nadpisaniem czegoś, czego nie da się inaczej.
- Mobile first: podstawowe reguły dla telefonu, rozszerzenia w `min-width`.

## Formatowanie

- Wcięcia 4 spacje, bez tabulatorów.
- Linia do 120 znaków.
- Klamra otwierająca klasy i metody w nowej linii, reszta w tej samej.
- Jedna pusta linia między metodami, dwie między sekcjami w dłuższym pliku.
- Pliki kończą się pustą linią, kodowanie UTF-8 bez BOM, końce linii LF.

## Komentarze

Komentarz odpowiada na pytanie "dlaczego", nie "co".

```php
// źle
// Pobierz klientkę po id
$client = $this->repo->find($id);

// dobrze
// Bufor liczony tylko po ostatniej usłudze, bo przy kilku usługach
// stanowisko sprząta się raz, a nie po każdej pozycji.
$buffer = end($services)->bufferAfterMin;
```

Każde odstępstwo od zasad z tej dokumentacji wymaga komentarza z uzasadnieniem. Szczególnie zapytanie bez `salon_id`.

## Testy

Nie ma pełnego pokrycia testami i nie będzie, bo przy tej skali koszt przewyższyłby korzyść. Są za to testy tam, gdzie błąd jest kosztowny i trudny do zauważenia:

| Obszar | Co testujemy |
|---|---|
| Izolacja salonów | Każdy adres z identyfikatorem cudzego rekordu zwraca 404 |
| Kolizje terminów | Nakładające się wizyty, granice przedziałów, bufory |
| Wolne terminy | Grafiki, urlopy, bufory, zmiana czasu, wyprzedzenie |
| Liczenie SMS | Długość, kodowanie, liczba części, koszt |
| Kredyty SMS | Saldo zgadza się z sumą transakcji po serii operacji |
| Webhook Stripe | Duplikaty, odwrotna kolejność, brak podpisu |
| Strefy czasowe | Konwersje w obie strony, dni zmiany czasu |
| Statystyki | Sumy zgadzają się z danymi wejściowymi |

Testy uruchamiane prostym skryptem, bez zewnętrznego narzędzia, jeśli hosting nie pozwala na composera. Każdy test ma opisową nazwę mówiącą, co sprawdza.

## Praca z paczkami ZIP

Skoro kod wędruje w paczkach, a nie przez repozytorium:

- Paczka nazywa się `salonio-X.Y.Z.zip` i zawiera pełny stan projektu, nie różnice.
- Paczka nigdy nie zawiera `config/config.php`, katalogu `storage` z danymi ani `installed.lock`.
- Na wierzchu paczki leży `AKTUALIZACJA.md` z listą kroków dla tej konkretnej wersji: co wgrać, jakie migracje uruchomić, co sprawdzić po aktualizacji.
- Paczka zawiera zaktualizowaną dokumentację. Dokumentacja i kod wędrują razem, zawsze.
- Poprzednie paczki zostają, bo są jedyną drogą wycofania wersji.
