# Разработка

## Окружение

```
PHP        d:\laragon\bin\php\php-8.3.30-Win32-vs16-x64\php.exe
Composer   d:\laragon\bin\composer\composer.phar
MySQL      d:\laragon\bin\mysql\mysql-8.4.3-winx64\bin\mysql.exe   (root, без пароля)
Redis      d:\laragon\bin\redis\redis-x64-5.0.14.1\redis-server.exe
Node       d:\laragon\bin\nodejs\node-v22\
```

Базы: `korzillax` (разработка), `korzillax_test` (тесты).

Redis запускается вручную и **пишет `dump.rdb` в рабочий каталог процесса** —
файл в `.gitignore`, но об этом стоит помнить.

Расширения `redis` в сборке PHP нет, поэтому используется `predis/predis`
(`REDIS_CLIENT=predis`). Для продакшена нужен `phpredis` — разница в скорости
заметная, переключение в одну строку `.env`.

## Первый запуск

```bash
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed --class=DemoSitesSeeder
php artisan module:discover
php artisan serve --host=127.0.0.1 --port=8123
```

Демо-сайты создаются на доменах `medtehnika.kzla.test`, `zapchasti.kzla.test`
и `medtehnika.ru`. Прописывать их в `hosts` не обязательно — можно ходить
заголовком:

```bash
curl -H "Host: medtehnika.ru" http://127.0.0.1:8123/
```

### Как открыть админку локально

Админка живёт на `/admin` домена сайта, поэтому нужен хост, который резолвится
в 127.0.0.1. Поддомены вида `medtehnika.kzla.test` в `hosts` не прописаны,
и проще добавить сайту dev-домен `localhost`:

```bash
php artisan tinker --execute="\
Korzilla\Core\Site\Models\Site::query()->where('login','medtehnika')->firstOrFail()\
->domains()->firstOrCreate(['host'=>'localhost'],['kind'=>'dev','is_active'=>true,'redirect_to_primary'=>false]);"
```

Учётные записи админки персайтовые, общего пароля платформы нет. Владелец
заводится командой:

```bash
php artisan admin:create medtehnika owner@example.com --role=owner
```

Пароль будет запрошен скрытым вводом (не короче 10 символов); `--reset` меняет
пароль существующей учётной записи. Демо-сидер заводит владельцев сам —
`owner@{логин}.test` с паролем `korzilla-dev`.

Дальше — http://localhost:8123/admin.

### Как войти покупателем локально

Вход по звонку по умолчанию идёт драйвером `log`: звонка нет, код пишется
в `storage/logs/laravel.log`. Переключается в `Настройки → Вход покупателей`;
на боевой установке этот драйвер отказывается работать. Подробности —
[auth.md](auth.md).

## Команды

```bash
php artisan module:discover              # пересобрать кэш модулей
php artisan module:list [--tier=site]    # что найдено и включено
php artisan module:make <key> --tier=site --name="Название"

composer gate                            # deptrac + pint + phpunit + vitest
composer gate:arch                       # только архитектурный гейт
composer gate:style
composer gate:test
composer gate:js                         # тесты клиентского кода админки

php vendor/bin/phpunit tests/Feature/Layout
php vendor/bin/deptrac analyse --config-file=deptrac.yaml --report-uncovered

# Гейт производительности каталога: набивает товары и меряет страницу.
# В общий прогон не входит — набивка занимает минуты.
php artisan catalog:benchmark --products=100000 --iterations=30
```

## Гейты

Все три обязаны быть зелёными перед коммитом.

**Deptrac** — архитектурные слои. Site-модуль не может ссылаться на
platform-модуль; ядро не знает ни об одном модуле. Запускается первым: он
дешевле тестов и падает раньше.

`Uncovered` должен быть равен нулю: непокрытая зависимость означает, что
появился слой, о котором конфиг не знает.

**Pint** — стиль кода, конфигурация Laravel по умолчанию.

**PHPUnit** — тесты по реальному MySQL.

**Vitest** — клиентский код админки (`tests/js`). Требует `node` в PATH:
в терминале Laragon он есть, в голом `cmd` — нет, и тогда `composer gate`
упадёт на последнем шаге. Отдельно: `npm test`.

jsdom по умолчанию не поднимается: тестам чистых функций он не нужен. Тест
компонента объявляет окружение сам первой строкой файла:

```js
// @vitest-environment jsdom
```

Первым так сделан `tests/js/fullscreenFrame.test.js`. Замер на нём: подъём
jsdom — около трёх секунд, весь набор — шесть; прежняя оценка «45 секунд
на прогон» не подтвердилась.

