CLI consolidation: - Delete 13 legacy CLI scripts from includes/cli/ (ondemand, proxy, queue, record, scanner, signals, startup, thumbnail, tools, update, watchdog, plex_item, watch_item) - Convert status and tools entry points to thin proxies that delegate to console.php - Update all shell_exec() calls across admin controllers, views, and API layer to use console.php command syntax instead of direct CLI file paths - Update src/service to launch daemons via console.php (signals, watchdog, queue, cache_handler, startup) - Update Python src/update script to call console.php update instead of includes/cli/update.php - Update test_installer to use console.php startup Streaming deduplication: - Extract StreamAuthMiddleware — common response headers and token decryption shared by live/vod/timeshift - Extract ShutdownHandler — unified shutdown logic replacing 3 duplicate function shutdown() blocks - Refactor live.php, vod.php, timeshift.php to use new middleware classes - Add streaming micro-router to www/stream/index.php as fallback entry point Routing fixes: - Fix admin index.php redirect to use relative path (supports access code prefixes) - Add access code root redirect in public/index.php to prevent broken CSS/JS asset resolution - Fix init.php for CLI compatibility: guard $_SERVER access, define PHP_ERRORS safely Migrations: - 001_update_crontab_filenames.sql — strip .php suffix from crontab filenames - Fix cache.php view query to match new filename format (cache_engine instead of cache_engine.php)
81 KiB
XC_VM — Архитектурный план реорганизации /src
Содержание
- Стратегические цели
- Диагноз текущего состояния
- Архитектурный стиль и принципы
- Целевая структура
/src - Описание компонентов
- Карта миграции: откуда → куда
- Система модулей
- Границы ядра и модулей
- Варианты сборки: MAIN vs LoadBalancer
- Транзакции и производительность
- Правила для контрибьюторов
План миграции (фазы 0–15, стратегия рисков, порядок выполнения) вынесен в MIGRATION.md.
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 ───┤──→ 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)
public/Controllers/Admin/
└── StreamController.php # Принять запрос → вызвать Service → отдать ответ
Вот и всё. Три файла на контекст. Контрибьютор видит /Stream/ — и знает где менять.
Правила зависимостей (просто)
Controller → Service → Repository → Database
↓
Infrastructure (nginx, redis, ffmpeg)
| Слой | Можно зависеть от | НЕЛЬЗЯ зависеть от |
|---|---|---|
public/ |
domain/ (Service + Repository), core/ |
streaming/, modules/ напрямую |
domain/ |
core/ (Database, Cache, Events) |
public/, streaming/, modules/, infrastructure/ |
core/ |
Только другие core/ подкаталоги |
Всё остальное |
streaming/ |
core/ (subset), domain/ (read-only queries) |
public/, modules/ |
modules/ |
domain/ (Service, Repository), core/ |
Другие модули (без явной зависимости), public/, 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/
│ │ ├── EPG.php # XML-парсер EPG (XmlStringStreamer), MAIN-only
│ │ └── 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
│
├── public/ # ═══ ТОЧКИ ВХОДА — HTTP (UI, API) ═══
│ ├── index.php # Единая точка входа (front controller)
│ │
│ ├── Controllers/
│ │ ├── Admin/
│ │ │ ├── BaseAdminController.php
│ │ │ ├── DashboardController.php
│ │ │ ├── StreamController.php
│ │ │ ├── LineController.php
│ │ │ ├── VodController.php
│ │ │ ├── ServerController.php
│ │ │ ├── SettingsController.php
│ │ │ ├── UserController.php
│ │ │ ├── BouquetController.php
│ │ │ ├── EpgController.php
│ │ │ └── ... (111 контроллеров, по одному на страницу)
│ │ │
│ │ ├── Reseller/
│ │ │ ├── BaseResellerController.php
│ │ │ ├── DashboardController.php
│ │ │ ├── LineController.php
│ │ │ └── ... (22 контроллера)
│ │ │
│ │ └── Api/ # (planned)
│ │ ├── AdminApiController.php
│ │ ├── ResellerApiController.php
│ │ ├── PlayerApiController.php # Публичный player API
│ │ └── InternalApiController.php # Межсерверный API (текущий www/api.php)
│ │
│ ├── Views/
│ │ ├── layouts/
│ │ │ ├── admin.php # Header + footer шаблон для admin
│ │ │ └── footer.php # Единый footer
│ │ │
│ │ ├── admin/
│ │ │ ├── dashboard.php
│ │ │ ├── streams/
│ │ │ │ ├── list.php
│ │ │ │ ├── edit.php
│ │ │ │ └── view.php
│ │ │ ├── lines/
│ │ │ ├── vod/
│ │ │ ├── servers/
│ │ │ └── settings/
│ │ │
│ │ ├── reseller/
│ │ │ ├── dashboard.php
│ │ │ └── ...
│ │ │
│ │ └── partials/
│ │ ├── modals.php
│ │ ├── topbar.php
│ │ └── table.php
│ │
│ └── routes/
│ ├── admin.php
│ ├── reseller.php
│ └── api.php
│
├── player/ # Встроенный web-плеер
│ └── ... (PHP + JS + CSS)
│
├── cli/ # ═══ (PLANNED) 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
│
├── 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/, public/. Зависимости направлены только внутрь ядра.
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). Не зависит от public/ или modules/.
4.3. streaming/ — Стриминг-движок
Ответственность: Весь hot path доставки видео. Выделен отдельно от domain/ потому что:
- Критичен к производительности (нельзя загружать всю бизнес-логику)
- Имеет собственный лёгкий bootstrap
- Работает на уровне байтов/сегментов, а не на уровне CRUD
Изоляция streaming
Streaming — отдельный контекст исполнения с минимальными зависимостями:
streaming/ зависит от:
✅ core/ (Database, Redis, Logging, GeoIP, Encryption, NetworkUtils)
✅ domain/ (Repository — только SELECT-запросы: потоки, серверы, букеты)
❌ НЕ зависит от:
- public/ (контроллеры)
- 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. public/ — Точки входа
Ответственность: Получение запроса, вызов 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/, каждый крон — один файл (crons/ удалён, Phase 12.8)includes/cli/*.php→cli/Commands/(includes/cli/ удалён, Phase 12.6)
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)
СТАЛО:
public/Controllers/Admin/StreamController.php — маршрутизация
domain/Stream/StreamRepository.php — данные
public/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
СТАЛО:
public/Views/layouts/admin.php — единый layout
public/Views/layouts/footer.php — единый footer
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. Пакетная диаграмма зависимостей
┌──────────────┐
│ public/ │ ← HTTP (Controllers, Views, Routes)
└──────┬───────┘
│ 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 в одном файле. Заменены на public/Controllers/Admin/ + public/Views/admin/.
5.3 reseller/ — Легаси reseller-страницы
~35 PHP-файлов — аналогично admin/, но для реселлерской панели. Заменены на public/Controllers/Reseller/ + public/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. Проблема LB-сборки (РЕШЕНО ✅ — Phase 9.1)
Старый Makefile собирал LB из фиксированного списка LB_FILES, который не включал новые архитектурные директории. После фаз 0–8 это означало, что LB-сборка не содержала core/, domain/, streaming/, infrastructure/, resources/, а также autoload.php и bootstrap.php.
Решение (Phase 9.1): LB_FILES разделён на LB_DIRS + LB_ROOT_FILES, добавлены все необходимые директории и admin-only исключения. Подробности — §8.3.
| Директория / файл | Нужен LB? | Статус |
|---|---|---|
autoload.php |
✅ ДА | ✅ Добавлен в LB_ROOT_FILES |
bootstrap.php |
✅ ДА | ✅ Добавлен в LB_ROOT_FILES |
core/ |
✅ ДА | ✅ Добавлен в LB_DIRS |
domain/ |
⚠ ЧАСТИЧНО | ✅ В LB_DIRS + исключения User/, Device/, Auth/ |
streaming/ |
✅ ДА | ✅ Добавлен в LB_DIRS |
infrastructure/ |
✅ ДА | ✅ Добавлен в LB_DIRS |
resources/ |
⚠ ЧАСТИЧНО | ✅ В LB_DIRS + исключения langs/, libs/ |
public/ |
❌ НЕТ | ✅ Корректно отсутствует |
modules/ |
❌ НЕТ | ✅ Корректно отсутствует |
admin/ |
❌ НЕТ | ✅ Корректно отсутствует |
ministra/ |
❌ НЕТ | ✅ Корректно отсутствует |
player/ |
❌ НЕТ | ✅ Корректно отсутствует |
reseller/ |
❌ НЕТ | ✅ Корректно отсутствует |
8.3. Реализованная конфигурация Makefile ✅
LB_DIRS + LB_ROOT_FILES (вместо единого LB_FILES)
# Директории (копируются через git ls-files | grep "^src/$dir/")
LB_DIRS := bin config content core crons domain includes \
infrastructure resources signals streaming tmp www
# Root-файлы (копируются через git ls-files --error-unmatch)
LB_ROOT_FILES := autoload.php bootstrap.php service status update
Почему раздельно: grep
"^src/$item/"с trailing/никогда не матчит файлы (autoload.php,service,status,update). Поэтому root-файлы копируются отдельным циклом черезgit ls-files --error-unmatch.
LB_DIRS_TO_REMOVE (admin-only директории)
LB_DIRS_TO_REMOVE := \
bin/install \
bin/redis \
bin/nginx/conf/codes \
includes/langs \
includes/api \
includes/libs/resources \
includes/bootstrap \
domain/User \
domain/Device \
domain/Auth \
resources/langs \
resources/libs
lb_copy_files — два цикла
- Директории:
for lb_item in $(LB_DIRS)→git ls-files | grep "^src/$$lb_item/"→ copy - Root-файлы:
for root_file in $(LB_ROOT_FILES)→git ls-files --error-unmatch "src/$$root_file"→ copy
lb_update_copy_files — каскадная проверка
# Для каждого changed file:
for lb_item in $(LB_DIRS); do # 1. проверка по директории
grep -q "^$$lb_item/"
done
for root_file in $(LB_ROOT_FILES); do # 2. проверка по root-файлу
[ "$$rel_path" = "$$root_file" ]
done
set_permissions — новые директории
# core/ domain/ streaming/ infrastructure/ resources/ → dirs:755, files:644
@for arch_dir in core domain streaming infrastructure resources; do \
if [ -d "$(TEMP_DIR)/$$arch_dir" ]; then \
find "$(TEMP_DIR)/$$arch_dir" -type d -exec chmod 755 {} +; \
find "$(TEMP_DIR)/$$arch_dir" -type f -exec chmod 644 {} +; \
fi; \
done
# autoload.php, bootstrap.php → 644
chmod 0644 $(TEMP_DIR)/autoload.php 2>/dev/null || true
chmod 0644 $(TEMP_DIR)/bootstrap.php 2>/dev/null || true
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)
console.php cron:* ──→ bootstrap.php (CONTEXT_CLI)
└──→ те же зависимости + domain/Stream/*
> **Примечание:** `crons/*.php` удалены (Phase 12.8). Все вызовы идут через `console.php cron:{name}`.
8.5. Правила для разработки с учётом LB
-
Proxy-методы в— РЕШЕНО: god-объекты удалены (Phase 8), все зависимости через прямые вызовы.CoreUtilities/StreamingUtilitiesдолжны быть безопасны для LB -
Makefile обновлён синхронно с миграцией ✅ —
LB_DIRSвключает все новые директории. При добавлении новых root-директорий — обновлятьLB_DIRS. -
Тестирование 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. Makefile обновлён ✅ (Phase 9.1)
Все 5 изменений применены:
LB_FILES→LB_DIRS(14 директорий) +LB_ROOT_FILES(5 файлов)lb_copy_files— добавлен второй цикл для root-файловlb_update_copy_files— каскадная проверка (dirs → root files)LB_DIRS_TO_REMOVE— 6 новых admin-only исключенийset_permissions— корректные права наcore/,domain/,streaming/,infrastructure/,resources/,autoload.php,bootstrap.php
9. Транзакции и производительность
9.1. Кто управляет транзакциями
Правило: Транзакцией управляет Service. Контроллер и Repository не открывают транзакции.
Controller ──→ Service ──→ Repository + Infrastructure
│
├── beginTransaction()
├── ... бизнес-операции ...
├── commit()
└── (rollback при исключении)
9.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;
}
}
}
9.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 не мутирует через транзакции |
9.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;
}
}
}
9.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, все модули |
9.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/*.
9.7. Async / Queue (будущее)
Длинные операции (ffprobe, tmdb, mass import) — кандидаты на async:
HTTP → Service → Queue Job (Redis LPUSH) → Worker (cron) → Result
Без RabbitMQ/Kafka — простая Redis-очередь через LPUSH/BRPOP.
10. Правила для контрибьюторов
Этот раздел — краткая выжимка для тех, кто не читал весь документ. Подробности — в CONTRIBUTING.md.
10.1. Как добавить новый эндпоинт (admin)
- Найти контекст в
domain/(напримерdomain/Stream/) - Добавить метод в
StreamService.php(бизнес-логика) - Добавить SQL-метод в
StreamRepository.php(если нужен новый запрос) - Добавить action в Controller или
admin_api.php→ вызов Service php -lна изменённые файлы
10.2. Как добавить новую таблицу
- Добавить миграцию в
infrastructure/install/ - Создать Repository в
domain/{Context}/с CRUD-методами - Зарегистрировать в
autoload.php
10.3. Как НЕ сломать streaming
- Никогда не менять
www/stream/*.phpбез явного ревью - Никогда не добавлять
requireтяжёлых классов в streaming bootstrap - Не использовать EventDispatcher / Router / ServiceContainer в hot path
- Проверять latency:
< 50ms p99
10.4. Правила PR
- Один PR = одна фича/багфикс. Не смешивать рефакторинг с фичами.
php -lна все изменённые файлы перед пушем.- Если трогаете
core/илиstreaming/— обязательный ревью. - proxy-методы помечать
// @legacy-proxy — убрать в Фазе 8. - Новые классы регистрировать в
autoload.php.
10.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лер-панель |
// 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 — всё готово
// 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
// 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.