134 KiB
XC_VM — Архитектурный план реорганизации /src
Содержание
- Стратегические цели
- Диагноз текущего состояния
- Архитектурный стиль и принципы
- Целевая структура
/src - Описание компонентов
- Карта миграции: откуда → куда
- Система модулей
- Границы ядра и модулей
- Варианты сборки: MAIN vs LoadBalancer
- Порядок миграции
- Транзакции и производительность
- Стратегия миграции по рискам
- Правила для контрибьюторов
0. Стратегические цели
0.1. Зачем это всё
| # | Цель | Приоритет | Метрика успеха |
|---|---|---|---|
| 1 | Контрибьюторская доступность | 🔴 Критический | PHP-разработчик среднего уровня делает первый PR за 2 часа, не зная DDD |
| 2 | Поддерживаемость | 🔴 Критический | Типичное изменение (добавить поток, изменить лимиты) < 1 час без риска сайд-эффектов |
| 3 | Изоляция отказов | 🔴 Критический | Баг в admin-панели не ломает стриминг. Баг в модуле не ломает ядро |
| 4 | Multi-node | 🟡 Важный | LB-сервер собирается из подмножества koda. Streaming-путь не загружает admin-логику |
| 5 | Open-core коммерция | 🟡 Важный | Коммерческие модули подключаются без модификации ядра. Удаление модуля не ломает систему |
| 6 | Тестируемость | 🟢 Поддерживающий | Любой сервис можно тестировать, подставив mock зависимости |
0.2. Чем это НЕ является
- Не переписывание с нуля. Итеративный рефакторинг. Каждый шаг обратимо совместим.
- Не DDD. Нет Event Sourcing, CQRS, Aggregate Root, Domain Events. Простой паттерн: Controller → Service → Repository.
- Не академический проект. Архитектура оптимизирована под понятность и предсказуемость, а не под теоретическую чистоту.
0.3. Принцип принятия решений
Каждое архитектурное решение проходит три фильтра:
- Контрибьютор поймёт за 5 минут? → если нет — упростить
- Не ломает streaming hot path? → если ломает — отклонить
- Можно изолировать в модуль? → если нет — обосновать
Если решение улучшает «красоту кода», но повышает барьер входа — отклонить.
1. Диагноз текущего состояния
Масштаб: 382 PHP-файла, ~199 000 строк
Критические проблемы
| # | Проблема | Где проявляется | Влияние |
|---|---|---|---|
| 1 | God-объекты | CoreUtilities (4847 стр.), admin_api.php (6981 стр.), admin.php (4448 стр.) |
Невозможно изменить одну подсистему без риска сломать другую |
| 2 | Дублирование bootstrap | www/constants.php vs www/stream/init.php — одни и те же ~70 define(), функции ошибок, flood-check |
Каждое изменение нужно делать в двух местах |
| 3 | Fork-дублирование | CoreUtilities vs StreamingUtilities — идентичные свойства, init(), cleanGlobals() |
Баги исправляются в одном классе и остаются в другом |
| 4 | Глобальные переменные как шина данных | global $db, $rSettings, $rServers, $rUserInfo в каждом файле |
Невозможно тестировать, невозможно изолировать |
| 5 | SQL в presentation-слое | Каждая admin-страница делает $db->query() прямо в HTML |
Бизнес-логика неразделима от отображения |
| 6 | Один include = побочные эффекты | require admin.php запускает сессию, создаёт БД, init 3 классов, определяет 50 констант |
Нет возможности подключить часть системы без всей |
| 7 | Admin = Reseller copy-paste | reseller/ — упрощённый клон admin/ с тем же header/footer/session |
Правки в одном не попадают в другой |
| 8 | Goto-лейблы | includes/cli/monitor.php содержит goto label235, label592 |
Следы обфускации, нечитаемый control flow |
| 9 | Inline data | Массивы стран/MAC-типов/разрешений по 150+ строк прямо в admin.php |
Данные переплетены с логикой инициализации |
| 10 | God-cron | crons/servers.php — мониторинг, перезапуск демонов, статистика, очистка |
Разные по частоте и природе задачи в одном файле |
Граф зависимостей (текущий)
admin/*.php ──────┐
reseller/*.php ───┤
crons/*.php ──────┤──→ includes/admin.php ──→ CoreUtilities (4847)
includes/api/ ────┘ │ ├── Database
│ ├── Redis
├──→ admin_api.php (6981)
├──→ reseller_api.php (1204)
└──→ constants.php (~70 define())
www/stream/*.php ──→ www/stream/init.php ──→ StreamingUtilities (1992)
│ ├── Database (тот же)
│ └── Redis (тот же)
└── constants (ДУБЛИКАТ)
Центральная точка отказа: includes/admin.php — каждый контекст исполнения (admin, reseller, crons, API) проходит через этот один файл 4448 строк.
2. Архитектурный стиль и принципы
2.0. Формализация архитектурного стиля
Это: структурированный монолит с предсказуемой организацией по контексту.
Не DDD. Не Hexagonal. Не Clean Architecture. Не микросервисы. Простой PHP-монолит, разложенный по контекстам с минимумом абстракций.
Паттерн: Controller → Service → Repository
Каждый контекст (Streams, VOD, Lines, Users...) внутри устроен одинаково:
domain/Stream/
├── StreamService.php # Бизнес-логика + оркестрация
├── StreamRepository.php # SQL-запросы (SELECT/INSERT/UPDATE/DELETE)
└── StreamProcess.php # Специализированные операции (ffmpeg, kill)
interfaces/Http/Controllers/Admin/
└── StreamController.php # Принять запрос → вызвать Service → отдать ответ
Вот и всё. Три файла на контекст. Контрибьютор видит /Stream/ — и знает где менять.
Правила зависимостей (просто)
Controller → Service → Repository → Database
↓
Infrastructure (nginx, redis, ffmpeg)
| Слой | Можно зависеть от | НЕЛЬЗЯ зависеть от |
|---|---|---|
interfaces/ |
domain/ (Service + Repository), core/ |
streaming/, modules/ напрямую |
domain/ |
core/ (Database, Cache, Events) |
interfaces/, streaming/, modules/, infrastructure/ |
core/ |
Только другие core/ подкаталоги |
Всё остальное |
streaming/ |
core/ (subset), domain/ (read-only queries) |
interfaces/, modules/ |
modules/ |
domain/ (Service, Repository), core/ |
Другие модули (без явной зависимости), interfaces/, streaming/ |
2.1. Инверсия зависимостей
Код верхнего уровня (UI, CLI, HTTP endpoints) зависит от абстракций ядра, а не от конкретных реализаций. Ядро не знает о существовании модулей.
2.2. Единая точка bootstrap — множественные контексты
Вместо дублированных init.php / constants.php / stream/init.php — один bootstrap с конфигурируемым набором загружаемых сервисов.
2.3. Strict Constructor Injection — запрет Service Locator
Правило: ServiceContainer используется ТОЛЬКО на этапе bootstrap (composition root).
После bootstrap все зависимости передаются через конструктор. Ни один сервис не вызывает $container->get() внутри своих методов.
// ✅ ПРАВИЛЬНО — constructor injection:
class StreamService {
public function __construct(
private StreamRepository $repository,
private EventDispatcher $events,
private FileLogger $logger
) {}
public function create(array $data): int {
// Использует $this->repository, $this->events, $this->logger
// НЕ вызывает ServiceContainer
}
}
// ✅ ПРАВИЛЬНО — composition root (bootstrap.php):
$container->register('stream.service', function($c) {
return new StreamService(
$c->get('stream.repository'),
$c->get('event.dispatcher'),
$c->get('logger.file')
);
});
// ❌ ЗАПРЕЩЕНО — Service Locator внутри сервиса:
class StreamService {
public function create(array $data): int {
$db = ServiceContainer::getInstance()->get('db'); // ← АНТИПАТТЕРН
$logger = ServiceContainer::getInstance()->get('logger'); // ← АНТИПАТТЕРН
}
}
Исключения (временные, фаза миграции):
- Legacy-код в
includes/может обращаться к контейнеру напрямую через proxy-методы - Каждое такое обращение помечается
// @legacy-container — убрать в Фазе 8 - В Фазе 8 все legacy-обращения к контейнеру заменяются на constructor injection
2.4. Консолидация мелких классов — запрет «один класс = один файл» вслепую
Правило: Не создавать отдельный файл/класс, если в нём < 150 строк и < 5 публичных методов. Маленькие классы живут в одном файле с родственным классом.
Зачем: Избежать взрыва файлов с классами по 1-2 функции. 44 файла по 40 строк — хуже одного бога на 4000, но не намного лучше для навигации.
Конкретные правила:
| Ситуация | Что делать | Пример |
|---|---|---|
| Repository < 5 методов | Объединить Service + Repository в один файл | EpgService.php содержит и бизнес-логику, и SQL |
| Контекст = 1 Service с 1-2 методами | Влить в ближайший родственный контекст | TicketService::submit() → UserService::submitTicket() |
| Отдельный «Validator» с 1 методом | Сделать private-методом в Service | HMACValidator::validate() → HMACService::validate() |
| Отдельный «Sync» с 1 методом | Влить в Service того же контекста | DeviceSync::sync() → MagService::syncLineDevices() |
| Service + Repository > 300 строк суммарно | Разделить в отдельные файлы | StreamService (700 стр.) + StreamRepository (400 стр.) = раздельно |
Когда МОЖНО создавать отдельный файл:
- Класс > 150 строк
- Класс имеет ≥ 5 публичных методов
- Класс используется из 3+ разных контекстов (например
ConnectionTracker) - Интерфейс для DI (
CacheInterface,LoggerInterface)
// ✅ ПРАВИЛЬНО — маленькие Service+Repository в одном файле:
// domain/Epg/EpgService.php
class EpgRepository {
public function __construct(private Database $db) {}
public function findById(int $id): ?array { ... }
public function getStreamEpg(int $streamId): array { ... }
}
class EpgService {
public function __construct(private EpgRepository $repo) {}
public function process(array $data): int { ... }
public function getChannelEpg(int $channelId): array { ... }
}
// ❌ НЕПРАВИЛЬНО — два файла по 40 строк:
// domain/Epg/EpgRepository.php (40 строк, 3 метода)
// domain/Epg/EpgService.php (50 строк, 2 метода)
2.5. Границы через интерфейсы, а не через global
Каждый сервис получает зависимости через конструктор. Ни один компонент не использует global.
2.6. Модуль = изолированная директория
Модуль — это директория с известным контрактом. Его можно удалить, и система продолжит работать (деградируя в функциональности, но не падая).
2.7. Open-core без лицензионных ограничений в ядре
Ядро (core/) полностью свободно. Модули (modules/) могут быть как open-source, так и коммерческими. Ядро не содержит проверок лицензий, шифрования, или скрытых ограничений. Лицензирование — это задача отдельного опционального модуля расширений.
3. Целевая структура /src
src/
├── bootstrap.php # Единый bootstrap: require_once подключения, DI container, config
├── constants.php # Все path/version/status константы (один файл)
│
├── core/ # ═══ ЯДРО (стабильный, свободный, минимальный) ═══
│ ├── Config/
│ │ ├── ConfigLoader.php # .ini → массив, кэширование, env-override
│ │ └── PathResolver.php # Все пути системы (заменяет 70 define())
│ │
│ ├── Database/
│ │ ├── Database.php # Базовая PDO-обёртка (перемещён из includes/)
│ │ ├── DatabaseHandler.php # Менеджер БД — расширенная обёртка с reconnect, bulk ops
│ │ ├── QueryBuilder.php # Конструктор запросов вместо raw SQL в UI
│ │ └── Migration.php # Миграции (извлечено из `status`)
│ │
│ ├── Cache/
│ │ ├── CacheInterface.php # Контракт для любого кэш-драйвера
│ │ ├── FileCache.php # Текущий igbinary file cache
│ │ └── RedisCache.php # Redis-обёртка
│ │
│ ├── Auth/
│ │ ├── SessionManager.php # Единый менеджер сессий (admin + reseller)
│ │ ├── Authenticator.php # Логин/пароль/api_key
│ │ └── Authorization.php # Проверка прав ($rPermissions → RBAC)
│ │
│ ├── Http/
│ │ ├── Request.php # Обёртка над $_GET/$_POST (заменяет cleanGlobals)
│ │ ├── Response.php # JSON/HTML ответ
│ │ ├── Router.php # Маршрутизация вместо switch($rAction)
│ │ └── Middleware/
│ │ ├── FloodProtection.php # Rate limiting (извлечено из constants.php)
│ │ ├── IpWhitelist.php # IP-фильтрация
│ │ └── CorsMiddleware.php
│ │
│ ├── Process/
│ │ ├── ProcessManager.php # Управление процессами (kill, ps, isRunning)
│ │ ├── DaemonRunner.php # Запуск PHP-демонов с PID-файлами
│ │ └── CronLock.php # Файловые блокировки кронов (checkCron)
│ │
│ ├── Logging/
│ │ ├── LoggerInterface.php # Контракт
│ │ ├── FileLogger.php # Файловое логирование
│ │ └── DatabaseLogger.php # panel_logs / login_logs / activity
│ │
│ ├── Events/
│ │ ├── EventDispatcher.php # Простой event bus
│ │ └── EventInterface.php # Контракт события
│ │
│ ├── Container/
│ │ └── ServiceContainer.php # Минимальный DI-контейнер
│ │
│ └── Util/
│ ├── GeoIP.php # Извлечено из CoreUtilities
│ ├── NetworkUtils.php # IP-операции, CIDR, subnet matching
│ ├── TimeUtils.php # secondsToTime(), timezone helper
│ ├── Encryption.php # AES, token generation
│ └── ImageUtils.php # Resize, thumbnail, upload
│
├── domain/ # ═══ БИЗНЕС-ЛОГИКА (сервисы и репозитории) ═══
│ ├── Stream/
│ │ ├── StreamService.php # Бизнес-логика + оркестрация
│ │ ├── StreamRepository.php # SQL-запросы (из admin_api.php)
│ │ ├── ChannelService.php # Каналы: create, massEdit, order
│ │ ├── CategoryService.php # Категории + CategoryRepository (< 150 стр. = один файл)
│ │ ├── StreamProcess.php # FFmpeg, kill, restart
│ │ ├── StreamMonitor.php # Мониторинг потока (из cli/monitor.php)
│ │ ├── ConnectionTracker.php # Redis: подключения, heartbeat
│ │ ├── StreamSorter.php # Сортировки и форматирование
│ │ ├── PlaylistGenerator.php # Генерация M3U/EPG плейлистов
│ │ ├── CronGenerator.php # Генерация кронов
│ │ └── M3UParser.php # Парсинг M3U
│ │
│ ├── Vod/
│ │ ├── MovieService.php # + MovieRepository (< 300 стр. суммарно = один файл)
│ │ ├── SeriesService.php # + SeriesRepository (< 300 стр. суммарно = один файл)
│ │ └── EpisodeService.php # process, massEdit, massDelete
│ │
│ ├── Line/
│ │ ├── LineService.php # + LineRepository (раздельно — > 300 стр. суммарно)
│ │ ├── LineRepository.php
│ │ └── PackageService.php # + PackageRepository (один файл, < 150 стр.)
│ │
│ ├── Device/
│ │ ├── MagService.php # + DeviceSync::syncLineDevices (влито)
│ │ └── EnigmaService.php
│ │
│ ├── User/
│ │ ├── UserService.php # + ProfileService::editAdminProfile (влито)
│ │ │ # + TicketService::submit (влито)
│ │ ├── UserRepository.php # getUserInfo, getUser, getRegisteredUsers...
│ │ └── GroupService.php # + GroupRepository (один файл, < 150 стр.)
│ │
│ ├── Server/
│ │ ├── ServerRepository.php
│ │ ├── ServerService.php # Мониторинг, health-check
│ │ └── SettingsService.php # edit, editBackup, editCacheCron (влито из domain/Settings/)
│ │
│ ├── Bouquet/
│ │ └── BouquetService.php # + BouquetRepository + BouquetMapper (один файл, < 300 стр.)
│ │
│ ├── Epg/
│ │ └── EpgService.php # + EpgRepository (один файл, < 300 стр.)
│ │
│ ├── Auth/
│ │ ├── AuthService.php # CodeService + HMACService + HMACValidator (объединены)
│ │ └── AuthRepository.php # CodeRepository + HMACRepository (объединены)
│ │
│ └── Security/
│ └── BlocklistService.php # + BlocklistRepository (один файл, < 300 стр.)
│
├── streaming/ # ═══ СТРИМИНГ-ДВИЖОК (hot path) ═══
│ ├── StreamingBootstrap.php # Лёгкий init для стриминг-контекста
│ ├── Auth/
│ │ ├── TokenAuth.php # HMAC/token парсинг (из auth.php)
│ │ ├── StreamAuth.php # Проверка доступа к потоку
│ │ └── DeviceLock.php # Привязка к устройству
│ │
│ ├── Delivery/
│ │ ├── LiveDelivery.php # Раздача live (из live.php)
│ │ ├── VodDelivery.php # Раздача VOD (из stream/vod.php)
│ │ ├── TimeshiftDelivery.php # Timeshift
│ │ └── SegmentReader.php # Чтение TS-сегментов
│ │
│ ├── Balancer/
│ │ └── LoadBalancer.php # Распределение + RedirectStrategy (один файл, < 150 стр.)
│ │
│ ├── Protection/
│ │ ├── RestreamDetector.php # Антипиратская защита
│ │ ├── ConnectionLimiter.php # Лимит подключений
│ │ └── GeoBlock.php # Блокировка по стране/ISP/IP/UA
│ │
│ ├── Codec/
│ │ ├── FFmpegCommand.php # Построение FFmpeg-команд (из CoreUtilities)
│ │ ├── TranscodeProfile.php # Профили транскодирования
│ │ └── TsParser.php # Парсер MPEG-TS (текущий ts.php)
│ │
│ └── Health/
│ ├── DivergenceDetector.php # Мониторинг качества
│ └── BitrateTracker.php # Отслеживание bitrate/FPS
│
├── interfaces/ # ═══ ТОЧКИ ВХОДА (UI, API, CLI) ═══
│ ├── Http/
│ │ ├── public/ # Web root (nginx document root)
│ │ │ ├── index.php # Единая точка входа (front controller)
│ │ │ ├── stream.php # Точка входа для стриминга
│ │ │ └── assets/ # CSS/JS/images/fonts
│ │ │ ├── admin/
│ │ │ └── player/
│ │ │
│ │ ├── Controllers/
│ │ │ ├── Admin/
│ │ │ │ ├── DashboardController.php
│ │ │ │ ├── StreamController.php
│ │ │ │ ├── LineController.php
│ │ │ │ ├── VodController.php
│ │ │ │ ├── ServerController.php
│ │ │ │ ├── SettingsController.php
│ │ │ │ ├── UserController.php
│ │ │ │ ├── BouquetController.php
│ │ │ │ ├── EpgController.php
│ │ │ │ └── ... (по одному на доменную область)
│ │ │ │
│ │ │ ├── Reseller/
│ │ │ │ ├── DashboardController.php
│ │ │ │ ├── LineController.php
│ │ │ │ └── ... (только разрешённые действия)
│ │ │ │
│ │ │ └── Api/
│ │ │ ├── AdminApiController.php
│ │ │ ├── ResellerApiController.php
│ │ │ ├── PlayerApiController.php # Публичный player API
│ │ │ └── InternalApiController.php # Межсерверный API (текущий www/api.php)
│ │ │
│ │ └── Views/
│ │ ├── layouts/
│ │ │ ├── admin.php # Header + footer шаблон для admin
│ │ │ └── reseller.php # Header + footer шаблон для reseller
│ │ │
│ │ ├── admin/
│ │ │ ├── dashboard.php
│ │ │ ├── streams/
│ │ │ │ ├── list.php
│ │ │ │ ├── edit.php
│ │ │ │ └── view.php
│ │ │ ├── lines/
│ │ │ ├── vod/
│ │ │ ├── servers/
│ │ │ └── settings/
│ │ │
│ │ ├── reseller/
│ │ │ ├── dashboard.php
│ │ │ └── ...
│ │ │
│ │ └── partials/
│ │ ├── modals.php
│ │ ├── topbar.php
│ │ └── table.php
│ │
│ ├── Cli/
│ │ ├── Commands/
│ │ │ ├── StartupCommand.php # cli/startup.php
│ │ │ ├── WatchdogCommand.php # cli/watchdog.php
│ │ │ ├── MonitorCommand.php # cli/monitor.php
│ │ │ ├── CacheHandlerCommand.php # cli/cache_handler.php
│ │ │ ├── QueueCommand.php # cli/queue.php
│ │ │ ├── SignalsCommand.php # cli/signals.php
│ │ │ ├── ScannerCommand.php # cli/scanner.php
│ │ │ ├── MigrateCommand.php # Из status
│ │ │ └── ToolsCommand.php # Из tools (rescue, access, ports и т.д.)
│ │ └── CronJobs/
│ │ ├── StreamsCron.php
│ │ ├── ServersCron.php
│ │ ├── CacheCron.php
│ │ ├── EpgCron.php
│ │ ├── CleanupCron.php
│ │ ├── BackupsCron.php
│ │ ├── StatsCron.php
│ │ ├── LogsCron.php # lines_logs + streams_logs
│ │ ├── VodCron.php
│ │ └── TmdbCron.php
│ │
│ └── Player/ # Встроенный web-плеер
│ ├── PlayerController.php
│ └── views/
│ └── ... (текущий player/)
│
├── modules/ # ═══ ОПЦИОНАЛЬНЫЕ МОДУЛИ ═══
│ ├── ministra/ # Ministra/Stalker middleware (текущий ministra/)
│ │ ├── module.json # Manifest: name, version, dependencies
│ │ ├── MinistraModule.php # Точка входа модуля (implements ModuleInterface)
│ │ ├── PortalHandler.php # Диспетчер type+action (15 обработчиков)
│ │ ├── PortalHelpers.php # Хелперы портала (19 статических методов)
│ │ └── assets/
│ │
│ ├── plex/ # Plex integration (текущий settings_plex, plex_add)
│ │ ├── module.json
│ │ ├── PlexModule.php
│ │ ├── PlexService.php
│ │ └── config/
│ │
│ ├── tmdb/ # TMDB Integration
│ │ ├── module.json
│ │ ├── TmdbModule.php
│ │ ├── TmdbService.php
│ │ ├── TmdbCron.php
│ │ ├── TmdbPopularCron.php
│ │ └── lib.php # proxy → includes/libs/tmdb.php
│ │
│ ├── watch/ # Watch/Recording (текущие watch, record файлы)
│ │ ├── module.json
│ │ ├── WatchModule.php
│ │ ├── WatchService.php
│ │ ├── RecordingService.php
│ │ ├── WatchController.php
│ │ ├── WatchCron.php
│ │ ├── WatchItem.php
│ │ └── views/
│ │ ├── watch.php
│ │ ├── watch_scripts.php
│ │ ├── watch_add.php
│ │ ├── watch_add_scripts.php
│ │ ├── settings_watch.php
│ │ ├── settings_watch_scripts.php
│ │ ├── watch_output.php
│ │ ├── watch_output_scripts.php
│ │ ├── record.php
│ │ └── record_scripts.php
│ │
│ ├── fingerprint/ # Fingerprint watermarking
│ │ ├── module.json
│ │ └── FingerprintModule.php
│ │
│ ├── theft-detection/ # Anti-theft/restream detection
│ │ ├── module.json
│ │ └── TheftDetectionModule.php
│ │
│ └── magscan/ # MAG device scanning
│ ├── module.json
│ └── MagscanModule.php
│
├── infrastructure/ # ═══ СИСТЕМНАЯ ИНФРАСТРУКТУРА ═══
│ ├── nginx/
│ │ ├── NginxConfigGenerator.php # Генерация nginx.conf (из CoreUtilities)
│ │ ├── templates/
│ │ │ ├── main.conf.tpl
│ │ │ ├── rtmp.conf.tpl
│ │ │ └── vhost.conf.tpl
│ │ └── NginxReloader.php
│ │
│ ├── redis/
│ │ └── RedisManager.php # Подключение, pipeline, pub/sub
│ │
│ ├── service/
│ │ ├── ServiceManager.sh # Текущий файл `service` (bash)
│ │ └── daemons.sh # Список демонов
│ │
│ ├── install/
│ │ ├── database.sql # Начальная схема
│ │ └── proxy.tar.gz
│ │
│ └── bin/ # Внешние бинарники (FFmpeg, certbot, yt-dlp и т.д.)
│ ├── ffmpeg_bin/
│ ├── certbot/
│ ├── maxmind/
│ ├── guess
│ ├── yt-dlp
│ └── network.py
│
├── data/ # ═══ ДАННЫЕ ВРЕМЕНИ ВЫПОЛНЕНИЯ ═══
│ ├── cache/ # Файловый кэш (igbinary)
│ ├── logs/ # Логи приложения
│ ├── tmp/ # Временные файлы (текущий tmp/)
│ ├── content/ # Медиа-контент (текущий content/)
│ │ ├── archive/
│ │ ├── epg/
│ │ ├── playlists/
│ │ ├── streams/
│ │ ├── video/
│ │ └── vod/
│ ├── backups/ # Резервные копии
│ └── signals/ # Сигнальные файлы (.gitkeep)
│
├── config/ # ═══ КОНФИГУРАЦИЯ ═══
│ ├── config.ini # Основной конфиг (DB, Redis, пути)
│ ├── modules.php # Список включённых модулей
│ ├── routes.php # Таблица маршрутов
│ ├── plex/
│ └── rclone.conf
│
└── resources/ # ═══ РЕСУРСЫ ═══
├── langs/ # Переводы (текущие .ini файлы)
│ ├── en.ini
│ ├── ru.ini
│ ├── de.ini
│ ├── es.ini
│ ├── fr.ini
│ ├── bg.ini
│ └── pt.ini
├── data/
│ ├── countries.php # Массивы стран (извлечены из admin.php)
│ ├── timezones.php
│ ├── mac_types.php
│ └── error_codes.php # $rErrorCodes (извлечены из constants.php)
└── libs/ # Сторонние PHP-библиотеки
├── MobileDetect.php
├── XmlStringStreamer.php
├── m3u/
└── Dropbox.php
4. Описание компонентов
4.1. core/ — Ядро системы
Ответственность: Инфраструктурные сервисы, которые нужны любому контексту исполнения. Не содержит бизнес-логики.
| Подкаталог | Что даёт | Что заменяет |
|---|---|---|
Config/ |
Загрузка конфигурации, резолв путей | 70 define() из constants.php и stream/init.php |
Database/ |
PDO-обёртка (Database + DatabaseHandler), query builder, миграции | Database.php + SQL-запросы из status |
Cache/ |
Унифицированный кэш с двумя драйверами | Разбросанные igbinary_serialize + ad-hoc Redis |
Auth/ |
Единая авторизация для admin и reseller | session.php × 2, API::processLogin(), ResellerAPI::processLogin() |
Http/ |
Абстракция запросов, роутинг, middleware | cleanGlobals() × 2, switch($rAction), flood-check в constants.php |
Process/ |
Управление PID, демонами, блокировками | shell_exec('ps aux'), posix_kill(), checkCron() разбросанные по файлам |
Logging/ |
Унифицированное логирование | CoreUtilities::saveLog(), StreamingUtilities::clientLog(), Logger::init() |
Events/ |
Event bus для связи без жёстких зависимостей | Прямые вызовы между компонентами |
Container/ |
DI-контейнер (composition root only — см. §2.3) | global $db, статические CoreUtilities::$rSettings |
Util/ |
Утилиты без состояния | Функции разбросанные по CoreUtilities, admin.php |
Ключевое правило: core/ не знает о существовании domain/, streaming/, modules/, interfaces/. Зависимости направлены только внутрь ядра.
4.2. domain/ — Бизнес-логика
Ответственность: Сервисы и репозитории, организованные по контекстам. Каждый контекст (Stream, Vod, Line, User...) — отдельная директория.
Паттерн для каждого контекста:
domain/Stream/
├── StreamService.php # Бизнес-логика + оркестрация (валидация, транзакции, side-effects)
├── StreamRepository.php # SQL-запросы (SELECT/INSERT/UPDATE/DELETE)
└── StreamProcess.php # Специфичные системные операции (ffmpeg, kill, PID)
Правила:
- Service = вся бизнес-логика контекста. Валидация, транзакции, вызов Infrastructure (nginx, ffmpeg), логирование. Один Service на контекст, при росте можно разбить.
- Repository = только SQL. Принимает массивы, возвращает массивы. Никакой логики. Никаких side-effects.
- Нет Entity-классов. Данные передаются как
array. Это PHP — массивы проще и понятнее, чем аномичные DTO-объекты. - Консолидация мелких классов (§2.4). Если Repository < 5 методов и < 150 строк — он живёт в одном файле с Service. Мелкие контексты (Ticket, Settings) вливаются в родственный контекст, а не создают отдельную директорию.
// ✅ ПРАВИЛЬНО — Service делает всё:
class StreamService {
public function __construct(
private StreamRepository $repository,
private ProcessManager $processManager,
private FileLogger $logger,
private Database $db
) {}
public function create(array $data): int {
// Валидация
if (empty($data['stream_source'])) {
throw new \InvalidArgumentException('Source required');
}
// Транзакция
$this->db->beginTransaction();
try {
$id = $this->repository->insert($data);
$this->db->commit();
} catch (\Throwable $e) {
$this->db->rollBack();
throw $e;
}
$this->logger->log('stream', "Created stream {$id}");
return $id;
}
}
// ✅ ПРАВИЛЬНО — Repository = только SQL:
class StreamRepository {
public function __construct(private Database $db) {}
public function findById(int $id): ?array {
return $this->db->row("SELECT * FROM streams WHERE id = ?", [$id]);
}
public function insert(array $data): int {
$this->db->query("INSERT INTO streams ...", $data);
return $this->db->lastInsertId();
}
}
Откуда берётся код:
admin_api.php(6981 строк) → разбивается на ~10 Service + ~6 Repository (§2.4 — мелкие Repository влиты в Service)admin.php→ процедурныеgetUserInfo(),getSeriesList()→ в Repository- Оркестрация (validate + save + nginx + cache + log) → в Service
Зависимости: domain/ зависит от core/ (Database, Cache, Events). Не зависит от interfaces/ или modules/.
4.3. streaming/ — Стриминг-движок
Ответственность: Весь hot path доставки видео. Выделен отдельно от domain/ потому что:
- Критичен к производительности (нельзя загружать всю бизнес-логику)
- Имеет собственный лёгкий bootstrap
- Работает на уровне байтов/сегментов, а не на уровне CRUD
Изоляция streaming
Streaming — отдельный контекст исполнения с минимальными зависимостями:
streaming/ зависит от:
✅ core/ (Database, Redis, Logging, GeoIP, Encryption, NetworkUtils)
✅ domain/ (Repository — только SELECT-запросы: потоки, серверы, букеты)
❌ НЕ зависит от:
- interfaces/ (контроллеры)
- modules/ (всё)
- domain/*Service.php (бизнес-мутации)
Главное правило: streaming вызывает только read-методы Repository. Запись данных (PID, stats, connections) — через собственные классы (ConnectionTracker, BitrateTracker), а не через Domain Services.
Откуда берётся код:
StreamingUtilities.php(1992 стр.) → распределяется по подкаталогамwww/stream/auth.php(800 стр.) →Auth/TokenAuth.php+Auth/StreamAuth.php+Protection/www/stream/live.php(708 стр.) →Delivery/LiveDelivery.phpincludes/cli/monitor.php(565 стр.) →domain/Stream/StreamMonitor.php(с рефакторингом goto)CoreUtilitiesметоды FFmpeg →Codec/FFmpegCommand.phpts.php→Codec/TsParser.php
4.4. interfaces/ — Точки входа
Ответственность: Получение запроса, вызов Service, формирование ответа. Никакой бизнес-логики.
HTTP:
- Один front controller
public/index.php+Router - Контроллеры вызывают Service из
domain/для мутаций и Repository для чтения - Шаблоны (
Views/) — чистый HTML с минимумом PHP-вставок - Admin и Reseller — разные контроллеры, общие шаблоны
// ✅ ПРАВИЛЬНО — контроллер вызывает Service:
class StreamController {
public function __construct(
private StreamService $streamService,
private StreamRepository $streamRepo
) {}
public function create(Request $request): void {
$id = $this->streamService->create($request->all());
Response::json(['id' => $id]);
}
public function list(): void {
$streams = $this->streamRepo->getAll();
include VIEW_PATH . '/admin/streams/list.php';
}
}
// ❌ ЗАПРЕЩЕНО — контроллер содержит бизнес-логику:
class StreamController {
public function create(Request $request): void {
// Валидация, транзакции, nginx — это задача Service
$this->db->beginTransaction();
$this->streamRepo->insert(...); // ← fat controller
$this->nginx->reload();
$this->db->commit();
}
}
CLI:
- Каждый демон/крон — отдельный Command-класс
- Общая инициализация через
bootstrap.php+ServiceContainer - Файл
service(bash) →infrastructure/service/ServiceManager.sh
Откуда берётся код:
- Каждый
admin/*.php(100+ файлов) → Controller + View. Пример:admin/streams.php→Controllers/Admin/StreamController.php+Views/admin/streams/list.php admin/header.php(675 стр.) +admin/footer.php(805 стр.) →Views/layouts/admin.php+ выделение JS в файлыassets/admin/post.php(1946 стр.) → обработчики форм распределяются по контроллерамcrons/*.php→Cli/CronJobs/, каждый крон — один файлincludes/cli/*.php→Cli/Commands/
4.5. modules/ — Опциональные модули
Ответственность: Дополнительные функции, которые не являются частью ядра. Каждый модуль — самодостаточная директория.
Контракт модуля:
interface ModuleInterface {
public function getName(): string;
public function getVersion(): string;
public function boot(ServiceContainer $container): void; // Регистрация сервисов
public function registerRoutes(Router $router): void; // Свои маршруты
public function registerCrons(): array; // Свои кроны
public function getEventSubscribers(): array; // Подписки на события
}
// module.json
{
"name": "ministra",
"version": "1.0.0",
"description": "Ministra/Stalker Portal middleware",
"requires_core": ">=2.0",
"dependencies": []
}
Текущий код → модули:
| Модуль | Источник | Почему модуль, а не ядро |
|---|---|---|
ministra/ |
src/ministra/ (2155+ строк, десятки JS-файлов) |
Сторонний портал, не всем нужен |
plex/ |
settings_plex.php, plex_add.php, crons/plex.php, config/plex/ |
Интеграция с внешним сервисом |
tmdb/ |
includes/libs/TMDb/, crons/tmdb.php, crons/tmdb_popular.php |
Внешний API для метаданных |
watch/ |
watch.php, watch_add.php, watch_output.php, settings_watch.php, crons/watch.php |
Запись/DVR — расширенная функция |
fingerprint/ |
admin/fingerprint.php |
Водяные знаки — enterprise-функция |
theft-detection/ |
admin/theft_detection.php |
Антипиратство — enterprise-функция |
magscan/ |
admin/magscan_settings.php |
Сканирование устройств |
4.6. infrastructure/ — Системный слой
Ответственность: Взаимодействие с ОС: nginx, redis, bash-скрипты, внешние бинарники.
Откуда берётся код:
CoreUtilities→ генерация nginx-конфигурации →nginx/NginxConfigGenerator.phpbin/nginx/,bin/php/,bin/redis/→bin/(бинарники как есть)bin/daemons.sh→service/daemons.shservice→service/ServiceManager.shbin/install/database.sql→install/database.sql
4.7. data/ — Runtime-данные
Ответственность: Всё, что генерируется при эксплуатации и не является кодом.
Заменяет текущие: tmp/, content/, backups/, signals/.
4.8. config/ — Конфигурация
Ответственность: Всё, что администратор может изменить без правки кода.
4.9. resources/ — Статические ресурсы и данные
Ответственность: Переводы, данные-справочники (страны, таймзоны), сторонние PHP-библиотеки.
Откуда берётся код:
includes/langs/*.ini→langs/- Inline-массивы стран из
admin.php→data/countries.php $rErrorCodesизconstants.php→data/error_codes.phpincludes/libs/*→libs/
5. Карта миграции: откуда → куда
5.1. God-объект CoreUtilities.php (4847 строк)
| Метод/блок | Целевой файл |
|---|---|
init(), config loading |
core/Config/ConfigLoader.php |
$db, getDatabase() |
core/Database/DatabaseHandler.php |
getCache(), setCache() |
core/Cache/FileCache.php |
| Redis operations | core/Cache/RedisCache.php, infrastructure/redis/RedisManager.php |
cleanGlobals(), parseIncomingRecursively() |
core/Http/Request.php |
startMonitor(), stopStream(), startMovie() |
domain/Stream/StreamService.php |
isStreamRunning(), isMonitorRunning() |
core/Process/ProcessManager.php |
| FFmpeg command building | streaming/Codec/FFmpegCommand.php |
| GeoIP lookup | core/Util/GeoIP.php |
| Nginx config generation | infrastructure/nginx/NginxConfigGenerator.php |
| Image resize/upload | core/Util/ImageUtils.php |
| Encryption (AES/token) | core/Util/Encryption.php |
saveLog() |
core/Logging/FileLogger.php |
5.2. God-объект admin_api.php (6981 строк)
| Метод/блок | Целевой файл |
|---|---|
processStream() |
domain/Stream/StreamService.php |
processMovie() |
domain/Vod/MovieService.php |
processSerie() |
domain/Vod/SeriesService.php |
processEpisode() |
domain/Vod/EpisodeService.php |
processLine() |
domain/Line/LineService.php |
processMAG() |
domain/Device/MagService.php |
processEnigma() |
domain/Device/EnigmaService.php |
processServer() |
domain/Server/ServerService.php |
processBouquet() |
domain/Bouquet/BouquetService.php (+ Repository + Mapper — один файл) |
processUser() |
domain/User/UserService.php (+ submit ticket, editProfile) |
processGroup() |
domain/User/GroupService.php (+ GroupRepository — один файл) |
processCode() |
domain/Auth/AuthService.php (+ Code + HMAC — объединены) |
processLogin() |
core/Auth/Authenticator.php |
processSettings() |
domain/Server/SettingsService.php (влито из domain/Settings/) |
massEditStreams() |
domain/Stream/StreamService::massEdit() |
checkMinimumRequirements() |
Валидация в каждом Service |
5.3. God-bootstrap admin.php (4448 строк)
| Блок | Целевой файл |
|---|---|
session_start(), session config |
core/Auth/SessionManager.php |
50+ define() статусов |
constants.php |
Database creation |
core/Container/ServiceContainer.php |
CoreUtilities::init() |
bootstrap.php |
$rCountryCodes, $rCountries |
resources/data/admin_constants.php |
$rPermissions |
resources/data/admin_constants.php → core/Auth/Authorization.php |
getUserInfo() |
domain/User/UserRepository.php |
getSeriesList(), updateSeries() |
domain/Vod/SeriesService.php (Repository влит — §2.4) |
secondsToTime() |
core/Util/TimeUtils.php |
hasPermissions() |
core/Auth/Authorization.php |
| Mobile detect init | core/Http/Middleware/ (если нужно) |
| Translator init | bootstrap.php → ServiceContainer |
5.4. Страницы admin/ (100+ файлов)
Паттерн трансформации:
БЫЛО:
admin/streams.php (один файл = SQL + HTML + JS)
СТАЛО:
interfaces/Http/Controllers/Admin/StreamController.php — маршрутизация
domain/Stream/StreamRepository.php — данные
interfaces/Http/Views/admin/streams/list.php — шаблон
5.5. Дублирование admin/ ↔ reseller/
БЫЛО:
admin/header.php (675 строк) + reseller/header.php (284 строки)
admin/footer.php (805 строк) + reseller/footer.php
admin/session.php + reseller/session.php
admin/functions.php + reseller/functions.php
СТАЛО:
interfaces/Http/Views/layouts/admin.php — единый layout
interfaces/Http/Views/layouts/reseller.php — наследует admin layout с ограничениями
core/Auth/SessionManager.php — единый менеджер сессий
core/Auth/Authorization.php — RBAC определяет, что видит пользователь
5.6. Стриминг-путь
БЫЛО:
www/stream/init.php (дублированный bootstrap)
www/stream/auth.php (800 строк: auth + block + token + balance)
www/stream/live.php (708 строк: token + ondemand + delivery + heartbeat)
StreamingUtilities.php (1992 строки: форк CoreUtilities)
СТАЛО:
streaming/StreamingBootstrap.php — лёгкий init без дублирования
streaming/Auth/TokenAuth.php — парсинг токенов
streaming/Auth/StreamAuth.php — проверка доступа
streaming/Protection/GeoBlock.php — блокировки (IP/ISP/UA/Country)
streaming/Delivery/LiveDelivery.php — раздача контента
streaming/Health/BitrateTracker.php — мониторинг качества
core/Cache/* — общий кэш (не дублируется)
core/Database/DatabaseHandler.php — общая БД (не дублируется)
6. Система модулей
6.1. Жизненный цикл модуля
1. Модуль размещается в modules/{name}/
2. Добавляется в config/modules.php
3. При bootstrap: ServiceContainer сканирует modules.php
4. Для каждого модуля: require module.json → проверка зависимостей → boot()
5. Модуль регистрирует: сервисы, маршруты, кроны, event-подписки
6. Удаление: убрать из modules.php → (опционально) удалить папку
6.2. Правила изоляции
✅ Модуль МОЖЕТ:
- Использовать сервисы из core/ через constructor injection
- Вызывать Service из domain/ для бизнес-операций
- Использовать Repository из domain/ для чтения данных
- Регистрировать свои маршруты, команды, кроны
- Подписываться на события-хуки ядра
- Иметь свои assets, views, конфиги
❌ Модуль НЕ МОЖЕТ:
- Модифицировать файлы core/ или domain/
- Напрямую обращаться к базе данных мимо Repository
- Зависеть от другого модуля без явной декларации в module.json
- Переопределять маршруты или сервисы ядра
- Добавлять ограничения лицензирования в ядро
6.3. Расширяемость через события-хуки
События используются только для модульных хуков, а не внутри обычного CRUD.
Когда использовать события:
- Модуль хочет отреагировать на действие ядра (например, запись DVR при запуске потока)
- Ядро не должно знать о модуле
Когда НЕ использовать:
- Внутри CRUD-операций (создал поток → обновил nginx) — это прямой вызов в Service
- Между ядерными компонентами (это магия, которую сложно отладить)
// ✅ ПРАВИЛЬНО — события для модульных хуков:
// Ядро публикует:
$dispatcher->dispatch(new StreamStartedEvent($streamId));
// Модуль watch подписывается:
class WatchModule implements ModuleInterface {
public function getEventSubscribers(): array {
return [
StreamStartedEvent::class => [WatchRecorder::class, 'onStreamStarted'],
];
}
}
// ❌ НЕПРАВИЛЬНО — события внутри CRUD:
class StreamService {
public function create(array $data): int {
$id = $this->repo->insert($data);
// НЕ нужно dispatch StreamCreatedEvent для обновления nginx.
// Просто вызвать $this->nginx->reload() напрямую.
$this->nginx->reload();
return $id;
}
}
6.4. Коммерческие модули (будущее)
Коммерческие модули — обычные модули в modules/, но:
- Доставляются отдельно (не в основном репозитории)
- Могут содержать собственную проверку лицензии внутри себя
- Ядро не содержит кода проверки лицензии — если модуль удалён, система работает
modules/
├── ministra/ ← open-source, в основном репо
├── plex/ ← open-source, в основном репо
├── enterprise-analytics/ ← коммерческий, отдельный репо
│ ├── module.json
│ ├── License.php ← проверка лицензии ВНУТРИ модуля
│ └── AnalyticsModule.php
└── advanced-security/ ← коммерческий, отдельный репо
7. Границы ядра и модулей
7.1. Пакетная диаграмма зависимостей
┌──────────────┐
│ interfaces/ │ ← HTTP, CLI, Player
└──────┬───────┘
│ depends on
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ domain/ │ │streaming/│ │ modules/ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ depends on │ depends on │ depends on
▼ ▼ ▼
┌──────────────────────────────────┐
│ core/ │ ← Config, DB, Cache, Auth, Process,
│ │ Logging, Events, Container, Util
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ infrastructure/ │ ← nginx, redis, bin, service
└──────────────────────────────────┘
Стрелки всегда направлены вниз. Ни один нижний слой не знает о верхних.
Исключение: streaming/ — горизонтальный слой на уровне domain/, но с read-only доступом к domain/Repository. У него свой лёгкий entry path (см. §4.3).
7.2. Что входит в ядро, а что нет
В ядро (core/ + domain/) |
В модули (modules/) |
|---|---|
| Управление потоками (CRUD, запуск, остановка) | Ministra portal |
| Управление подписками (линии, пакеты) | Plex integration |
| Управление VOD (фильмы, сериалы) | TMDb metadata fetching |
| Авторизация и RBAC | Fingerprint watermarking |
| Серверная инфраструктура | Theft detection |
| EPG (базовый импорт) | MAG device scanning |
| Bouquet management | Watch/DVR recording |
| Стриминг-движок | Advanced analytics (будущее) |
| API (admin, reseller, player) | White-label theming (будущее) |
5.1 includes/ — Устаревшие файлы
includes/
├── admin.php # ~150 глобальных функций -> proxy к domain/*/Service.php
├── admin_api.php # ~80 методов класса AdminAPI -> proxy к domain/*/Service.php
├── reseller_api.php # ~9 методов класса ResellerAPI
├── CoreUtilities.php # ~135 статических методов -> proxy к core/ и domain/
├── StreamingUtilities.php # ~66 статических методов -> proxy к streaming/
├── ts.php # Timeshift утилиты
├── bootstrap/ # Старые файлы инициализации admin
│ ├── admin_bootstrap.php
│ ├── admin_runtime.php
│ └── admin_session.php
├── api/ # API-обработчики
│ ├── admin/
│ └── reseller/
├── cli/ # CLI-скрипты (monitor, balancer, watchdog и т.д.)
├── data/ # Статические данные (permissions)
├── langs/ # Файлы локализации (en.ini, ru.ini, de.ini, ...)
├── libs/ # Сторонние библиотеки
│ ├── Logger.php
│ ├── AsyncFileOperations.php
│ ├── Dropbox.php
│ ├── TMDb/
│ └── ...
└── python/ # Python-скрипты
Статус: Все 150 функций admin.php и 135 методов CoreUtilities уже являются proxy-обёртками, делегирующими в domain/ и core/. После завершения миграции файлы будут удалены.
5.2 admin/ — Легаси admin-страницы
~120 PHP-файлов — старые admin-страницы, которые содержали SQL+HTML в одном файле. Заменены на interfaces/Http/Controllers/Admin/ + interfaces/Http/Views/admin/.
5.3 reseller/ — Легаси reseller-страницы
~35 PHP-файлов — аналогично admin/, но для реселлерской панели. Заменены на interfaces/Http/Controllers/Reseller/ + interfaces/Http/Views/reseller/.
5.4 ministra/ — Stalker Portal JS
JavaScript-файлы для поддержки Stalker Portal (MAG-устройства). Не PHP — отдаются nginx напрямую. Сохраняется как есть.
5.5 player/ — Встроенный web-плеер
Мини-приложение (PHP + JS + CSS) для веб-отображения потоков. Имеет собственные header.php, footer.php, functions.php. Сохраняется как есть до отдельного рефакторинга.
8. Варианты сборки: MAIN vs LoadBalancer
8.1. Два артефакта из одной кодовой базы
Система собирается в два варианта из одного src/:
| Артефакт | Назначение | Что включает | Что исключает |
|---|---|---|---|
| MAIN | Основной сервер (admin + streaming) | Всё содержимое src/ |
Ничего |
| LoadBalancer (LB) | Стриминг-сервер без управления | Только стриминг + инфраструктура | Админ-панель, реселлер, player, ministra, admin-only модули |
Ключевой принцип: LB — это подмножество MAIN. Код не форкается, а фильтруется при сборке.
8.2. Текущая проблема (до миграции Makefile)
Makefile собирает LB из фиксированного списка директорий (LB_FILES):
# ТЕКУЩИЙ (устаревший) список:
LB_FILES := bin config content crons includes signals tmp www status update service
Эти новые директории НЕ входят в LB_FILES и НЕ копируются в LB:
| Директория / файл | Нужен LB? | Статус |
|---|---|---|
autoload.php |
✅ ДА — без него новые классы не загружаются | ⚠ Отсутствует в LB |
bootstrap.php |
✅ ДА — единая точка инициализации (CONTEXT_STREAM) | ⚠ Отсутствует в LB |
core/ |
✅ ДА — Database, Cache, Auth, Config, Process, Http, Logging, Container, Util | ⚠ Отсутствует в LB |
domain/ |
⚠ ЧАСТИЧНО — Stream/, Server/, Bouquet/ нужны; User/, Ticket/, Device/ — нет | ⚠ Отсутствует в LB |
streaming/ |
✅ ДА — весь стриминг-движок | ⚠ Отсутствует в LB |
infrastructure/ |
✅ ДА — redis/, nginx/ | ⚠ Отсутствует в LB |
resources/ |
⚠ ЧАСТИЧНО — data/error_codes.php нужен; langs/ — нет | ⚠ Отсутствует в LB |
data/ |
⚠ ЧАСТИЧНО — runtime-данные создаются динамически | ⚠ Отсутствует в LB |
interfaces/ |
⚠ ЧАСТИЧНО — Cli/ частично нужен | ⚠ Отсутствует в LB |
modules/ |
❌ НЕТ — все текущие модули admin-only | ⚠ Отсутствует в LB |
admin/ |
❌ НЕТ | ✅ Корректно отсутствует |
ministra/ |
❌ НЕТ | ✅ Корректно отсутствует |
player/ |
❌ НЕТ | ✅ Корректно отсутствует |
reseller/ |
❌ НЕТ | ✅ Корректно отсутствует |
Последствие: Proxy-методы в CoreUtilities.php (который IS копируется в LB через includes/) вызывают классы из core/ и domain/, но эти директории отсутствуют → LB-сборка ломается при вызове мигрированного кода.
8.3. Целевая конфигурация Makefile
Обновлённый LB_FILES
# ЦЕЛЕВОЙ (обновлённый) список:
LB_FILES := autoload.php bootstrap.php \
bin config content core crons data domain includes infrastructure \
interfaces resources signals streaming tmp www status update service
Обновлённый LB_DIRS_TO_REMOVE
LB_DIRS_TO_REMOVE := \
# Существующие исключения:
bin/install \
bin/redis \
includes/langs \
includes/api \
includes/libs/resources \
bin/nginx/conf/codes \
# Новые исключения для целевой архитектуры:
admin \
ministra \
player \
reseller \
domain/User \
domain/Ticket \
domain/Device \
interfaces/Http/Controllers/Admin \
interfaces/Http/Controllers/Reseller \
interfaces/Http/Views \
interfaces/Player \
modules/fingerprint \
modules/magscan \
modules/ministra \
modules/plex \
modules/theft-detection \
modules/tmdb \
modules/watch \
resources/langs
Обновлённый LB_FILES_TO_REMOVE
LB_FILES_TO_REMOVE := \
# Существующие (пока admin.php/admin_api.php живут в includes/):
includes/admin_api.php \
includes/admin.php \
includes/reseller_api.php \
# ... остальные существующие исключения ...
# Новые:
includes/cli/migrate.php \
includes/cli/cache_handler.php \
includes/cli/balancer.php \
crons/backups.php \
crons/cache_engine.php \
crons/epg.php \
crons/update.php \
crons/providers.php \
crons/root_mysql.php \
crons/series.php \
crons/tmdb.php \
crons/tmdb_popular.php
8.4. Что LB использует — карта зависимостей
www/stream/*.php ──→ bootstrap.php (CONTEXT_STREAM)
│
├──→ autoload.php (class map)
├──→ core/Config/ (пути, конфигурация)
├──→ core/Database/ (PDO-обёртка)
├──→ core/Cache/ (Redis + File кэш)
├──→ core/Auth/ (BruteforceGuard)
├──→ core/Http/ (Request, RequestGuard)
├──→ core/Process/ (ProcessManager)
├──→ core/Logging/ (FileLogger, DatabaseLogger)
├──→ core/Util/ (GeoIP, NetworkUtils, Encryption)
├──→ core/Error/ (ErrorHandler, ErrorCodes)
│
├──→ streaming/Auth/ (TokenAuth, StreamAuth, DeviceLock)
├──→ streaming/Delivery/ (LiveDelivery, VodDelivery, SegmentReader)
├──→ streaming/Balancer/ (LoadBalancer, RedirectStrategy)
├──→ streaming/Protection/(RestreamDetector, ConnectionLimiter, GeoBlock)
├──→ streaming/Codec/ (FFmpegCommand, TsParser)
│
├──→ domain/Stream/ (StreamProcess, ConnectionTracker, StreamSorter)
├──→ domain/Server/ (ServerRepository)
├──→ domain/Bouquet/ (BouquetMapper — для плейлистов)
├──→ domain/Vod/ (для VOD-доставки)
│
├──→ infrastructure/redis/(RedisManager)
└──→ resources/data/ (error_codes.php)
crons/*.php (на LB) ──→ bootstrap.php (CONTEXT_CLI)
└──→ те же зависимости + domain/Stream/*
8.5. Правила для разработки с учётом LB
-
Proxy-методы в
CoreUtilities/StreamingUtilitiesдолжны быть безопасны для LB. Покаautoload.phpиcore//domain/не добавлены в LB-сборку, proxy-вызовы сломают LB. -
Makefile должен обновляться синхронно с миграцией, конкретно при добавлении новых
LB_FILESкаждый раз, когда код изincludes/мигрирует в новые директории. -
Тестирование LB-сборки — после каждой фазы миграции нужно проверять:
make new && make lb # Убедиться, что все нужные файлы присутствуют # Убедиться, что admin-only файлы отсутствуют -
domain/ частично нужен LB — нельзя целиком исключать
domain/. Исключаются только admin-specific поддомены (User/,Ticket/,Device/). -
modules/ полностью исключается из LB — все текущие модули (ministra, plex, tmdb, watch, fingerprint, theft-detection, magscan) — это admin-функциональность. В будущем, если появится LB-specific модуль, его нужно будет явно добавить.
8.6. Переходный период (сейчас)
Пока миграция идёт итеративно (фазы 1-3 в процессе), Makefile нужно обновить немедленно:
Минимально необходимое изменение (LB_FILES):
LB_FILES := bin config content core crons domain includes \
infrastructure resources signals streaming tmp www status update service
Плюс два root-файла — autoload.php и bootstrap.php — нужно копировать отдельно, т.к. LB_FILES работает только с директориями:
lb_copy_files:
# ... существующий код ...
@echo "==> [LB] Copying root files"
@for root_file in autoload.php bootstrap.php; do \
if [ -f "$(MAIN_DIR)/$$root_file" ]; then \
cp "$(MAIN_DIR)/$$root_file" "$(TEMP_DIR)/$$root_file"; \
fi; \
done
9. Порядок миграции
Принцип: извлечение → делегирование → замена
Каждый шаг миграции следует одному паттерну:
1. Создать новый класс в целевой директории
2. Перенести в него методы из god-объекта
3. В старом файле оставить proxy-метод:
public static function oldMethod(...$args) {
return NewClass::method(...$args);
}
4. Зарегистрировать класс в autoloader
5. Проверить: система работает как раньше
6. (позже) Обновить вызывающий код → удалить proxy
Так каждый шаг безопасен и обратимо совместим.
Фаза 0: Подготовка ✅
- ✅ Автозагрузчик
src/autoload.php— карта классов + fallback spl_autoload - ✅ Скелет директорий (98 каталогов)
- ✅
bootstrap.php— XC_Bootstrap с 4 контекстами - ✅
core/Container/ServiceContainer.php— DI-контейнер - ✅ Разбиение
www/constants.php→ 7 core-файлов + тонкий фасад (74 строки)- ✅
core/Config/Paths.php,AppConfig.php,Binaries.php,ConfigLoader.php - ✅
core/Error/ErrorCodes.php,ErrorHandler.php - ✅
core/Http/RequestGuard.php - ✅
www/stream/init.php→ подключён autoload.php
- ✅
Фаза 1: Извлечение core/ (базовые компоненты)
1.1 ✅ Database
- ✅
core/Database/DatabaseHandler.php(530 стр.) ←includes/Database.php - ✅
core/Database/Database.php(299 стр.) — перемещён изincludes/ - ✅ Все 20 файлов переключены на DatabaseHandler
1.2 ✅ Cache
- ✅
core/Cache/CacheInterface.php(84 стр.) - ✅
core/Cache/FileCache.php(229 стр.) ←CoreUtilities::getCache/setCache - ✅
core/Cache/RedisCache.php(262 стр.)
1.3 ✅ Http / Request
- ✅
core/Http/Request.php(449 стр.) ←cleanGlobals()+parseIncomingRecursively() - ✅
core/Http/Response.php(139 стр.)
1.4 ✅ Auth
- ✅
core/Auth/SessionManager.php(305 стр.) ← из двухsession.php
1.5 ✅ Process
- ✅
core/Process/ProcessManager.php(366 стр.) ← shell_exec/posix_kill
1.6 ✅ Util
- ✅
core/Util/GeoIP.php(159 стр.) ←CoreUtilities - ✅
core/Util/Encryption.php(137 стр.) ←CoreUtilities - ✅
core/Util/TimeUtils.php(114 стр.) ←CoreUtilities+admin.php::secondsToTime() - ✅
core/Util/NetworkUtils.php(129 стр.) ← IP/CIDR/subnet изCoreUtilities
Фаза 1.7: Оставшиеся извлечения core/
Из CoreUtilities.php (4847 строк) → новые классы:
Шаг 1.7.1 — Логирование ✅
- ✅ Извлечь
saveLog()(стр. 489–504) →FileLogger::log() - ✅ Извлечь
clientLog()(StUtil стр. 1169–1177) →DatabaseLogger::log() - ✅ Написать
LoggerInterfaceс методомlog($type, $message, $extra) - ✅ Proxy:
CoreUtilities::saveLog()→FileLogger::log() - ✅ Proxy:
StreamingUtilities::clientLog()→DatabaseLogger::log()
Шаг 1.7.2 — Системная информация ✅
- ✅ Извлечь 9 методов (стр. 534–671, 827–852, 1338–1344, 4621–4665) →
SystemInfo - ✅ Proxy: каждый вызов
CoreUtilities::getStats()→SystemInfo::getStats()
Шаг 1.7.3 — Защита от брутфорса (Flood/Bruteforce) ✅
- ✅ Извлечь 4 метода из CoreUtilities (стр. 709–826) →
BruteforceGuard - ✅ 4 дублированных метода в StreamingUtilities (стр. 215–394) → proxy на тот же
BruteforceGuard - ✅ Первая дедупликация CoreUtilities ↔ StreamingUtilities
Шаг 1.7.4 — HTTP-клиент (cURL) ✅
- ✅ Извлечь 3 метода (стр. 373–428, 1408–1442, 3568–3578) →
CurlClient
Шаг 1.7.5 — Событийная система ✅
- ✅ Написать простой EventDispatcher (publish/subscribe)
- ✅ Пока без подписчиков — подготовка к модульной системе
Шаг 1.7.6 — RBAC / Авторизация ✅
- ✅ Извлечь
hasPermissions()(стр. 582–619) иhasResellerPermissions()(стр. 575–581) - ✅ Массив
$rAdvPermissions→resources/data/admin_constants.php - ✅ Proxy: глобальные функции в
admin.php→Authorization::check()
Шаг 1.7.7 — Аутентификация ✅
- ✅ Извлечь
processLogin()изadmin_api.php(стр. 1399–1478) →Authenticator::login() - ✅ Извлечь
processLogin()изreseller_api.php→Authenticator::resellerLogin() - ✅ Proxy:
API::processLogin()→Authenticator::login()
Шаг 1.7.8 — Утилиты изображений ✅
- ✅ Извлечь методы работы с изображениями (resize, thumbnail, URL-валидация)
- ✅
StreamingUtilities::validateImage()(стр. 1355) →ImageUtils::validateURL() - ✅
admin.php::getAdminImage()(стр. 871–883) →ImageUtils::resize()
Фаза 2: Дедупликация CoreUtilities ↔ StreamingUtilities ✅
Форк устранён. 53 дублированных метода дедуплицированы.
Шаг 2.1 — Инвентаризация дубликатов ✅
Инвентаризация выполнена: 53 общих метода между CoreUtilities и StreamingUtilities. Все методы распределены по шагам 2.2–2.5 и успешно дедуплицированы.
Шаг 2.2 — Redis и сигналы ✅
- ✅ Извлечь Redis-подключение в
RedisManager(единый для обоих) - ✅ Proxy: оба класса вызывают
RedisManager::connect() - ✅
getDomainName()→DomainResolver::resolve()(чистая утилита конфигурации)
Шаг 2.3 — Трекинг подключений (Redis) ✅
- ✅ ~17 методов вынесены в
ConnectionTracker(работа с Redis-ключами подключений) - ✅ Proxy: оба god-объекта делегируют в
ConnectionTracker
Шаг 2.4 — Справочные данные и сортировки ✅
- ✅ Сортировки и форматирование вынесены в
StreamSorter - ✅ Данные-справочники вынесены в
BouquetMapper/CategoryRepository/BouquetRepository/ServerRepository/SettingsRepository
Шаг 2.5 — Дедупликация init() ✅
- ✅ Оба
init()стали тонкими обёртками - ✅ Общая логика вынесена в
core/Init/LegacyInitializer.php - ✅ Состояние init синхронизируется через
ServiceContainer
Фаза 3: Извлечение domain/ — бизнес-логика ✅
Все entity/repository/service извлечены из god-объектов. Proxy-методы оставлены в исходных файлах.
Шаг 3.1 — domain/Stream/ ✅
StreamService.php— processStream, massEdit, massDelete, move, replaceDNS ← admin_api.phpChannelService.php— processChannel, massEdit, setOrder ← admin_api.phpCategoryService.php— processCategory, orderCategories ← admin_api.phpStreamRepository.php— getStream, getStreamStats, getStreamErrors, getStreamPIDs, getStreamOptions, getStreamSys, getNextOrder ← admin.phpCategoryRepository.php— getCategories ← admin.phpM3UParser.php— parseM3U ← admin.phpStreamProcess.php— startMonitor, stopStream, startProxy, startThumbnail, queueChannel, createChannel, updateStream(s), createChannelItem, startMovie, stopMovie, startLoopback, queueMovie(s), refreshMovies, startStream, startLLOD ← CoreUtilities
Шаг 3.2 — domain/Vod/ ✅
MovieService.php— process, massEdit, massDelete, import ← admin_api.phpSeriesService.php— process, massEdit, massDelete, import ← admin_api.phpEpisodeService.php— process, massEdit, massDelete ← admin_api.phpSeriesRepository.php— getList, updateFromTMDB, queueRefresh, getSimilar, generatePlaylist ← admin.phpMovieRepository.php— getSimilar, deleteFile ← admin.php
Шаг 3.3 — domain/Line/ ✅
LineService.php— process, massEdit, massDelete, delete, update (одиночные и массовые) ← admin_api.php + CoreUtilitiesLineRepository.php— deleteMany ← admin.php
Шаг 3.4 — domain/User/ ✅
UserService.php— process, massEdit, massDelete ← admin_api.phpGroupService.php— process ← admin_api.phpProfileService.php— editAdminProfile ← admin_api.phpUserRepository.php— getUserInfo, getUser, getRegisteredUser(s), getResellers, getDirectReports, getSubUsers, getParent, getStreamingUserInfo ← admin.php + StreamingUtilitiesGroupRepository.php— getAll, getById, deleteById ← admin.php
Шаг 3.5 — domain/Device/ ✅
MagService.php— process, massEdit, massDelete ← admin_api.phpEnigmaService.php— process, massEdit, massDelete ← admin_api.phpDeviceSync.php— syncLineDevices ← admin.php
Шаг 3.6 — domain/Server/ ✅
ServerService.php— process, processProxy, install, reorder ← admin_api.phpServerRepository.php— getAllSimple, getStreamingSimple, getProxySimple, getFreeSpace, getStreamsRamdisk, killPID, getRTMPStats, deleteById, probeSource, getSSLLog, checkSource, freeTemp, freeStreams ← admin.php
Шаг 3.7 — domain/Bouquet/ ✅
BouquetService.php— process, reorder, sort, scan, scanOne ← admin_api.php + admin.phpBouquetRepository.php— getAllSimple, getOrder, getUserBouquets ← admin.php
Шаг 3.8 — domain/Epg/ ✅
EpgService.php— process, getChannelEpg ← admin_api.php + admin.phpEpgRepository.php— getById, findByName, getStreamEpg, getStreamsEpg, getProgramme, search ← admin.php + CoreUtilities
Шаг 3.9 — domain/Settings/ + domain/Ticket/ ✅
SettingsService.php— edit, editBackup, editCacheCron ← admin_api.phpTicketService.php— submit ← admin_api.php
Шаг 3.10 — domain/Security/ ✅
BlocklistService.php— processISP, processUA, blockIP, processRTMPIP, checkBlockedUAs, checkISP, checkServer ← admin_api.php + StreamingUtilitiesBlocklistRepository.php— getBlockedIPsSimple, getRTMPIPsSimple, getBlockedUA, getBlockedIPs, getBlockedISP, getBlockedServers, getProxyIPs ← admin.php + CoreUtilities
Шаг 3.11 — domain/Auth/ ✅
CodeService.php— process ← admin_api.phpHMACService.php— process ← admin_api.phpPackageService.php— process ← admin_api.php (в domain/Line/)CodeRepository.php— getActiveCodes, updateCodes, getCurrentCode ← admin.phpHMACRepository.php— getAll, getById ← admin.phpPackageRepository.php— deleteById ← admin.php (в domain/Line/)HMACValidator.php— validate ← StreamingUtilities
Шаг 3.12 — Playlist-генератор ✅
PlaylistGenerator.php— generate ← CoreUtilities (самый длинный метод, live/vod/series/radio)CronGenerator.php— generate ← CoreUtilities
Фаза 4: Извлечение streaming/ (hot path) ✅
Шаг 4.1 — streaming/Auth/ ✅
StreamAuth.php— checkAccess, validateConnections ← auth.php + StreamingUtilitiesTokenAuth.php— парсинг токеновDeviceLock.php— привязка к устройствуConnectionLimiter.php— closeConnections, closeRTMP → Protection/
Шаг 4.2 — streaming/Delivery/ ✅
LiveDelivery.php← www/stream/live.phpStreamRedirector.php— redirectStream, showVideoServerOffAirHandler.php— getOffAirVideoHLSGenerator.php— generateHLSSegmentReader.php— getLLODSegmentsSignalSender.php— sendSignalProxySelector.php→ Balancer/
Шаг 4.3 — streaming/Codec/ ✅
FFprobeRunner.php— probeStream, parseFFProbeFFmpegCommand.php— сборка FFmpeg-командSubtitleExtractor.php— extractSubtitle
Шаг 4.4 — streaming/Protection/ + Health/ ✅
ConnectionLimiter.php— closeConnectionsHealthChecker.php— isRunningProcessChecker.php— isPIDRunning, isPIDsRunning, checkPIDWatchdogMonitor.php— getWatchdog
Шаг 4.5 — streaming/StreamingBootstrap.php ✅
- Лёгкий bootstrap заменяет
www/stream/init.php - Загружает: autoload → constants → Database → Redis
Фаза 5: Вынесение модулей
Каждый модуль извлекается атомарно — система продолжает работать без него.
Шаг 5.1 — modules/plex/ ⚡ (в процессе)
Завершено:
- ✅
PlexAuth.php— аутентификация (getPlexToken, checkPlexToken, cachePlexToken и др.) - ✅
PlexRepository.php— данные (getPlexServers, getPlexSections) - ✅
PlexService.php— бизнес-логика (editPlexSettings, processPlexSync, forcePlex) - ✅
PlexCron.php— крон синхронизации (Thread, Multithread, PlexCron::run()) - ✅
PlexItem.php— CLI обработка элементов (PlexItem::run()) - ✅
crons/plex.php— тонкая обёртка → PlexCron::run() - ✅
includes/cli/plex_item.php— тонкая обёртка → PlexItem::run() - ✅ Все 5 классов зарегистрированы в autoload.php
- ✅
admin/settings_plex.php→ modules/plex/views/ (контроллер + вынесенный view/scripts) - ✅
admin/plex_add.php→ modules/plex/views/ (контроллер + общие view/scripts) - ✅
admin/plex.php→ modules/plex/views/ (контроллер + список вынесен в view) - ✅
config/plex/→ modules/plex/config/
Осталось: —
Шаг 5.2 — modules/watch/
- ✅
API::editWatchSettings()→ modules/watch/WatchService.php - ✅
API::processWatchFolder()→ modules/watch/WatchService.php - ✅
API::scheduleRecording()→ modules/watch/RecordingService.php - ✅
admin.php::getWatchFolders()→ modules/watch/WatchController.php - ✅
admin.php::getWatchCategories()→ modules/watch/WatchController.php - ✅
admin.php::forceWatch()→ modules/watch/WatchController.php - ✅
admin.php::getRecordings()→ modules/watch/WatchController.php - ✅
admin.php::deleteRecording()→ modules/watch/WatchController.php - ✅
admin/settings_watch.php→ modules/watch/views/ (thin wrapper + view/scripts) - ✅
admin/watch.php→ modules/watch/views/ (thin wrapper + view/scripts) - ✅
admin/watch_add.php→ modules/watch/views/ (thin wrapper + view/scripts) - ✅
admin/watch_output.php→ modules/watch/views/ (thin wrapper + view/scripts) - ✅
admin/record.php→ modules/watch/views/ (thin wrapper + view/scripts) - ✅
crons/watch.php→ modules/watch/WatchCron.php (entry point + extracted class) - ✅
includes/cli/watch_item.php→ modules/watch/WatchItem.php (entry point + extracted class) - ✅
WatchModule::registerCrons()wired to WatchCron
Осталось: —
Шаг 5.3 — modules/tmdb/
- ✅
admin/api.php::tmdb_search→ modules/tmdb/TmdbService.php (search) - ✅
admin/api.php::tmdb→ modules/tmdb/TmdbService.php (getDetails) - ✅
crons/tmdb.php→ modules/tmdb/TmdbCron.php (entry point + extracted class) - ✅
crons/tmdb_popular.php→ modules/tmdb/TmdbPopularCron.php (entry point + extracted class) - ✅
TmdbModule.php— registerCrons() wired (TmdbCron + TmdbPopularCron) - ✅
includes/libs/TMDb/→ modules/tmdb/lib.php (proxy loader, lib stays in place)
Осталось: —
Шаг 5.4 — modules/ministra/
- ✅
ministra/portal.php(2156 строк) → PortalHandler.php (15 статических обработчиков, ~1345 строк) - ✅
ministra/portal.phphelper functions → PortalHelpers.php (19 статических методов, ~860 строк) - ✅
MinistraModule.php— точка входа модуля (standalone endpoint, без cron/routes) - ✅
ministra/portal.php→ тонкая обёртка: init/auth → PortalHandler (269 строк + хелперы + комментированный оригинал) - 🔲
ministra/*.js→ modules/ministra/assets/ (JS-файлы портала — отложено)
Осталось: JS-ассеты (отложено до Фазы 6)
Шаг 5.5 — modules/fingerprint/ + modules/theft-detection/ + modules/magscan/
- ✅
FingerprintModule.php— модуль Fingerprint Stream (выбор потока + наложение текста + таблица активности) - ✅
TheftDetectionModule.php— модуль обнаружения кражи VOD (таблица просмотров, данные из cache_engine) - ✅
MagscanModule.php— модуль настроек MAGSCAN (белые/чёрные списки MAC + IP) - ✅
module.json× 3 — метаданные модулей - ✅
config/modules.php— все 3 модуля раскомментированы и включены
Источники:
admin/fingerprint.php (330 стр.) — UI-страница, API: fingerprint + line_activity
admin/theft_detection.php (239 стр.) — UI-страница, данные: CACHE_TMP_PATH/theft_detection
admin/magscan_settings.php (301 стр.) — UI-страница, POST: submit_magscan
Шаг 5.6 — ModuleInterface + загрузчик модулей
- ✅
core/Module/ModuleInterface.php— контракт модуля (111 строк): getName, getVersion, boot, registerRoutes, registerCrons, getEventSubscribers - ✅
core/Module/ModuleLoader.php— загрузчик модулей (243 строки): loadAll, load, checkDependencies, isLoaded, getModule - ✅
config/modules.php— конфигурация модулей: plex, watch, fingerprint, theft-detection, magscan (enabled), tmdb, ministra (commented)
Аудит Фазы 5 ✅
Проведена полная проверка всех файлов Фазы 5. Найдено и исправлено:
- КРИТИЧЕСКОЕ:
Thread/Multithreadдублировались в 3 файлах (PlexCron, WatchCron, cache_engine) → вынесены вcore/Process/Thread.php+core/Process/Multithread.php, все 3 файла переведены наrequire_once - autoload.php: добавлены 14 отсутствующих записей (WatchCron, WatchItem, Tmdb*, Ministra*, Fingerprint*, TheftDetection*, Magscan*, Thread, Multithread)
- module.json: созданы для
tmdbиministra(отсутствовали) - config/modules.php:
tmdbиministraраскомментированы и включены (былиenabled => false)
Фаза 6: Контроллеры и Views (admin/reseller) ⚡ (в процессе)
Выполняется ПОСЛЕ извлечения domain/ — контроллеры вызывают сервисы.
Шаг 6.1 — Единый layout
admin/header.php (675 стр.) + reseller/header.php (284 стр.)
→ interfaces/Http/Views/layouts/admin.php
admin/footer.php (804 стр.) + reseller/footer.php (719 стр.)
→ interfaces/Http/Views/layouts/footer.php
+ assets/admin/js/*.js (вынесенный inline JS)
Сделано (старт шага 6.1):
- ✅ Создан
interfaces/Http/Views/layouts/admin.php— unified header wrapper (renderUnifiedLayoutHeader) - ✅ Создан
interfaces/Http/Views/layouts/footer.php— unified footer wrapper (renderUnifiedLayoutFooter) - ✅ Удалён
interfaces/Http/Views/layouts/.gitkeep - ✅ Переведены на unified wrappers (пилот):
admin/fingerprint.phpadmin/magscan_settings.phpadmin/theft_detection.php
- ✅ Переведены на unified wrappers (группа F — настройки):
admin/settings.phpadmin/profile.phpadmin/edit_profile.php
- ✅ Переведены на unified wrappers (группа E — букеты):
admin/bouquets.phpadmin/bouquet.phpadmin/bouquet_order.phpadmin/bouquet_sort.php
- ✅ Переведены на unified wrappers (группа D — серверы):
admin/servers.phpadmin/server.phpadmin/server_view.phpadmin/server_install.php
- ✅ Переведены на unified wrappers (группа C — линии/устройства):
admin/lines.phpadmin/line.phpadmin/mags.phpadmin/mag.phpadmin/enigmas.phpadmin/enigma.php
- ✅ Переведены на unified wrappers (группа B — VOD):
admin/movies.phpadmin/movie.phpadmin/series.phpadmin/serie.phpadmin/episodes.phpadmin/episode.php
- ✅ Переведены на unified wrappers (группа A — потоки):
admin/streams.phpadmin/stream.phpadmin/stream_view.phpadmin/created_channel.phpadmin/created_channels.php
- ✅ Переведены на unified wrappers (группа G — остальное, пакет 1):
admin/archive.phpadmin/dashboard.phpadmin/epg.phpadmin/epgs.phpadmin/group.phpadmin/groups.php
- ✅ Переведены на unified wrappers (группа G — остальное, пакет 2):
admin/providers.phpadmin/provider.phpadmin/packages.phpadmin/package.phpadmin/profiles.phpadmin/proxies.php
Дальше в 6.1:
- 🔄 Перенос общих CSS/JS-блоков из
admin|resellerв единые partials - 🔄 Подключение
partials/header.php+partials/footer.phpк layout system (сейчас не используются) - ✅
Постраничное переключение include— завершеноheader.php/footer.php→ unified layout wrappers
Reseller миграция на unified wrappers:
- ✅ Переведены (ранее):
dashboard.php,streams.php,live_connections.php,user.php,users.php,user_logs.php - ✅ Переведены (batch 2):
movies.php,enigma.php(footer fix),epg_view.php,enigmas.php,tickets.php,ticket_view.php,ticket.php,radios.php,mags.php,mag.php,line_activity.php,lines.php,line.php,episodes.php,created_channels.php,edit_profile.php - ℹ️ Не требуют миграции (utility/support):
header.php,footer.php,functions.php,session.php,api.php,table.php,post.php,topbar.php,modals.php,resize.php,index.php,logout.php - ℹ️ Отдельная страница (свой HTML):
login.php - Admin: 112/112 page-файлов — 100% мигрированы
- Reseller: 22/22 page-файлов — 100% мигрированы
Шаг 6.2 — Router + Front Controller
Новый → core/Http/Router.php
Новый → interfaces/Http/public/index.php (front controller)
Новый → interfaces/Http/routes/admin.php (admin route definitions)
Новый → interfaces/Http/routes/api.php (API route definitions)
Статус 6.2: ✅ Завершён
- ✅
core/Http/Router.php— полная реализация (450 стр.): GET/POST/API routes, group(), middleware, permissions, dispatch(), dispatchApi(), singleton- Fix: normalizePage() —
buildRoute()теперь вызываетnormalizePage()на результате, чтобы ключи регистрации совпадали с ключами dispatch (rtmp_ips → rtmp/ips) - Fix: ServiceContainer DI —
callHandler()теперь используетServiceContainer::getInstance()->get($class)с fallback наnew $class()при ошибке
- Fix: normalizePage() —
- ✅
core/Http/Request.php— HTTP request wrapper (450 стр.): capture(), input access, sanitization, backward compat - ✅
core/Http/Response.php— HTTP response helper: json(), redirect(), notFound(), cors(), noCache() - ✅
core/Http/RequestGuard.php— flood protection, host verification, Logger init - ✅
interfaces/Http/public/index.php— Front Controller:- Трёхрежимный парсинг URL → scope + pageName + accessCode:
- Режим A (Access Code + XC_SCOPE): nginx передаёт scope через
fastcgi_param XC_SCOPE - Режим B (Direct URL): URL содержит
/admin/...или/reseller/... - Режим C (Access Code без XC_SCOPE): fallback через
PHP_SELF(какCodeRepository::getCurrentCode)
- Режим A (Access Code + XC_SCOPE): nginx передаёт scope через
- Маппинг
#TYPE#→ scope: admin, reseller, ministra, includes/api/admin → admin, и т.д. - Bootstrap через legacy chain (session.php → functions.php)
- Загрузка маршрутов из routes/{scope}.php + routes/api.php
- Dispatch через Router → fallback в legacy include
- Поддержка API action dispatch (/admin/api?action=xxx)
- Трёхрежимный парсинг URL → scope + pageName + accessCode:
- ✅
interfaces/Http/routes/admin.php— шаблон маршрутов admin (заглушки для Step 6.3) - ✅
interfaces/Http/routes/api.php— шаблон API маршрутов (заглушки для Step 6.3)
Access Codes (bin/nginx/conf/codes/):
Панель доступна не по /admin/, а по /RANDOMCODE/. Коды генерируются из шаблона:
- Шаблон:
bin/nginx/conf/codes/template - Генератор:
domain/Auth/CodeRepository.php→updateCodes() - URL:
http://host/MYCODE/dashboard→ nginx alias →/home/xc_vm/admin/dashboard.php - Типы кодов (field
typeв таблице):
| type | #TYPE# (alias path) | scope (Router) |
|---|---|---|
| 0 | admin | admin |
| 1 | reseller | reseller |
| 2 | ministra | ministra |
| 3 | includes/api/admin | admin |
| 4 | includes/api/reseller | reseller |
| 5 | ministra/new | ministra |
| 6 | player | player |
Активация — два варианта:
- Прямой доступ (dev/test, без access codes):
location ~ ^/(admin|reseller)(/.*)?$ {
try_files $uri /interfaces/Http/public/index.php?$args;
}
- Access codes (production) — обновлённый шаблон
bin/nginx/conf/codes/template:
location ^~ /#CODE# {
alias /home/xc_vm/#TYPE#;
index index.php;
# Статика и legacy PHP отдаются напрямую
try_files $uri $uri.html $uri/ @fc_#CODE#;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
}
}
# Front Controller fallback для access code #CODE#
location @fc_#CODE# {
fastcgi_param XC_SCOPE #TYPE#;
fastcgi_param XC_CODE #CODE#;
fastcgi_param SCRIPT_FILENAME /home/xc_vm/interfaces/Http/public/index.php;
fastcgi_pass php;
include fastcgi_params;
}
Пока nginx шаблон не обновлён — legacy URL через access codes (/MYCODE/dashboard → admin/dashboard.php) работают как раньше. Front Controller активируется только после обновления template.
Шаг 6.3 — Конвертация admin-страниц (Controller/View)
Паттерн: Thin Controller + View файл
Каждая legacy admin-страница разделяется на:
- Controller (
interfaces/Http/Controllers/Admin/XxxController.php) — подготовка данных, редиректы, проверка прав - View (
interfaces/Http/Views/admin/xxx.php) — только HTML-контент (между header и footer) - Scripts (
interfaces/Http/Views/admin/xxx.scripts.php) — page-specific JS (DataTables, api(), etc.)
**✅ interfaces/Http/Controllers/Admin/BaseAdminController.php — render(), redirect(), json(), setTitle(), requirePermission(), requireAdvPermission(), input(), getStatus()
- ✅
interfaces/Http/Views/admin/_scripts_init.php— общий JS-бойлерплейт (ResizeObserver, Switchery, DataTable errMode, bindHref, inputFilter, js_navigate и т.д.) - ✅
interfaces/Http/Views/layout.php— мёртвый код, помечен@deprecated(актуальный layout:Views/layouts/admin.php+footer.php) - ✅
interfaces/Http/Views/partials/header.php+footer.php— мёртвый код, помечены@deprecated
Render flow: renderUnifiedLayoutHeader('admin') → view.php → renderUnifiedLayoutFooter('admin') → scripts.php (включает _scripts_init.php) → </body></html>
Инфраструктурные исправления перед Phase 6.3:
- ✅ Router
buildRoute()— нормализация ключей при регистрации (rtmp_ips → rtmp/ips) - ✅ Router
callHandler()— DI через ServiceContainer с fallback - ✅ Autoloader — раскомментированы
domain/,streaming/,modules/, добавленinterfaces/ - ✅ Autoloader —
MAIN_HOMEfallback:__DIR__ . '/'вместо/home/xc_vm/ - ✅ Мёртвый Layout код (layout.php + partials/header.php + partials/footer.php) помечен
@deprecated
Пилотная группа (простые листинги, no POST, inline DataTables):
- ✅
IpController.php+Views/admin/ips.php+ips.scripts.php— Blocked IPs, flush action, getBlockedIPs() - ✅
IspController.php+Views/admin/isps.php+isps.scripts.php— Blocked ISPs, getISPs() - ✅
HmacController.php+Views/admin/hmacs.php+hmacs.scripts.php— HMAC Keys, getHMACTokens() - ✅
GroupController.php+Views/admin/groups.php+groups.scripts.php— Member Groups, getMemberGroups(), adv:edit_group - ✅
CodeController.php+Views/admin/codes.php+codes.scripts.php— Access Codes, getcodes(), type mapping - ✅
PackageController.php+Views/admin/packages.php+packages.scripts.php— Packages (filtered !is_addon), getPackages() - ✅
RtmpIpController.php+Views/admin/rtmp_ips.php+rtmp_ips.scripts.php— RTMP IPs, getRTMPIPs(), push/pull icons - ✅
ProfileController.php+Views/admin/profiles.php+profiles.scripts.php— Transcode Profiles, getTranscodeProfiles(), JSON profile_options - ✅
ProviderController.php+Views/admin/providers.php+providers.scripts.php— Stream Providers, getStreamProviders(), reload action, JSON data - ✅
TheftDetectionController.php+Views/admin/theft_detection.php+theft_detection.scripts.php— VOD Theft Detection, igbinary cache, range filter, custom search - ✅ Маршруты в
interfaces/Http/routes/admin.php— 10 GET-маршрутов зарегистрированы
Статус 6.3: ✅ Завершён — 111/111 admin-страниц мигрировано
| Артефакт | Кол-во | Статус |
|---|---|---|
Контроллеры (Controllers/Admin/) |
111 + BaseAdminController | ✅ |
View-прокси (Views/admin/) |
111 | ✅ |
Маршруты (routes/admin.php) |
111 | ✅ |
Legacy-обёртки ($__viewMode) |
111 (108 std + 1 settings variant + 2 module bypass) | ✅ |
Группы миграции (все завершены):
- ✅ Pilot (10): ips, isps, hmacs, groups, codes, packages, rtmp_ips, profiles, providers, theft_detection
- ✅ A (19): streams, stream, stream_view, stream_mass, stream_categories, stream_category, stream_errors, stream_rank, stream_review, stream_tools, channel_order, created_channel, created_channels, created_channel_mass, live_connections, rtmp_monitor, radio, radios, radio_mass
- ✅ B (10): movies, movie, movie_mass, series, serie, series_mass, episodes, episode, episodes_mass, ondemand
- ✅ C (6): lines, line, line_mass, line_activity, line_ips, mass_delete
- ✅ D (4): server_order, server_install, server, server_view
- ✅ E (4): bouquets, bouquet, bouquet_order, bouquet_sort
- ✅ F (4): settings, settings_plex, settings_watch, quick_tools
- ✅ G (6): login_logs, mysql_syslog, mag_events, restream_logs, panel_logs, epgs
- ✅ H (9): dashboard, cache, queue, process_monitor, backups, client_logs, user_logs, useragent, useragents
- ✅ I (6): mags, mag, mag_mass, enigmas, enigma, enigma_mass
- ✅ J (6): epg, epg_view, code, hmac, ip, isp
- ✅ K (5): group, package, profile, provider, rtmp_ip
- ✅ L (5): users, user, user_mass, plex, plex_add
- ✅ M (8): tickets, ticket, ticket_view, serie, watch, watch_add, watch_output, magscan_settings
- ✅ N (9): credit_logs, edit_profile, fingerprint, proxies, proxy, record, review, archive, asns
16 non-page файлов (миграция не требуется): header.php, footer.php, topbar.php, modals.php, functions.php, session.php, post.php, login.php, logout.php, setup.php, database.php, index.php, resize.php, player.php, api.php, table.php
Шаг 6.4 — Объединение admin/reseller ✅
Статус: Завершён — 22 reseller-страницы мигрированы.
Артефакты:
- ✅
BaseResellerController— расширяет BaseAdminController,$scope = 'reseller', переопределяетrequirePermission()→checkResellerPermissions() - ✅ 22 контроллера (
Reseller*Controller) вinterfaces/Http/Controllers/Reseller/ - ✅ 22 view proxy в
interfaces/Http/Views/reseller/ - ✅ 22 legacy файла обёрнуты
$__viewModeгардами - ✅
interfaces/Http/routes/reseller.php— 22 маршрута
Reseller-страницы (22): Dashboard & Profile: dashboard, edit_profile Lines: lines, line, line_activity, live_connections Devices: mags, mag, enigmas, enigma Content: streams, movies, radios, episodes, created_channels, epg_view Tickets: tickets, ticket, ticket_view Users: users, user, user_logs
Ключевые отличия от admin:
- Контроллеры с префиксом
Reseller(ResellerDashboardController и т.д.) для избежания конфликтов с admin-контроллерами в безнеймспейсном автозагрузчике checkResellerPermissions()вместоcheckPermissions()$_SESSION['reseller']вместо$_SESSION['hash']
reseller/table.php (1836 стр.) → переиспользует admin-контроллеры с RBAC-фильтром
reseller/api.php (997 стр.) → ResellerApiController (ограниченный AdminApiController)
Шаг 6.5 — Стабилизация Controller/View контракта ✅
Статус: Завершён (первый стабилизационный проход после 6.3/6.4).
Сделано:
- ✅ Расширен базовый контракт
BaseAdminController::$viewGlobals:- добавлены
rTMDBLanguages,rGeoCountries,rMAGs,rTimezones - ранее добавлен
allowedLangs
- добавлены
- ✅ Исправлен рендер update-уведомления/changelog в
admin/settings.php:- защита
foreachпоchangelogчерезis_array - защита вложенного
changesчерез безопасный fallback на[]
- защита
- ✅ Исправлен
modules/watch/views/watch_add.php:- защита
foreachпо$rTMDBLanguagesпри отсутствии/некорректном типе
- защита
- ✅ Второй стабилизационный проход (расширенный, 14 файлов):
- контроллеры
watch/plex/settings_*теперь нормализуют массивы ([]fallback) - в
watch/plex/profileview добавлены безопасныеforeachиin_array/countс(array)/is_arrayguard - закрыты риски
foreach(null)иUndefined variableдля страницwatch_output,watch_add,settings_watch,record,plex/index,plex/settings,plex/library_edit,admin/edit_profile
- контроллеры
Цель шага:
- убрать class-runtime регрессий «Undefined variable / foreach() argument must be array|object»
- зафиксировать единый контракт данных для legacy view в controller-режиме
Дальше (следующий проход 6.5):
- 🔄 точечный аудит
Reseller-view (особенно echo-сгенерированные файлы) наforeach/in_arrayс nullable-данными - 🔄 выборочно перенести оставшиеся high-risk случаи в контроллерный data-contract вместо локальных view-guard
Фаза 7: Миграция admin.php bootstrap
admin.php (4448 стр.) — после извлечения всех функций в domain/.
Шаг 7.1 — Вынос inline-данных ✅
- ✅ Данные bootstrap (
rMAGs,rCountryCodes,rCountries,rAdvPermissions) консолидированы вresources/data/admin_constants.php - ✅ Отдельный
resources/data/permissions.phpудалён
Шаг 7.2 — Замена процедурного bootstrap 🔄
admin.php::initDatabase() → ServiceContainer::get('db')
admin.php::CoreUtilities::init() → bootstrap.php
admin.php::session_start() → SessionManager::start()
admin.php::$rPermissions → Authorization::getPermissions()
admin.php::Translator init → ServiceContainer::get('translator')
- ✅ Первый инкремент: инициализация runtime в
includes/admin.phpвынесена вbootstrapAdminRuntime()(поведение сохранено) - ✅ Второй инкремент: session/runtime-логика вынесена в
includes/bootstrap/admin_session.phpиincludes/bootstrap/admin_runtime.php(без изменения поведения) - ✅ Третий инкремент:
includes/bootstrap/admin_runtime.phpпереключён наXC_Bootstrap::boot(XC_Bootstrap::CONTEXT_ADMIN)с legacy fallback - ✅ Четвёртый инкремент: прямые
require_onceзависимостей (DatabaseHandler/CoreUtilities/API/Translator/MobileDetect) удалены изincludes/admin.phpи централизованы вincludes/bootstrap/admin_runtime.php - ✅ Пятый инкремент:
includes/admin.phpбольше не подключаетwww/constants.phpнапрямую — инициализация идёт через bootstrap-поток - ✅ Шестой инкремент: убрана двойная
shutdown-регистрация — в bootstrap-ветке используетсяXC_Bootstrap, в legacy fallback регистрируется локальный close-db shutdown callback - ✅ Седьмой инкремент:
STATUS_*константы удалены изincludes/admin.phpи централизованы черезbootstrapAdminStatusConstants()(XC_Bootstrap::defineStatusConstants()+ fallback) - ✅ Восьмой инкремент: bootstrap-цепочка в
includes/admin.phpсведена к фасадуincludes/bootstrap/admin_bootstrap.php(bootstrapAdminInclude())
Шаг 7.3 — Удаление proxy-обёрток из admin.php ✅
- Все функции уже в domain/ (proxy-вызовы)
- Все данные в resources/data/
- Bootstrap через
bootstrap.php admin.php— сокращён с ~4448 до ~3070 строк (удалено 40 proxy-определений)
Статус (7.3 завершён):
- ✅ Первый инкремент: пакет простых proxy-обёрток вынесен из
includes/admin.phpвincludes/bootstrap/admin_proxies.php - ✅ Второй инкремент: вынесены proxy/report/permission-обёртки (
getStreamArguments,getTranscodeProfiles,getResellers,getDirectReports,hasPermissionsи связанный блок) вincludes/bootstrap/admin_proxies.php - ✅ Третий инкремент: начат прямой отказ от proxy-call-site — HMAC-ветка переведена на прямые вызовы
AuthRepository::*, proxygetHMACTokens/getHMACTokenудалены - ✅ Четвёртый инкремент: массовая замена всех внешних call-site (560+) для proxy-функций:
getBouquets()→BouquetService::getAllSimple()(47 call-sites)getStreamingServers()→ServerRepository::getStreamingSimple($rPermissions, $type)(29 call-sites)getStream()→StreamRepository::getById($rID)(~30 call-sites)getUser()→UserRepository::getLineById($rID)(~34 call-sites)getRegisteredUser()→UserRepository::getRegisteredUserById($rID)(~48 call-sites)getBouquetOrder()→BouquetService::getOrder()(6 call-sites)getMemberGroup()→GroupService::getById($rID)(4 call-sites)getBlockedIPs()→BlocklistService::getBlockedIPsSimple()(2 call-sites)getRTMPIPs()→BlocklistService::getRTMPIPsSimple()(2 call-sites)getSSLLog()→ServerRepository::getSSLLog(...)(1 call-site)getWatchdog()→WatchdogMonitor::getWatchdog(...)(1 call-site)getIP()→CoreUtilities::getUserIP()(2 call-sites)getFPMStatus()→systemapirequest(...)inline (1 call-site)- И ещё ~20 proxy-функций с 0 внешних вызовов
- ✅ Пятый инкремент: все 40 proxy-определений удалены из
admin.php - ✅ Рефакторинг:
PackageService::process()— удалены callback-параметрыgetBouquetOrder/sortArrayByArray, заменены прямыми вызовамиBouquetService::getOrder() - ⚠️ Оставлены:
getCategories()(~15 внешних вызовов, не чистый proxy — трансформирует ключи),getOutputs()(3 внешних вызова, собственная SQL-логика)
Шаг 7.4 — Устранение параметра $db из Domain-классов (global $db)
Суть: Все 28 Domain-классов (Service/Repository) принимали $db как параметр метода. Рефакторинг: убрать $db из сигнатур, внутри каждого метода объявить global $db;.
Затронутые классы (100 методов):
StreamRepository(7),StreamService(4),StreamProcess(13),ChannelService(3)SeriesService(3),MovieService(3),EpisodeService(2),PlaylistGenerator(1)CronGenerator(1),ConnectionTracker(2),ConnectionLimiter(3),CategoryService(3)UserService(2),UserRepository(5),GroupService(2),PackageService(2)LineRepository(1),LineService(4),SettingsService(3),ServerRepository(5)ServerService(4),BlocklistService(10),EpgService(3),AuthService(3)BouquetService(8),TokenAuth(1),FFmpegCommand(1),WatchdogMonitor(1)
Обновлено ~357 call-sites по всей кодовой базе:
src/includes/CoreUtilities.php— 28 вызововsrc/includes/admin_api.php— 60 вызововsrc/includes/reseller_api.php— 15 вызововsrc/includes/admin.php— 16 вызововsrc/includes/StreamingUtilities.php— 6 вызововsrc/includes/bootstrap/admin_runtime.php— 3 вызоваsrc/includes/api/admin/— 20 вызововsrc/includes/api/reseller/— 11 вызововsrc/admin/*.php— ~100 вызововsrc/reseller/*.php— 18 вызововsrc/interfaces/Http/Controllers/Admin/— 37 вызововsrc/domain/(кросс-вызовы) — 15 вызововsrc/modules/+src/crons/— 11 вызовов
Паттерн (до/после):
// Было:
public static function getById($db, $rID) { ... }
StreamRepository::getById($db, $rStreamID);
// Стало:
public static function getById($rID) {
global $db;
...
}
StreamRepository::getById($rStreamID);
Статус: ✅ Завершено
Фаза 8: Очистка и финализация
Шаг 8.1 — Удаление proxy-методов
- Обновить все вызывающие места на прямые вызовы новых классов
- Удалить proxy-методы из CoreUtilities, StreamingUtilities, admin_api.php
Шаг 8.2 — Удаление god-объектов
CoreUtilities.php→ удалить (все методы разнесены)StreamingUtilities.php→ удалить (все методы разнесены)- Если что-то осталось — вынести в утилитный класс
Шаг 8.3 — Ревизия core/
- Убедиться:
core/не содержит бизнес-логики - Убедиться:
domain/не знает обinterfaces/ - Убедиться: удаление любого модуля не ломает систему
Шаг 8.4 — Рефакторинг cli/monitor.php
- Удалить goto-лейблы (
label235,label592) - Переписать с нормальным control flow
- Вынести в
interfaces/Cli/Commands/MonitorCommand.php
Сводка: объём работы по файлам
| Исходный файл | Строк | Сколько целевых классов | Фазы |
|---|---|---|---|
CoreUtilities.php |
4847 | ~20 классов | 1.7, 2, 3, 4, 5 |
admin_api.php |
6981 | ~25 классов (Service) | 3.1–3.12, 5 |
admin.php |
4448 | ~15 классов (Repository + данные) | 3, 5, 7 |
StreamingUtilities.php |
1992 | ~10 классов | 2, 4 |
admin/post.php |
1946 | распределяется по контроллерам | 6 |
admin/table.php |
6003 | распределяется по контроллерам | 6 |
includes/api/admin/table.php |
6868 | распределяется по контроллерам | 6 |
www/stream/auth.php |
799 | 3 класса (Auth) | 4 |
www/stream/live.php |
707 | 3 класса (Delivery) | 4 |
| Прочие admin/*.php | ~100 файлов | Controller + View | 6 |
Рекомендуемый порядок работы
Фаза 1.7 → Фаза 2 → Фаза 3.1–3.4 → Фаза 4.1–4.3 → Фаза 3.5–3.12
↓
Фаза 5.1–5.5 → Фаза 5.6 → Фаза 6.1–6.4 → Фаза 7 → Фаза 8
Каждый шаг (1.7.1, 1.7.2 ...) — это одна рабочая сессия (1–3 часа). Каждая фаза (1.7, 2, 3...) — это неделя–две постепенной работы. После каждого шага система полностью работоспособна.
10. Транзакции и производительность
10.1. Кто управляет транзакциями
Правило: Транзакцией управляет Service. Контроллер и Repository не открывают транзакции.
Controller ──→ Service ──→ Repository + Infrastructure
│
├── beginTransaction()
├── ... бизнес-операции ...
├── commit()
└── (rollback при исключении)
10.2. Паттерн транзакции
class StreamService {
public function massEdit(array $streamIds, array $changes): int {
$this->db->beginTransaction();
try {
$affected = 0;
foreach ($streamIds as $id) {
$this->repository->update($id, $changes);
$affected++;
}
$this->db->commit();
return $affected;
} catch (\Throwable $e) {
$this->db->rollBack();
$this->logger->log('error', "Mass edit failed: " . $e->getMessage());
throw $e;
}
}
}
10.3. Границы по контексту
| Контекст | Транзакция | Кто управляет | Пример |
|---|---|---|---|
| Admin CRUD | Одна операция = одна транзакция | Service | StreamService::create() |
| Mass edit | Весь batch = одна транзакция | Service | StreamService::massEdit() |
| Import | Chunk по 100 записей | Service | StreamService::importM3U() |
| Cron | Каждая итерация = отдельная транзакция | CronJob | ServersCron::processServer() |
| Streaming | ❌ Нет транзакций | — | Hot path не мутирует через транзакции |
10.4. Внешние процессы
Операция «создать поток + запустить ffmpeg + обновить nginx» — не атомарна. FFmpeg/nginx — внешние процессы, откатить нельзя.
Паттерн:
- DB-операции — в транзакции
- Внешние процессы — после commit, с обработкой ошибок
- При сбое внешнего процесса — обновить статус в БД (
status = 'error')
class StreamService {
public function start(int $streamId): void {
// 1. DB: обновить статус
$this->repository->updateStatus($streamId, 'starting');
// 2. Внешние процессы (вне транзакции)
try {
$pid = $this->processManager->startFFmpeg($streamId);
$this->repository->updatePid($streamId, $pid);
$this->repository->updateStatus($streamId, 'running');
} catch (\Throwable $e) {
$this->repository->updateStatus($streamId, 'error');
$this->logger->log('error', "Start failed: {$e->getMessage()}");
throw $e;
}
}
}
10.5. Два режима работы системы
| Режим | Путь | Частота | Допустимая latency | Загрузка |
|---|---|---|---|---|
| Hot path (streaming) | www/stream/*.php |
~10K–100K req/min | < 50ms p99 | Минимальный bootstrap, никаких модулей |
| Cold path (admin) | admin/*.php, API |
~1–100 req/min | < 500ms p99 | Полный bootstrap, все модули |
10.6. Бюджет hot path
Streaming-запрос (auth.php → live.php) должен уложиться в:
Bootstrap: < 5ms (autoload + constants + DB)
Auth: < 10ms (token + Redis + bruteforce)
Stream lookup: < 5ms (Redis cache) | < 15ms (DB fallback)
Delivery: < 10ms (redirect + headers)
─────────────────────────────────────
Total: < 30ms (target) | < 50ms (max)
НЕЛЬЗЯ загружать в hot path: Router, EventDispatcher с подписчиками, ServiceContainer полный boot. МОЖНО: Database (persistent), Redis (single connection), GeoIP (mmap), streaming/*.
10.7. Async / Queue (будущее)
Длинные операции (ffprobe, tmdb, mass import) — кандидаты на async:
HTTP → Service → Queue Job (Redis LPUSH) → Worker (cron) → Result
Без RabbitMQ/Kafka — простая Redis-очередь через LPUSH/BRPOP.
11. Стратегия миграции по рискам
11.1. Принцип: каждый шаг обратим
Каждое изменение следует паттерну extract → delegate → verify → replace:
1. Extract: создать новый класс
2. Delegate: старый код вызывает новый через proxy
3. Verify: система работает как раньше + новый код работает
4. Replace: обновить вызывающий код на прямые вызовы (отдельный шаг)
Если шаг 3 провалился — откат = удалить новый класс + убрать proxy. Система возвращается в предыдущее состояние.
11.2. Регрессионная стратегия
| Уровень | Что проверяется | Как проверяется | Когда |
|---|---|---|---|
| Syntax | PHP-файлы компилируются | php -l на каждый изменённый файл |
После каждого коммита |
| Smoke | Система запускается | make new && make lb + проверка HTTP 200 |
После каждой фазы |
| Functional | Основные сценарии работают | Ручной checklist (создать поток, запустить, остановить) | После каждой фазы |
| Integration | LB-сборка работает | Деплой на тестовый LB-сервер + стриминг-тест | После фаз 1–4 |
| Backward compat | API не сломан | Проверка ответов API (формат JSON, коды ошибок) | После фазы 6 |
11.3. Dual bootstrap на переходном этапе
Текущее состояние (фазы 1–6): Dual bootstrap работает параллельно.
bootstrap.php (новый) includes/admin.php (старый legacy)
│ │
├── autoload.php ├── require Database.php
├── ServiceContainer ├── CoreUtilities::init()
├── ConfigLoader ├── API/ResellerAPI init
└── новые core/ классы └── 50 define() + global $db
Правила dual bootstrap:
bootstrap.phpзагружается ПЕРВЫМ в каждой точке входаincludes/admin.phpзагружается ПОСЛЕ — для legacy-кода, который ещё не мигрирован- Новые классы (
core/,domain/) инициализируются черезServiceContainer - Legacy-код (
CoreUtilities,admin_api.php) продолжает работать через proxy-методы - После полной миграции (Фаза 8) —
includes/admin.phpудаляется
11.4. API backward compatibility
Правило: Внешний API (player.api, xmltv.php, межсерверный API) не меняет формат ответов до Фазы 8.
Фазы 1–7: Внутренняя рефакторизация, внешний API неизменен
Фаза 8: API v2 (опционально) с новой маршрутизацией
API v1 продолжает работать через compatibility layer
11.5. Разделение релизов
| Релиз | Содержит | Риск |
|---|---|---|
| v1.8 | Фазы 0–2 (core/ extraction, dedup) | 🟢 Низкий — proxy-методы, обратная совместимость |
| v1.9 | Фазы 3–4 (domain/ + streaming/ extraction) | 🟡 Средний — больше перемещений, proxy покрывает |
| v2.0 | Фазы 5–6 (modules + controllers) | 🟡 Средний — новая маршрутизация, dual bootstrap |
| v2.1 | Фазы 7–8 (cleanup, удаление legacy) | 🔴 Высокий — удаление god-объектов, нет fallback |
11.6. Rollback plan
Если v2.0 ломает production:
1. git revert последний merge в main
2. Пересобрать: make new && make lb
3. Задеплоить предыдущую сборку
4. Post-mortem: что сломалось, почему не поймали на smoke test
Для v2.1 (удаление legacy) — feature flag:
// config.ini
[migration]
use_legacy_bootstrap = false ; true = откат на admin.php
use_legacy_api = false ; true = откат на admin_api.php switch
12. Правила для контрибьюторов
Этот раздел — краткая выжимка для тех, кто не читал весь документ. Подробности — в CONTRIBUTING.md.
12.1. Как добавить новый эндпоинт (admin)
- Найти контекст в
domain/(напримерdomain/Stream/) - Добавить метод в
StreamService.php(бизнес-логика) - Добавить SQL-метод в
StreamRepository.php(если нужен новый запрос) - Добавить action в Controller или
admin_api.php→ вызов Service php -lна изменённые файлы
12.2. Как добавить новую таблицу
- Добавить миграцию в
infrastructure/install/ - Создать Repository в
domain/{Context}/с CRUD-методами - Зарегистрировать в
autoload.php
12.3. Как НЕ сломать streaming
- Никогда не менять
www/stream/*.phpбез явного ревью - Никогда не добавлять
requireтяжёлых классов в streaming bootstrap - Не использовать EventDispatcher / Router / ServiceContainer в hot path
- Проверять latency:
< 50ms p99
12.4. Правила PR
- Один PR = одна фича/багфикс. Не смешивать рефакторинг с фичами.
php -lна все изменённые файлы перед пушем.- Если трогаете
core/илиstreaming/— обязательный ревью. - proxy-методы помечать
// @legacy-proxy — убрать в Фазе 8. - Новые классы регистрировать в
autoload.php.
12.5. Простые правила (cheat sheet)
✗ Не пиши SQL в Controller → Пиши в Repository
✗ Не пиши логику в Controller → Пиши в Service
✗ Не добавляй global → Используй constructor injection
✗ Не трогай streaming без ревью → hot path = священная территория
✗ Не модифицируй core/ из модулей → Модуль вызывает, не меняет
Приложение: Один bootstrap вместо трёх
bootstrap.php предоставляет класс XC_Bootstrap с четырьмя контекстами инициализации:
| Контекст | Что загружает | Для чего |
|---|---|---|
CONTEXT_MINIMAL |
autoload + constants + Logger | Скрипты, которым нужны только пути |
CONTEXT_CLI |
+ Database + CoreUtilities | Cron-задачи, CLI-скрипты |
CONTEXT_STREAM |
+ Database + StreamingUtilities (кэш) | Стриминг-эндпоинты (hot path) |
CONTEXT_ADMIN |
+ Database + CoreUtilities + API + ResellerAPI + Translator + session + Redis | Админ/реselлер-панель |
// interfaces/Http/public/index.php — admin/reseller entry point
require_once '/home/xc_vm/bootstrap.php';
XC_Bootstrap::defineStatusConstants();
XC_Bootstrap::boot(XC_Bootstrap::CONTEXT_ADMIN);
// $db, CoreUtilities, API, ResellerAPI, Translator — всё готово
// interfaces/Http/public/stream.php — streaming entry point (lightweight)
require_once '/home/xc_vm/bootstrap.php';
XC_Bootstrap::boot(XC_Bootstrap::CONTEXT_STREAM);
// Только Database + StreamingUtilities, без admin_api, без Translator
// interfaces/Cli/CronJobs/StreamsCron.php — cron
require_once '/home/xc_vm/bootstrap.php';
XC_Bootstrap::boot(XC_Bootstrap::CONTEXT_CLI, [
'cached' => true,
'redis' => true,
'process' => 'XC_VM[Streams]'
]);
// Database + CoreUtilities + Redis, без session и admin API
Один автозагрузчик. Один набор констант. Каждая точка входа вызывает boot() с нужным контекстом.
Новые классы миграции находятся автоматически через XC_Autoloader — без ручного require_once.