Verify every dev-doc claim against src/ and fix factual drift: wrong method signatures/return types, wrong enum casing (BootContext cases are PascalCase), stale paths (M3u parsers are Composer deps under vendor/, MobileDetect is mobiledetect/mobiledetectlib v4.9.0 \Detection\MobileDetect, NotFoundException lives in XcVm\Core\Container\Psr), a fictional `stream:check` command/class, reversed migration-failure semantics ([FAIL] = not recorded, retried), inverted isStreamRunning/isStreamAlive descriptions, findProcessPIDs ANY-not-ALL, acquireCronLock has no shutdown callback, and nonexistent make targets. Split oversized pages and fix nav + cross-links: - modules.md -> module-authoring / module-lifecycle / module-extension-points - cli-tools.md -> cli-tools + database-migrations - streaming-subsystem.md -> + streaming-diagnostics - geoip-and-device-detection.md -> geoip-isp-and-geo-routing + device-detection-and-stb-locking Add development/core-wiring.md: how the core assembles itself at boot (container population, ServiceContainer reference, bootAll orchestration, CLI command auto-discovery, end-to-end Admin/CLI boot walkthroughs). Only docs/en + mkdocs.yml touched; docs/ru is regenerated before release.
4.7 KiB
Module Lifecycle
How XC_VM discovers, loads, enables/disables, installs and distributes modules at runtime. To author a module see Module Authoring; for its extension hooks see Module Extension Points.
Enable / disable modules
All discovered modules load by default. Use src/config/modules.php to override state:
return [
'my-module' => ['state' => 'disabled'], // preferred
// or legacy boolean (still accepted):
'my-module' => ['enabled' => false],
];
Available state values (backed by ModuleState enum):
| Value | Meaning |
|---|---|
enabled |
Module loads and boots (default) |
disabled |
Module is discovered but skipped |
installing |
Transient state set by ModuleManager during install |
failed |
Install failed; module skipped (not loaded) |
Panel diagnostics. The Modules page shows a yellow ⚠ Dependency issue badge next to a module's status when a required dependency is missing or not enabled (e.g.
plexreadsEnabledbutwatchisfailed). The badge tooltip lists the concrete problems. Thisdependency_warningsfield is computed byModuleManager::listModules().
To override the class resolved for a module:
return [
'my-module' => ['class' => 'XcVm\\Module\\MyModuleV2\\MyModuleV2Module'],
];
config/modules.php contains only overrides. An empty or missing file means all discovered
modules load.
How loading works
ModuleLoader follows these steps on every request:
- Scans
src/Modules/*/module.json - Applies overrides from
config/modules.php - Filters by environment (
main/lb/any) - Resolves the load order:
pruneUnsatisfiableModules()drops modules whose required dependencies are unavailable (cascading, with a logged warning) so the load never aborts- Topological sort (DFS) over the dependency graph
- Within the same dependency group, sorts by
prioritydescending, then alphabetically - Throws
ModuleCycleExceptionon cycles (a subclass of\RuntimeException; cyclic dependencies remain fatal) - Missing optional dependencies are silently skipped
- Resolves class name:
my-module→ FQNXcVm\Module\MyModule\MyModuleModule(kebab-case → PascalCase; can be overridden viaclasskey in config) - Registers the module's PSR-4 autoloader (maps
XcVm\Module\<Name>onto the module directory) - Instantiates the module class
In web context:
bootAll($container, $router)→ callsboot(),registerRoutes(),registerNavbar(), and subscribes to events for every loaded module
In CLI context:
registerAllCommands($registry)→ callsregisterCommands()on every loaded module
Marketplace: install via C extension
Modules from the platform are installed via ModuleManager::downloadFromPlatform():
$manager->downloadFromPlatform(slug: 'my-module', version: '1.2.0', apiKey: $key);
Under the hood:
XC_VM::module_install($slug, $version, $apiKey)— C extension downloads, decrypts, unpacksinstallModule($slug)— runsinstall()on the moduleEventDispatcher::dispatch(new PackageInstalledEvent(...))— dispatches the eventhotReload($slug, $path)— loads and boots the module in the current request without PHP-FPM restart
Isolated subsystems
A module can be a fully isolated subsystem with its own entry point and bootstrap
(like Ministra). This is a convention, not a marker interface — it stays an
ordinary ModuleInterface/BaseModule module:
class MyModule extends BaseModule {
public function getName(): string {
return 'my-module';
}
public function getVersion(): string {
return '1.0.0';
}
}
Isolation means the subsystem runs through its own public entry point (e.g.
my-module/portal.php, a path relative to src/ that handles its own bootstrap)
with a separate bootstrap path. It shares infrastructure (db, cache, config) but
does not participate in the main Router, ModuleLoader::bootAll(), or
NavbarRegistry. The boot() and registerRoutes() implementations are typically
left as inherited no-ops.
Composer package discovery
Modules can be distributed as Composer packages with "type": "xcvm-module":
{
"name": "vendor/my-xcvm-module",
"type": "xcvm-module",
"extra": {
"xcvm": {
"module-path": "src"
}
}
}
ModuleLoader automatically scans vendor/composer/installed.json (Composer 1 and 2
formats) and discovers any installed xcvm-module packages alongside the built-in
src/Modules/ directory. Packages are deduplicated — a module in both modules/ and
vendor/ is loaded only once.