Skip to content

04 Architecture

Mikhail Deynekin edited this page Oct 31, 2026 · 1 revision

04 · Устройство

Слои

index.php ─ Router ─ Controller ─ Service ─ Fornex API
                       │            │
                       │            ├── ZonePlanner  (правила о записях)
                       │            └── DnsResolver  (проверка)
                       │
                       └── View (оболочка SPA)     SQLite: audit_events, snapshots,
                                                          api_tokens, login_attempts

backend/bin/fornex ─ Cli\Application ─ те же Services

Веб-вход и командная строка вызывают один backend/bootstrap.php, который возвращает App с контейнером Services. Второй сборки и второй копии правил нет: пресет, применённый из интерфейса, из скрипта или из php backend/bin/fornex, идёт одним путём PresetService → ZonePlanner → ZoneService.

Ядро

Класс Ответственность
Kernel\Env читает KEY=VALUE из файла; настоящая переменная окружения важнее файла
Kernel\Config неизменяемое дерево настроек, перекрытие по карте ENV_MAP, path() считает относительные пути от корня приложения
Kernel\Request метод, путь, query, JSON-тело, bearer-токен, адрес клиента
Kernel\Response {ok:true, …} / {ok:false, error:{code, message, details}}
Kernel\Router табличные маршруты, {name} — один сегмент пути, 404 и 405 различаются
Kernel\Session сессия в cookie, таймаут бездействия, регенерация при входе
Kernel\Logger журнал с ротацией; секрет в него не попадает
Kernel\App выполняет маршрут, применяет флаги auth/csrf/write, превращает исключения в ответы

Сервисы

Сервис Ответственность
Fornex\Client HTTP к ru.fornex.com/api, браузерный User-Agent, повторы на сеть и 5xx, FornexException с кодом и сообщением
Service\ZoneService читает зону, снимает снимок перед каждой записью, выполняет добавление/правку/удаление по id
Service\ZonePlanner правила: дубли, слияние SPF, политика DMARC, форма плана (create, update, skip, conflict)
Service\PresetService пресеты, шаги мастера, поля по шагу, нормализация DKIM, stepsOverview
Service\VerifyService NS, MX и цели, SPF, DKIM по четырём селекторам, DMARC, A @/www, PTR — один отчёт
Service\NsService состояние делегирования, инструкция регистратору, смена NS для домена, заказанного в Fornex
Service\TokenService выпуск токена (префикс + хэш), проверка, отзыв; по умолчанию только чтение
Service\AuditService запись и чтение audit_events
Database\Database SQLite, миграции из backend/migrations

План

Любая запись начинается с плана:

{
  "create":   [{"host": "@", "type": "MX", "value": "SMTP.GOOGLE.COM", "prio": 1, "why": "…"}],
  "update":   [{"id": 4237354, "from": "…", "to": "…", "why": "…"}],
  "skip":     [{"id": 4237363, "reason": "уже есть"}],
  "conflict": [{"id": 4232119, "reason": "MX на локальный mail мешает Google"}]
}

preview возвращает план и ничего не пишет. apply пересчитывает план по текущей зоне и выполняет create и update; skip и conflict не применяются автоматически никогда. Снимок снимается между этими шагами, поэтому план, который видел человек, и снимок на диске описывают одну и ту же зону.

Состояния шага мастера

PresetService::stepsOverview считает статус каждого шага:

Статус Значение Что видит человек
done запись, которую пишет шаг, уже в зоне (present_if) «уже есть»
todo шаг можно применять активная кнопка
empty шагу не хватает значения в поле кнопка активна, сервер отвечает списком «чего не хватает»
blocked не выполнен шаг из requires предпросмотр выключен, назван недостающий шаг

empty намеренно не отказ: шаг DKIM остаётся empty, пока не вставлен ключ, и запрет кнопки на этом состоянии делал шаг невозможным — сначала нельзя было вставить ключ, потом получить план.

Локальное состояние

В SQLite лежит только то, чего нет в зоне: audit_events (кто, что, был ли сухой прогон, итог), snapshots (путь, sha256, число записей), api_tokens (префикс и хэш, никогда сам токен), login_attempts (ограничение попыток входа). Сама зона читается из API при каждом запросе, поэтому панель не может показать устаревшую копию.

Проверка

VerifyService спрашивает два сервера на каждый тип записи: публичный резолвер (VERIFY_RESOLVER) и авторитетный сервер Fornex (VERIFY_AUTHORITATIVE). В отчёте видно, где записи действительно нет, а где ответ просто закеширован.

Clone this wiki locally