mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-19 04:03:00 +02:00
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.
190 lines
5.9 KiB
Markdown
190 lines
5.9 KiB
Markdown
# Module Extension Points
|
||
|
||
The core extension points a module plugs into: the DI container, stream middleware, cron tasks, versioned migrations, and typed events. To author a module see [Module Authoring](module-authoring.md); for load/lifecycle see [Module Lifecycle](module-lifecycle.md).
|
||
|
||
## DI container and service decoration
|
||
|
||
|
||
Services are registered in `boot()` via `ServiceContainer`. The container supports:
|
||
|
||
- **`set(id, factory)`** — lazy singleton via callable, or direct value
|
||
- **`factory(id, callable)`** — new instance on every `get()`
|
||
- **`decorate(id, callable, priority)`** — wrap an existing service
|
||
|
||
```php
|
||
// Decorate a service (adds behaviour around the original)
|
||
$container->decorate('stream.encoder', function (mixed $inner, ServiceContainer $c): MyEncoder {
|
||
return new MyEncoder($inner, $c->get('settings'));
|
||
}, priority: 20);
|
||
```
|
||
|
||
Decorators are chained by priority (highest wraps outermost). Protected services
|
||
(`db`, `settings`, `config`, `auth`) cannot be decorated — any attempt throws `RuntimeException`.
|
||
|
||
### PSR-11 compliance
|
||
|
||
`ServiceContainer` implements `ContainerInterface`:
|
||
|
||
```php
|
||
public function get(string $id): mixed; // throws NotFoundException if missing
|
||
public function has(string $id): bool;
|
||
```
|
||
|
||
`NotFoundException` implements `NotFoundExceptionInterface extends ContainerExceptionInterface`.
|
||
|
||
---
|
||
|
||
## PSR-14 events
|
||
|
||
Modules subscribe to typed events via `getEventSubscribers()` or the `#[ListensTo]` attribute. This is documented in full — dispatch, listener registration, priorities, stoppable events and the built-in event catalogue — on the dedicated [Event System](event-system.md) page.
|
||
|
||
---
|
||
|
||
## Stream Middleware
|
||
|
||
|
||
Modules can inject middleware into the stream pipeline by implementing
|
||
`StreamMiddlewareProviderInterface` (separate from `ModuleInterface`):
|
||
|
||
```php
|
||
class MyStreamMiddleware implements StreamMiddlewareInterface {
|
||
|
||
public function getPriority(): int {
|
||
return 50;
|
||
}
|
||
|
||
public function handle(StreamContext $ctx, callable $next): StreamContext {
|
||
// before — read or set attributes
|
||
$ctx->set('my.key', 'value');
|
||
$ctx = $next($ctx);
|
||
// after
|
||
return $ctx;
|
||
}
|
||
}
|
||
```
|
||
|
||
`StreamContext` is an attribute bag (`get`, `set`, `has`, `abort`, `isAborted`). `StreamPipeline`
|
||
executes middleware sorted by `getPriority()` descending.
|
||
|
||
### Pipeline priorities
|
||
|
||
| Range | Owner |
|
||
| ---------- | ----------------- |
|
||
| `80–100` | Core (Auth, Permission, ConnectionLimit) |
|
||
| `0–79` | Modules |
|
||
|
||
### Reserved navbar slots
|
||
|
||
| Parent node | Module slots |
|
||
| ------------------- | ------------------ |
|
||
| `management.service_setup` | `order` ≥ 60 |
|
||
| `management.logs` | `order` ≥ 170 |
|
||
|
||
---
|
||
|
||
## Cron task
|
||
|
||
|
||
**Cron logic** (`MyCron.php`) — business logic only, no CLI wiring.
|
||
|
||
**CronJob wrapper** (`MyCronJob.php`) — implements `CommandInterface`, uses `CronTrait`:
|
||
|
||
```php
|
||
class MyCronJob implements CommandInterface {
|
||
use CronTrait;
|
||
|
||
public function getName(): string { return 'cron:my_task'; }
|
||
public function getDescription(): string { return 'Cron: my task'; }
|
||
|
||
public function execute(array $rArgs): int {
|
||
if (!$this->assertRunAsXcVm()) {
|
||
return 1;
|
||
}
|
||
|
||
require INCLUDES_PATH . 'admin.php';
|
||
require_once __DIR__ . '/MyCron.php';
|
||
|
||
$this->initCron('XC_VM[MyTask]');
|
||
MyCron::run();
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
```
|
||
|
||
Registration in the module:
|
||
|
||
```php
|
||
public function registerCommands(CommandRegistry $registry): void {
|
||
$registry->register(new MyCronJob());
|
||
}
|
||
```
|
||
|
||
Declare the crontab entry by overriding `getCronEntries()` in the module class:
|
||
|
||
```php
|
||
public function getCronEntries(): array {
|
||
return [
|
||
'*/5 * * * *' => 'cron:my_task',
|
||
];
|
||
}
|
||
```
|
||
|
||
`ModuleLoader::collectCronEntries()` aggregates all modules' entries and `StartupCommand` /
|
||
`StatusCommand` write them to the system crontab automatically — no core file changes needed.
|
||
|
||
**Format:** key = cron expression, value = console command name registered via `registerCommands()`.
|
||
|
||
---
|
||
|
||
## Versioned migrations (MigratableInterface)
|
||
|
||
|
||
> **Two mechanisms, both additive.** The **file-based schema** described under
|
||
> [Module directory structure](module-authoring.md#module-directory-structure) (`database.sql` master +
|
||
> `database_drop.sql` teardown + `migrations/<semver>.sql` deltas) is the default for
|
||
> plain DDL/seed. `MigratableInterface` below is the **programmatic** path for upgrade
|
||
> steps that need PHP logic (data backfills, conditional changes). A module can use
|
||
> either or both; `ModuleManager::updateModule()` runs the file deltas first, then the
|
||
> callable migrations.
|
||
|
||
Modules whose upgrades need PHP logic implement `MigratableInterface`:
|
||
|
||
```php
|
||
namespace XcVm\Module\MyModule;
|
||
|
||
use BaseModule;
|
||
use MigratableInterface;
|
||
use ServiceContainer;
|
||
|
||
class MyModuleModule extends BaseModule implements MigratableInterface {
|
||
|
||
public function getMigrations(): array {
|
||
return [
|
||
'1.1.0' => function (): void {
|
||
// runs when upgrading from any version < 1.1.0 to >= 1.1.0
|
||
global $db;
|
||
$db->query("ALTER TABLE xc_my_table ADD COLUMN new_col INT DEFAULT 0");
|
||
},
|
||
'1.2.0' => function (): void {
|
||
// runs when upgrading from < 1.2.0 to >= 1.2.0
|
||
},
|
||
];
|
||
}
|
||
}
|
||
```
|
||
|
||
`ModuleManager::updateModule()` reads `installed_version` from the override store, filters
|
||
the map to only the entries `> fromVersion && <= toVersion`, sorts by semver, and runs each
|
||
callable in its own DB transaction. `installModule()` records `installed_version` after
|
||
a successful install; `uninstallModule()` clears it.
|
||
|
||
**Key rules:**
|
||
|
||
- Keys are semver strings (`'1.1.0'`, `'2.0.0'`) — `version_compare` ordering is used
|
||
- Each migration runs in its own transaction — failure rolls back only that step
|
||
- `BaseModule` provides a default `getMigrations(): array { return []; }` so implementing
|
||
`MigratableInterface` is optional
|
||
|
||
---
|