Files
XC_VM/docs/en/development/module-lifecycle.md
T
Divarion_D c982bb2f1e docs: audit dev docs for source drift, split oversized pages, add core-wiring
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.
2026-08-26 22:42:43 +03:00

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. plex reads Enabled but watch is failed). The badge tooltip lists the concrete problems. This dependency_warnings field is computed by ModuleManager::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:

  1. Scans src/Modules/*/module.json
  2. Applies overrides from config/modules.php
  3. Filters by environment (main / lb / any)
  4. 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 priority descending, then alphabetically
    • Throws ModuleCycleException on cycles (a subclass of \RuntimeException; cyclic dependencies remain fatal)
    • Missing optional dependencies are silently skipped
  5. Resolves class name: my-module → FQN XcVm\Module\MyModule\MyModuleModule (kebab-case → PascalCase; can be overridden via class key in config)
  6. Registers the module's PSR-4 autoloader (maps XcVm\Module\<Name> onto the module directory)
  7. Instantiates the module class

In web context:

  • bootAll($container, $router) → calls boot(), registerRoutes(), registerNavbar(), and subscribes to events for every loaded module

In CLI context:

  • registerAllCommands($registry) → calls registerCommands() 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:

  1. XC_VM::module_install($slug, $version, $apiKey) — C extension downloads, decrypts, unpacks
  2. installModule($slug) — runs install() on the module
  3. EventDispatcher::dispatch(new PackageInstalledEvent(...)) — dispatches the event
  4. hotReload($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.