# Модульная система

Модуль — единица расширения платформы. Всё, что не является инфраструктурой
арендатора, живёт в модулях: компоненты, типы блоков, интеграции, админские
экраны.

## Уровни модулей

| Уровень | Каталог | Судьба при выгрузке сайта |
|---|---|---|
| `site` | `modules/site/{key}/` | едет в автономную копию |
| `platform` | `modules/platform/{key}/` | физически удаляется сборщиком |

Site-модуль **не имеет права** зависеть от platform-модуля. Это проверяется
трижды: в рантайме при построении порядка загрузки, юнит-тестом реестра
и Deptrac'ом в CI. Без этого правила вырезание платформенных модулей оставило бы
битые ссылки.

Обратное разрешено: platform-модуль (например, сборщик выгрузки) может зависеть
от site-модуля.

## Структура модуля

```
modules/site/news/
  module.json                   манифест — единственный источник правды
  src/
    NewsServiceProvider.php     наследник ModuleServiceProvider
    NewsComponent.php
    Models/NewsItem.php
  database/migrations/          подхватываются автоматически
  resources/views/              namespace = ключ модуля: news::listing
  resources/lang/               переводы, namespace = ключ модуля
  routes/web.php, api.php       подключаются автоматически
  config/news.php               сливается в config('news.*')
```

## module.json

```json
{
    "key": "news",
    "name": "Новости",
    "tier": "site",
    "version": "0.1.0",
    "provider": "Korzilla\\Modules\\Site\\News\\NewsServiceProvider",
    "namespace": "Korzilla\\Modules\\Site\\News",
    "requires": [],
    "enabled": true
}
```

| Поле | Обязательно | Замечание |
|---|---|---|
| `key` | да | kebab-case, глобально уникален |
| `tier` | да | `site` или `platform` |
| `provider` | нет | без него модуль даёт только миграции и представления |
| `namespace` | нет | по умолчанию `Korzilla\Modules\{Tier}\{StudlyKey}` |
| `requires` | нет | ключи модулей; влияют на порядок загрузки |
| `enabled` | нет | выключенный модуль обнаруживается, но не грузится |

## Обнаружение и загрузка

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

Реестр читает скомпилированный кэш `bootstrap/cache/korzilla-modules.php`
и **не сканирует файловую систему в рантайме**. Кэш пересобирают при деплое
и при добавлении модуля.

Порядок загрузки — топологическая сортировка по `requires`. Реестр отказывается
работать при циклической зависимости, ссылке на несуществующий или выключенный
модуль, дубликате ключа и нарушении правила уровней.

### Резервный автозагрузчик

`ModuleAutoloader` регистрирует PSR-4 для пространств имён модулей
с низким приоритетом (`spl_autoload_register(..., prepend: false)`).

Зачем: только что созданный модуль работает **до** `composer dump-autoload`.
На прогретом продакшене выигрывает оптимизированная карта классов composer,
поэтому платы за производительность нет.

## Провайдер модуля

Наследник `ModuleServiceProvider` бесплатно получает миграции, представления,
переводы, конфиг и маршруты. Своё регистрируется в двух точках расширения:

```php
final class NewsServiceProvider extends ModuleServiceProvider
{
    protected function registerModule(): void
    {
        // фаза register(): биндинги контейнера
    }

    protected function bootModule(): void
    {
        // фаза boot(): регистрация в реестрах ядра
        $this->app->make(ComponentRegistry::class)->register(new NewsComponent);

        $templates = $this->app->make(ComponentTemplateRegistry::class);
        $templates->register(ComponentTemplate::listing('news', 'default', 'Список', 'news::listing'));
        $templates->register(ComponentTemplate::block('news', 'default', 'Список ссылок', 'news::block'));
        $templates->register(ComponentTemplate::detail('news', 'default', 'Карточка', 'news::detail'));

        // Обязательно: иначе тест инварианта упадёт, а сборщик выгрузки
        // не будет знать, что делать с таблицей.
        $this->app->make(TenantTables::class)->tenant('news_items');

        $this->app->make(SettingsSchemaRegistry::class)->register(
            SettingsSchema::make('news', 'Новости')
                ->group('listing', 'Список новостей', function (SettingGroup $g): void {
                    $g->number('news.per_page', 'Новостей на странице')->default(10);
                })
        );
    }
}
```

Порядок фаз важен: реестры компонентов наполняются в `boot()`, поэтому
встроенные типы блоков ядра тоже регистрируются в `boot()` — после модулей.

## Чек-лист нового модуля

1. `php artisan module:make my-module --tier=site --name="Моё"`
2. Миграция таблицы с колонкой `site_id`.
3. `TenantTables::tenant('my_table')` в `bootModule()`.
   У модуля `tier = platform` — `TenantTables::platform('my_table')`: его данные
   в выгрузку сайта не идут вовсе. Категория `global` для этого не годится,
   она означает «справочник, который копии нужен»
   ([почему](export.md#platform--категория-которой-не-было)).
4. Компонент (если модуль даёт тип раздела) — см. [components.md](components.md).
5. Шаблоны для нужных контекстов: список, блок, карточка.
6. Схема настроек, если у модуля есть настраиваемое поведение.
7. `composer gate` — должен быть зелёным.
8. Перед продакшеном: `composer dump-autoload -o`.

## Частые ошибки

**«Провайдер модуля не найден»** — ключ в `module.json` и физический путь
разошлись, либо не совпадает `namespace`. Проверьте `php artisan module:list`.

**Новый модуль не виден** — не пересобран кэш: `php artisan module:discover`.

**«Site-модуль зависит от platform-модуля»** — правило уровней; вынесите общий
код в site-модуль или в ядро.

**Таблица не в реестре** — `TenantTableInvariantTest` падает с перечислением
забытых таблиц.