⚠️ **jsdom не считает раскладку**: размеры и прокрутка в нём нули,
а `position: fixed` ни на что не влияет. Такое проверяется только
в браузере — сброс прокрутки при развороте редактора на весь экран тест
не увидел бы
([admin.md](admin.md#на-весь-экран)).

### Условия видимости проверяются одной фикстурой на два языка

`tests/fixtures/condition-cases.json` читают и `ConditionParityTest` (PHP),
и `tests/js/condition.test.js` (Vitest). Условие считается дважды — клиентом
при рисовании формы и сервером при сохранении, — и расхождение реализаций даёт
поле, которое показано, но не сохраняется. Два похожих списка случаев разошлись
бы незаметно, поэтому список один.

## Тесты

```
tests/Unit/       чистая логика без базы и HTTP
tests/Feature/    по MySQL, многие — через HTTP-запросы
```

### Почему MySQL, а не sqlite

Схема опирается на особенности MySQL 8. Тест на sqlite доказывал бы
работоспособность не той базы, на которой всё поедет в продакшене.

### Завершающий слэш в тестовых URL

`Tests\TestCase` переопределяет `prepareUrlForRequest`. Штатный харнесс Laravel
делает `trim($url, '/')` и срезает завершающий слэш — из-за этого разницу между
`/catalog` и `/catalog/` физически невозможно выразить в тесте, хотя первая
форма обязана отдавать 301 на вторую.

### Тесты, которые считают запросы

Несколько тестов проверяют, что на прогретом кэше подсистема **не ходит в базу**:

```php
DB::enableQueryLog();
DB::flushQueryLog();

$this->get('http://alpha.test/novosti/');

$routing = collect(DB::getQueryLog())
    ->filter(fn (array $q) => str_contains($q['query'], '`sections`'));

$this->assertCount(0, $routing);
```

Это не микрооптимизация: при 300–500 сайтах лишний запрос на роутинг
на каждой странице — это лишний запрос на каждой странице платформы.

## Подводные камни инструментов

⚠️ **Heredoc в Git Bash съедает обратные слэши.** `cat <<'EOF'` схлопывает
`\\` в `\`, из-за чего PHP-файл с namespace получается битым. PHP-файлы писать
инструментом Write, а не через heredoc.

⚠️ **Одинарные кавычки внутри `php -r '...'` обрывают строку bash.** Использовать
`chr(39)` или вынести скрипт в файл.

⚠️ **`php artisan db:seed --class=Namespace\Seeder`** — бэкслэши в bash
съедаются. Передавать только имя класса: `--class=DemoSitesSeeder`.

⚠️ **Redis отказывается писать при нехватке места.** `MISCONF Redis is
configured to save RDB snapshots, but it is currently not able to persist
on disk` — рабочий каталог процесса, куда он пишет `dump.rdb`, недоступен
для записи. Лечится перезапуском Redis из подходящего каталога; для разовых
консольных команд достаточно `CACHE_STORE=array SESSION_DRIVER=array`.

⚠️ **Замер каталога съедает память.** Набивка ста тысяч товаров и последующие
запросы поднимают потребление MySQL; на машине с малым файлом подкачки прогон
падает с `VirtualAlloc() failed` или `Out of memory`. Данные при этом остаются,
и повторный запуск команды идёт уже без набивки.

## Соглашения по коду

**Комментарий объясняет причину, а не действие.** «Memo привязан к номеру
версии, иначе изменение в том же запросе не видно» — полезно. «Получаем
раскладку» — шум.

**Ссылки на легаси уместны там, где решение неочевидно.** Почти каждое
нетривиальное место здесь такое, потому что в предыдущей версии оно было сделано
иначе и это привело к конкретной проблеме. Такой комментарий защищает от
«упрощения» обратно.

**Русский в комментариях и сообщениях об ошибках, английский в коде.**

**Writer владеет инвалидацией.** Всё, что меняет данные пачкой, идёт через
соответствующий writer: `Model::query()->delete()` не поднимает события Eloquent,
и наблюдатель кэш не сбросит.

**Запрос данных начинается с `site_id` из `SiteContext`**, а не из `request()`.

## Git

```
main    единственная ветка
```

Сообщение коммита: тип, узел, суть, затем — почему. Пример из истории:

```
feat(m2): зоны, блоки, 24-колоночная сетка на CSS Grid

- сетка: ширина блока едет в grid-column: span N, перенос строк делает браузер;
  разбиения по строкам в PHP больше нет
- minmax(0,1fr) вместо 1fr: иначе широкая таблица внутри блока распирает колонку
```

Репозиторий приватный: `git@github.com:korzilla-ru/korzillaX.git`, доступ по
deploy-ключу с правом записи.

**Перед пушем убедиться, что в индекс не попало лишнее:** `.env`, `vendor/`,
`storage/app/sites`, `dump.rdb`, кэш модулей.

## Отладка

**Страница отдаёт не тот сайт** → [multitenancy.md](multitenancy.md),
начать с `kz:host:{host}` в Redis.

**Страница отдаёт 404 или бесконечный 301** → [structure.md](structure.md),
проверить завершающий слэш и таблицу редиректов.

**Изменение не видно на сайте** → [cache.md](cache.md), проверить, что операция
шла через writer, а не через `query()->update()`.

**Блок не появляется** → [layout.md](layout.md), правило видимости: блок без
закрепления и без единого `allow` не виден нигде.

Полезно:

```bash
php artisan tinker --execute='dump(app(Korzilla\Core\Cache\SiteCache::class)->versions(1));'
redis-cli KEYS 'kz:1:*'
```
