The committed PHPUnit runner lived at tools/.bin/phpunit.phar, away from the suite it runs. Move it next to the tests it drives — tests/phpunit.phar — and update every invocation to `php tests/phpunit.phar -c tests/phpunit.xml.dist`: - CI workflows (ci, build-release, build_pre-release) + the ci.yml header, - CLAUDE.md, CONTRIBUTING.md, tools/README.md, the qa-lead-reviewer agent, - docs/en (dev-workflow, updates_checklist, phpunit-phar, refactoring). docs/ru is generated from docs/en (make docs-translate) and is left for the next regeneration, per the docs workflow.
9.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
XC_VM is an open-source, Xtream-Codes-style IPTV management panel (PHP 8.1+, AGPL-3.0). It is a modular monolith that has been migrated to Composer PSR-4. The application lives under src/ and is deployed verbatim to /home/xc_vm/ (so src/ is the deploy root and MAIN_HOME maps to it).
Layout essentials
src/is the application root.composer.json,vendor/,bootstrap.php,console.phpall live insrc/, not the repo root. PSR-4:XcVm\→src/(e.g.XcVm\Core\Database\DatabaseHandler=src/Core/Database/DatabaseHandler.php).- The repo root holds build/release tooling:
Makefile,tools/,tests/,install/,lb_configs/,build/phpcs.xml.dist.
Commands
Everything is driven from the repo root via the Makefile. Static-analysis/style tools are require-dev packages and are NOT in the committed vendor/; install them first.
make dev-tools # composer install in src/ — adds PHPStan + phpcs (Slevomat) to src/vendor (do this first)
make phpstan # static analysis (phpstan.dist.neon, --memory-limit=2G)
make cs # code-style check (dry-run, fails on diff)
make cs-fix # apply style fixes in place
make gates # fast PSR-4 regression gates (see below)
make dev-clean # prune src/vendor back to production-only (composer install --no-dev)
# Tests — PHPUnit 10.5, config in tests/phpunit.xml.dist (suite "Unit", bootstrap tests/bootstrap.php)
php tests/phpunit.phar -c tests/phpunit.xml.dist
php tests/phpunit.phar -c tests/phpunit.xml.dist --filter SomeTestName # single test
php -l path/to/File.php # quick syntax check (used constantly; no DB needed)
make gates runs three CI blockers: check-procedural-use (every procedural/view file must use-import the classes it references at the top of the file — PHP use is positional), verify-lb-archive (the load-balancer build must contain no privileged code), and check-vendor-prod-only (the committed vendor/ must stay production-only).
Build / release (Makefile)
make main— build the full panel archive (dist/xc_vm.tar.gz+XC_VM.zipinstaller).make lb— build the load-balancer archive: the same tree with admin/reseller/player code, installers, and privileged commands stripped out (seeLB_DIRS_TO_REMOVE/LB_FILES_TO_REMOVE). Modules are MAIN-only and excluded from LB.make new— wipedist/. Builds copy only git-tracked files and runverify_no_lfs_pointers.
Critical constraints
vendor/is committed and PRODUCTION-ONLY. Never runcomposer installon a deploy path. To change autoload, runcomposer dump-autoloadfromsrc/. After changing deps, re-commit acomposer install --no-devvendor (andcomposer.lock).- Git LFS:
src/bin/install/database.sql, the bundledffmpeg/ffprobe,redis-server,yt-dlp, fonts/videos, etc. are LFS objects. Editing an LFS file is transparent (the clean filter re-stages it as an LFS object ongit add;git pushuploads it). A checkout without LFS materialised ships 130-byte pointer stubs — the build'sverify_no_lfs_pointersguards against this. - Never
git push. Commit freely (Conventional Commits, English messages, grouped logically), but pushing to remote is the user's call — do not push unless explicitly told to. - PHP runs with
short_open_tag=1(view templates use<?/<?=). The phpcs code-style ruleset excludes view templates, so no short-tag handling is needed there.
Architecture (big picture)
Bootstrap & entry points. src/bootstrap.php defines XC_Bootstrap::boot(BootContext::{Minimal|Cli|Stream|Admin}) — context-scoped init (constants → config → DB → LegacyInitializer → … → DI container + module boot). Entry points:
src/Public/index.php— front controller for admin/reseller/player + the streaming/web API (routes by nginx-suppliedXC_SCOPE/XC_API; instantiatesXcVm\Public\Controllers\Api\*Controller).src/console.php— CLI; builds theCommandRegistry, loads modules, runs commands/crons.- Lightweight bootstraps
WebApiBootstrapandStreamingRequestBootstrapfor high-traffic API/stream endpoints.
All of these create the DI container and (for admin/CLI) call ModuleLoader::bootAll(), which is where module routes, commands, navbar entries, and event subscribers get registered.
Core building blocks (src/Core/). Container/ServiceContainer (PSR-11 DI, lazy factories, tags), Events/EventDispatcher (static, PSR-14 typed events; modules subscribe with the #[ListensTo(Event::class)] attribute on public methods), Http/Router + CommandRegistry + Module/NavbarRegistry. Init/LegacyInitializer bridges new code to legacy superglobals.
Database access pattern. Domain and module classes do NOT receive $db by constructor. They use \XcVm\Infrastructure\Database\DatabaseAware and call self::db(), which lazily resolves the connection from the DatabaseFactory singleton (set by every bootstrap path). Do not reintroduce per-class setDb() wiring. Core/Database/DatabaseHandler is the PDO wrapper used via $db->query(...).
Module system (src/Modules/<name>/, core in src/Core/Module/). Each module ships a module.json manifest (name, version, dependencies, optional_dependencies, requires_core) and a <Name>Module extends BaseModule. ModuleLoader discovers modules, topologically sorts by dependency (with cycle detection), and boots them; ModuleManager handles install/update/uninstall and writes state to config/modules.php.
- Modules own their DB schema via file migrations:
Modules/<name>/migrations/<semver>.up.sql/.down.sql, executed byModuleMigrator(install →up(null→version), update →up(from→to), uninstall →down). The recordedinstalled_versionis the watermark; there is no per-module tracking table. Do NOT add module tables tosrc/bin/install/database.sql.ModuleManager::syncBundledModules()(called fromStatusCommand/console.php status) auto-installs not-yet-installed bundled modules in dependency order. - Core must not touch module-owned tables directly (they may be dropped on uninstall). Instead, core dispatches an event (e.g.
StreamsDeletedEvent,BouquetDeletedEvent) and the owning module subscribes via#[ListensTo]to clean up its own data.
Main vs Load Balancer. The MAIN server runs the panel + MySQL/MariaDB. LB nodes only restream; they get a stripped codebase (the make lb archive) and are provisioned over SSH by Cli/Commands/ServerInstallCommand + LbInstallFlow, which also pull distribution binaries (PHP/nginx/ffmpeg) from a separate GitHub binaries release repo. Streaming code runs in both; admin/reseller/player and privileged CLI commands are MAIN-only.
xcvm_core C extension. A bundled Zend extension (loaded via bin/php/lib/php.ini, alongside ioncube/opcache/maxminddb) provides marketplace module installation, module decryption, and install_id/environment fingerprinting. PHP code reaches it via the global \XC_VM::* API.
Conventions & gotchas
- Outbound HTTPS from PHP-FPM must use cURL —
file_get_contents()over https does not work in this environment. - The TMDb client is the legacy global
\TMDBclass (vendored insrc/Infrastructure/Tmdb/lib/, not PSR-4). Build it throughXcVm\Infrastructure\Tmdb\TmdbApiService::createClient($apiKey, $language)(or load viaTmdbApiService::requireLibrary()), which loads the library itself — do notrequire_oncethe lib path manually. - Config:
config.ini(DB creds,server_id), thesettingsDB table viaSettingsManager, andconfig/modules.phpfor module enable/disable/version state.
Documentation (docs/)
The docs are a MkDocs Material site (mkdocs.yml), deployed to GitHub Pages by .github/workflows/pages.yml.
- Edit ONLY
docs/en/— English is the single source of truth. Never hand-editdocs/ru/(or any other language tree): it is GENERATED fromdocs/enbytools/docs/translate.pyand any manual change is overwritten on the next regeneration. docs/ruis committed but regenerated locally before a release (make docs-translate), not in CI — translation is slow, sopages.ymlonly builds the committed tree. See the "Regenerate translated documentation" step indocs/en/builds/updates_checklist.md.- After editing English docs:
make docs-build(strict — fails on broken links/anchors) to verify; before a release alsomake docs-translateand commit the regenerateddocs/ru. - The two nav tabs (User Guide / Developer Guide) are a
mkdocs.ymlnav:grouping only — do not move files to reorganize; editnav:(andnav_translationsfor ru labels).
Agent skills
Issue tracker
Issues and specs live as GitHub issues in Vateron-Media/XC_VM (via the gh CLI). See docs/agents/issue-tracker.md.
Domain docs
Single-context — one CONTEXT.md + docs/adr/ at the repo root. See docs/agents/domain.md.