Plan reviewed by a multi-agent pass (technical-auditor, solution-architect, devops-lead, security-architect, qa-lead). Captures the phased approach, blockers (LB archive leak, vendor audit), per-phase test gates and rollback constraints. Source of truth for the migration.
27 KiB
План: миграция на PSR-4 + Composer-автолоадер (инкрементально)
Контекст (зачем)
src/autoload.php (XC_Autoloader) резолвит глобальные имена классов, сканируя
core/domain/infrastructure/streaming/modules/public, строит карту класс→файл и
кеширует её через igbinary (tmp/cache/autoload_map) с логикой chown под root
и записью на shutdown. Этот кеш хрупкий: требует расширение igbinary, ломал PHPStan
(полные пересканы → OOM), непрозрачен.
Цель: заменить на PSR-4 автолоадер без кеширования классов. В PSR-4 namespace
кодирует путь → резолв это прямой file_exists на класс, без скана и persistent-кеша.
Проект уже начал движение в эту сторону (XcVm\Module\* у 4 модульных классов).
Утверждённые решения: инкрементально (сначала фундамент, потом слой за слоем);
Composer с обычным PSR-4 (без оптимизированной classmap → без кеша); переименовать
7 нижних верхнеуровневых каталогов в PascalCase (core→Core и т.д. — подкаталоги/файлы
уже PascalCase); корневой namespace XcVm\. Итог: стандартный автолоадер без кеша;
легаси XC_Autoloader остаётся как fallback без кеша на время миграции, удаляется в конце.
Блокеры (обязательны до старта Фазы 1)
По итогам ревью командой агентов (technical-auditor, solution-architect, devops-lead, security-architect, qa-lead). Два пункта — security-VETO; без них Фазу 1 запускать нельзя.
- 🔴 CRITICAL — утечка привилегированного кода на LB-серверы.
MakefileLB_DIRS_TO_REMOVE(9 путей) иLB_FILES_TO_REMOVE(15 файлов) содержат lowercase-пути (domain/User,public/Controllers/Admin,public/Controllers/Reseller,cli/Commands/*,cli/CronJobs/*,public/Controllers/Api/*,domain/Epg/EPG.php). Послеgit mvв PascalCaserm -rfмолча промахивается (exit 0) → Admin/Reseller-контроллеры, auth/user-домен, install-команды иRootMysqlCronJobпопадают в LB-архив (DMZ, internet-facing) = раскрытие исходников + привилегированные endpoints. Правки Makefile обязаны быть в ТОМ ЖЕ коммите, что иgit mv. - 🟠 HIGH —
vendor/в git безcomposer audit. Нетcomposer.lock, нет audit-шага в CI → уязвимые зависимости (включая bundled M3uParser/PhpM3u8, парсят пользовательский ввод) не детектируются. Закоммититьcomposer.lock+ добавитьcd src && composer audit --no-devв CI.
Факты, определяющие план
- 707 PHP-файлов; 661 глобальных, 46 в namespace (вендорные
M3uParser,Chrisyue\PhpM3u8+ 4XcVm\Module\*). ~6439 вызововClass::, 292 extends/implements, 31 строковыйclass_exists, ~25::class(безопасны). - ~190 процедурных файлов в
public/+ bootstrap-склейка/конфиги/шаблоны (всего 236) — не автолоадятся. Глобальные функции: кандидатов дляautoload.filesбольше 20 — точный список требует ручного аудита, т.к. многие файлы это view-шаблоны и точки входа (НЕ дляfiles); вautoload.filesидут только файлы с чистыми глобальными функциями без side-effects (безecho/print/header()/$_GET/$_POST/$_SESSION/DB). Список фиксируется явно в PR Фазы 0. - Хардкод-пути
require/include MAIN_HOME . 'core/...'(и др. lowercase-каталоги) — НЕ только вbootstrap.php: 9 файлов с ~41 живым оператором —bootstrap.php(10),infrastructure/bootstrap/WebApiBootstrap.php(9),infrastructure/bootstrap/StreamingRequestBootstrap.php(6),core/Backup/BackupService.php(5),public/progress/index.php(3),cli/CronJobs/CacheEngineCronJob.php(2),streaming/StreamingBootstrap.php(1),domain/Stream/StreamService.php(1),core/Http/RequestGuard.php(1),cli/CronJobs/EpgCronJob.php(1). PHPStan их НЕ ловит (конкатенация константы с литералом) → падают только в рантайме после переименования (Фаза 1). - 5 файлов с несколькими классами надо разделить (PSR-4):
Core/Parsing/XmlStringStreamer.php(7),Core/Storage/DropboxClient.php(2) и 3×Public/Controllers/Api/*—AdminApiController.php(2),Enigma2ApiController.php(2),ResellerRestApiController.php(2). - Два динамических «хаба» ломаются под PSR-4:
console.php(glob + basename==classname; вдобавок ручнойrequire_once cli/CommandInterface.php/CommandRegistry.phpдо autoload и проверка глобальной строкойimplementsInterface('CommandInterface')) иModuleLoader— резолв двухуровневый: flatmodulePath/Short.php+ one-level globmodulePath/*/Short.php(отсюда риск тихих коллизий коротких имён между модулями). ТакжеresolveClassName()имеет веткуoverrides[$name]['class'], дополняющую namespace, если класс задан без\— перед Фазой 2 проверить конфигиmodules.phpна кастомные классы без FQN. - Текущий
XC_Autoloader::registerDirectories()сканирует 6 каталогов (core/domain/infrastructure/streaming/modules/public),cli/НЕ сканируется (CLI-классы грузитconsole.phpвручную). Composer-маппингCli/в Фазе 0 впервые добавит cli в автолоадер. python3 installраспаковывает архив целиком (tar/zip extractallв/home/xc_vm/), хардкод-путей к lowercase-каталогам НЕ содержит, chown рекурсивный → переименование (Фаза 1) проходит для install-скрипта прозрачно.- ioncube
\XC_VM+ioncube_server_data()— только за guard'амиclass_exists/function_exists; ОСТАЮТСЯ глобальными, не трогаем. - Билд:
Makefileкопируетsrc/черезgit ls-files(переименования =git mv- коммит);
python3 installразворачивает в/home/xc_vm/без composer-шага;deleted_files.txtуправляет чисткой у клиентов; есть PHPStan baseline + CI.
- коммит);
- igbinary используется ТАКЖЕ для кешей настроек/серверов/доменов — не трогаем; убираем только class-map кеш автолоадера.
Сквозные решения
composer.jsonиvendor/лежат ВНУТРИsrc/(на продеsrc/→/home/xc_vm/, копируется только содержимоеsrc/). То естьsrc/composer.jsonиsrc/vendor/→ на проде/home/xc_vm/composer.jsonи/home/xc_vm/vendor/. Коммитимsrc/vendor/в git (на install-пути нет Composer/сети). Обычный PSR-4 →vendor/это лишь склейка автолоадера Composer + маппинги 2 вендорных либ, без загрузок.composer dump-autoloadзапускать изsrc/(никогдаinstall), только при изменении префиксов/files. ДобавитьvendorвLB_DIRS;bootstrap.php→require __DIR__ . '/vendor/autoload.php'.- Обычный PSR-4 в dev — без
-o/-a→ живой резолв пути, без кеша классов. Для prod-сборкиcomposer dump-autoload -oдопустим опционально (OPcache нивелирует stat-overhead, но classmap полезен на CLI/cron-нодах без OPcache);-a(classmap-authoritative) не использовать. - Атомарное переименование каталогов отдельной фазой до пофайлового namespace.
- Подстраховка (fallback):
XC_Autoloaderостаётся зарегистрированным, но в конце очереди (prepend=false) и без кеша. Composer (впереди) выигрывает для мигрированныхXcVm\-классов; ещё-глобальные проваливаются в рантайм-сканер. Каждый класс ровно в одном состоянии — без коллизий. Удаляется в финале. - НЕ добавлять префикс
XcVm\Module\в composer.json — каталоги модулей в нижнем регистре (plex), а slug'и маркетплейса (watch-d2bho) не подходят ни под одно PSR-4-правило; модули грузит переписанныйModuleLoader, не Composer.
Фазы
Фаза 0 — Фундамент Composer (без изменения поведения, без переименований)
src/composer.json(внутриsrc/, т.к.src/→/home/xc_vm/):autoload.psr-4={"XcVm\\":"./", "M3uParser\\":"core/Parsing/M3uParser/src/", "Chrisyue\\PhpM3u8\\":"core/Parsing/PhpM3u8/src/"}(пути относительноsrc/; после Фазы 1 —Core/Parsing/...);autoload.files= 20 файлов с глобальными функциями (пути относительноsrc/);config.optimize-autoloader:false,classmap-authoritative:false.cd src && composer dump-autoload; закоммититьsrc/vendor/иsrc/composer.lock..gitignore: убедиться, чтоsrc/vendor/НЕ игнорируется (добавить!src/vendor/при необходимости) — иначеgit ls-filesсоберёт архив без автолоадера → fatal на всех серверах.composer.jsonscripts.post-install-cmd: предупреждение +exit 1(защита от случайногоcomposer install, который рассинхронит закоммиченныйvendor/).phpstan.dist.neon: добавитьexcludePaths: analyse: [src/vendor/*](сейчас исключён только M3uParser) — иначе PHPStan начнёт анализировать vendor → CI красный.- CI: добавить шаг
cd src && composer audit --no-dev(читаетcomposer.lock, безinstall); side-effect-grep дляautoload.files(нетecho/print/header()/$_GET/$_POST/$_SESSION/DB). src/bootstrap.php: сначалаrequire __DIR__ . '/vendor/autoload.php', потом легасиautoload.php.tests/bootstrap.php: тоже сначалаvendor/autoload.php(иначе тесты идут по старому стеку = false green).src/autoload.php: регистрация → в конец очереди; убрать igbinary-кеш (enableFileCache/saveCache/shutdown/root-chown + нижний вызовenableFileCache); оставить in-memory сканер. (Удаление root-chown заодно убирает TOCTOU-symlink-риск — security-плюс.)- Деплой Фазы 0 на все LB-ноды атомарно ДО внесения
require vendor/autoload.phpвbootstrap.php(иначе окно с LB-нодами безvendor/→ fatal на каждом запросе). - Проверка: бутятся все контексты;
tmp/cache/autoload_mapбольше не пишется; Composer-лоадер первым,XC_Autoloaderпоследним;makesmoke (архив содержитvendor/); PHPStan + PHPUnit зелёные;composer auditзелёный.
Фаза 1 — Атомарное переименование 7 каталогов в PascalCase + последствия
Вся фаза = один атомарный коммит (
git mv+ ВСЕ правки путей/Makefile/PHPStan ниже). Двухшаговый подход запрещён: любой промежуточныйmake lbсобирает «отравленный» LB-архив (см. блокер 1).
git mv:core→Core, domain→Domain, infrastructure→Infrastructure, streaming→Streaming, modules→Modules, cli→Cli, public→Public.- Поправить захардкоженные пути во всех 9 файлах с живыми require (см. секцию «Факты», ~41 оператор),
не только
bootstrap.php(10 штrequire_once MAIN_HOME.'core/...'→Core/...;resources/остаётся lowercase), плюсautoload.php registerDirectories(),console.php(Cli/...),public/index.php+ точки входа. Makefile— в этом же коммите:LB_DIRS: PascalCase 7 каталогов + добавитьvendor(bin Cli config content Core Domain Modules vendor ...);config/content/resources/signalsостаются lowercase.LB_DIRS_TO_REMOVE: 9 путей → PascalCase (Domain/User,Domain/Device,Domain/Auth,Public/Controllers/Admin,Public/Controllers/Player,Public/Controllers/Reseller,Public/Views,Public/assets,Public/routes).LB_FILES_TO_REMOVE: 15 файлов → PascalCase (Public/Controllers/Api/AdminApiController.php,Public/Controllers/Api/ResellerRestApiController.php, всеCli/Commands/*,Cli/CronJobs/*,Domain/Epg/EPG.php).lb_delete_files_listawk-фильтр поLB_DIRS— проверить согласованность с PascalCase вdeleted_files.txt.
deleted_files.txt: добавить старые нижнерегистровые пути (make generate_deleted_files).phpunit.xml.dist: coverage-paths → PascalCase.- PHPStan: PascalCase
paths/scanDirectories/excludePaths(+ исключениеPhpM3u8;src/vendor/*уже из Фазы 0); перегенерироватьphpstan-baseline.neonв этом же коммите. - nginx/web-roots: grep
lb_configs+config/*.confпо 7 именам каталогов (особенно docroot наpublic/). - Проверка:
git status= чистые переименования; grep-gate: 0 живыхrequire/includeс lowercase-именами 7 каталогов в путях; все контексты бутятся;console.php --listполный; verify LB-архива —tar -tzfНЕ содержитDomain/Auth|Public/Controllers/Admin|Public/Controllers/Reseller|Cli/CronJobs; архив содержит переименованные каталоги +vendor/.
Фаза 2 — Переписать динамические хабы (корректно под PSR-4)
console.php: discovery по FQCN из пути (XcVm\Cli\Commands\. basename /…\CronJobs\), убрать ручной requirecli/CommandInterface.php/CommandRegistry.php(Composer автолоадит), заменить глобальную строкуimplementsInterface('CommandInterface')наimplementsInterface(\XcVm\Cli\CommandInterface::class). Обязано переключаться в ТОМ ЖЕ коммите, что и namespace слоя Cli (жёсткое требование — глобальная строка'CommandInterface'сломается, как только интерфейс уйдёт в namespace; «временный двойной путь» НЕ годится как постоянное состояние).ModuleLoader:resolveClassName()→XcVm\Module\{Pascal}\{Pascal}Module;load()require_once modulePath/{Pascal}Module.phpзатемclass_exists($fqcn,false). ПереписатьregisterModuleAutoloader()в настоящий PSR-4-резолвер: снять префикс namespace модуля, маппить ОСТАТОК в подпуть подmodulePath(XcVm\Module\Watch\Service\WatchService→modulePath/Service/WatchService.php) вместо потери информации через short-name+glob. Сохранить обработку зашифрованных файлов; slug-каталоги маркетплейса не затрагиваются (require использует реальныйmodulePath).
Фазы 3..N — Пофайловый namespace по слоям
Порядок (от наименее зависимых): Core leaf (Config, Error, Logging, Parsing(не-вендор), Storage) → Core hubs (Database, Http, Module, Auth) → Domain → Infrastructure
→ Streaming → Cli (тут переключаем console.php) → Modules (подтверждаем новый
ModuleLoader) → Public последним (в основном процедурный).
Known-исключение нарушителя порядка (Core hub → Domain): core/Init/LegacyInitializer.php
вызывает BouquetService::getAll()/CategoryService::getFromDatabase(), а core/Auth/Authenticator.php
и core/Auth/AuthRepository.php — UserRepository. При миграции Core hubs использовать
\BouquetService::/\UserRepository:: (ведущий \) до завершения слоя Domain, затем обновить импорты
на use XcVm\Domain\....
Механический паттерн на файл:
- Добавить
namespace XcVm\<Layer>\<SubPath>;(точный регистр PSR-4). - Ссылки на ещё-глобальные классы → ведущий
\(\SettingsManager::) илиuse SettingsManager;. Ссылки на мигрированные →use XcVm\...\Foo;. Встроенные (\Exception,\PDO,\ReflectionClass,\ZipArchive) →\. ioncube\XC_VM→ всегда\XC_VM, никогда не импортировать. - Строковый
class_exists('Foo')→ FQCN-строка /Foo::classпосле миграцииFoo. Оставить'XC_VM'/'ZipArchive'/ext-guard'ы литералами. - Разделить multi-class файлы на своём слое; старые пути → в
deleted_files.txt. - Процедурные/view-файлы (~190 в
public/): НЕ в namespace — когда вызываемый класс мигрирует, добавить вверху файлаuse XcVm\...\ClassName;, чтобы существующие вызовыClassName::не менялись (минимум правок). После каждого слоя — автоматизированная проверкаci/check-procedural-use-statements.sh <классы слоя>(а не ручной grep): процедурный файл, использующий короткое имя мигрированного класса безuse, валит проверку. Опционально временно добавлятьpublic/Viewsв PHPStanpathsна время слоя для отловаClass not found.
Финальная фаза — Удалить fallback
Все классы в namespace → перестать require'ить autoload.php; оставить его no-op заглушкой
на один релиз, затем удалить (обновить deleted_files.txt, LB_ROOT_FILES, PHPStan
scanFiles); остаются только Composer + ModuleLoader; финальный composer dump-autoload;
коммит vendor/.
Ключевые файлы
src/composer.json(внутриsrc/, т.к.src/→/home/xc_vm/), закоммиченныеsrc/vendor/+src/composer.lock.gitignore— НЕ игнорироватьsrc/vendor/Makefile—LB_DIRS/LB_DIRS_TO_REMOVE/LB_FILES_TO_REMOVEPascalCase +vendor(блокер 1).github/workflows/*—composer audit+ grep-gate hardcoded-путей + verify LB-архиваtests/bootstrap.php—vendor/autoload.phpпервымsrc/autoload.php— убрать кеш, в конец очереди, в итоге удалитьsrc/bootstrap.php— vendor первым; PascalCase в require-путяхsrc/console.php— discovery по FQCNsrc/core/Module/ModuleLoader.php— PSR-4-резолвер модулей (resolveClassName,registerModuleAutoloader)phpstan.dist.neon/phpstan-baseline.neon— PascalCase пути +excludePaths: src/vendor/*, перегенерироватьsrc/migrations/deleted_files.txt— старые нижнерегистровые пути + разделённые файлы
Проверка (каждая фаза)
php -l изменённых файлов; make phpstan зелёный (перегенерация baseline после
переименования/слоёв — должен сокращаться, не должно появляться «unknown class XcVm...»);
php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist; console.php --list; бут
CLI/Stream/Admin/Player; загрузка 4 модулей + класс из под-namespace + модульная команда;
make-архив содержит vendor/ + переименованные каталоги; порядок spl_autoload_functions()
(Composer первый, fallback последний до финала).
Тест-план и CI-гейты
Новые автоматизированные гейты (добавлять на указанной фазе, оставлять в CI):
- Grep-gate (Фаза 1, CI job): 0 живых
require/includeс lowercase-именами 7 каталогов в путях. Добавить ДО Фазы 1 в baseline-режиме (не давать расти), после — ожидаемое значение 0. tests/Unit/AutoloadOrderTest.php(Фаза 0): Composer первым,XC_Autoloaderпоследним вspl_autoload_functions();tmp/cache/autoload_mapне пишется.tests/Unit/BootstrapPathsTest.php(Фаза 1): рантайм-guard — ни один.phpвsrc/не содержитrequire/includeс lowercase-именем переименованного каталога в пути.tests/Unit/ModuleLoaderPsr4ResolverTest.php(Фаза 2): резолв класса из под-namespace модуля без glob; сохранение обработки зашифрованных.php.enc.tests/Unit/ConsoleDiscoveryTest.php(Фаза 2):console.php --list== baseline-snapshot.ci/check-procedural-use-statements.sh(каждый слой 3..N): процедурные файлы имеютuseдля мигрированных классов слоя.composer audit(Фаза 0+): безinstall; side-effect-grep дляautoload.files.- Smoke 4 контекстов (CLI/Stream/Admin/Player):
php -lточек входа + проверка порядка автолоадеров. - Verify LB-архива (Фаза 1+):
tar -tzfНЕ содержитDomain/Auth|Public/Controllers/Admin|Public/Controllers/Reseller|Cli/CronJobs.
Acceptance каждой фазы: PHPUnit зелёный (тестов не меньше прежнего); make phpstan зелёный, baseline не
растёт (0 новых unknown class XcVm\); все гейты выше зелёные.
Риски и откат
- Неверный namespace/неквалифицированная ссылка → ловит PHPStan; fallback не спасёт → откат правок слоя.
- Процедурные файлы тихо ломаются → после слоя grep + добавление
use+ smoke-рендер страниц. - Билд пропустил переименование →
git mv+ проверка содержимого архива. - Устаревшие нижнерегистровые каталоги у клиентов →
deleted_files.txt+ проверка обновления на стейджинге. - Каждая фаза = изолированный коммит/ветка; в git Фазы 0 и 1 откатываются независимо;
mainостаётся пригодным к релизу после каждой смерженной фазы. Fallback прикрывает ещё-глобальные классы (но не неверно-неймспейснутые). - Откат Фазы 1 на уже задеплоенных клиентах ФАКТИЧЕСКИ НЕОБРАТИМ через обычный
update: старыйcore/уже удалён черезdeleted_files.txt, а revert вернётcore/, оставивCore/на диске → два каталога одновременно → autoload находит классы в обоих → непредсказуемо. Варианты: (а) отдельный rollback-архив с обратнымdeleted_files.txt(содержащимCore/,Cli/,Domain/, ...); либо (б) явно задокументировать «откат только через новый релиз-fix вперёд». Дополнительно проверитьsrc/update: удаляет ли он опустевшие каталоги (rmdir) — иначе мёртвыйcore/останется; при необходимости добавить сами каталоги вdeleted_files.txt. - Supply chain:
vendor/в git → обновлять отдельным коммитом с пометкой[vendor]; bundled M3uParser/PhpM3u8 (path-mapped, без semver) патчить вручную вCore/Parsing/с записью в CONTRIBUTING.