Files
XC_VM/docs/en/development/core-wiring.md
T

206 lines
12 KiB
Markdown
Raw Normal View History

# Core Wiring & Registration
How the panel assembles itself at boot: the one service container, how it is populated, and how
modules push their routes, events, commands, cron entries and navbar items into the core registries.
This page is the **end-to-end narrative and the consume-side** of the core registries. The
author-side of each extension point is documented elsewhere and linked from
[What lives elsewhere](#what-lives-elsewhere) — this page does not repeat it.
---
## The one container
Everything hangs off a single process-wide `ServiceContainer`
(`src/Core/Container/ServiceContainer.php`), a PSR-11 `ContainerInterface` singleton obtained with
`ServiceContainer::getInstance()`. `XC_Bootstrap::boot()` creates it, populates it, and every later
consumer (`XC_Bootstrap::getContainer()`, module `boot()`, route handler resolution) reads the same
instance. Tests reset it with `ServiceContainer::resetInstance()`.
There is **no service-provider auto-discovery** for core services: the canonical set is registered
imperatively by the bootstrap (see below), and modules add their own services inside their
`boot()`. What you see registered is exactly what the code `set()`s — nothing is wired by
convention or annotation scanning.
---
## Container population at boot
This is the spine of the system. `XC_Bootstrap::boot()` registers services in two phases.
**Early (in `boot()` itself), before any subsystem loads:**
| Key | Value | Source |
| --- | --- | --- |
| `context` | the active `BootContext`'s string value | `src/bootstrap.php` |
| `options` | the `$options` array passed to `boot()` | `src/bootstrap.php` |
| `config` | `ConfigReader::getAll()` (parsed `config.ini`) | `src/bootstrap.php` |
**Canonical service set — `XC_Bootstrap::populateContainer()`** (runs for every context except
`Minimal`, i.e. once the DB is up):
| Key | Value | Notes |
| --- | --- | --- |
| `db` | the `Database` handle | protected (see below) |
| `settings` | `SettingsManager::getAll()` | protected |
| `servers` | `ServerRepository::getAll()` | |
| `bouquets` | `BouquetService::getAll()` | |
| `categories` | `CategoryService::getFromDatabase()` | |
| `redis` | `RedisManager::instance()` | only when Redis was booted for this context |
| `translator` | `Translator::class` | |
| `events` | a fresh `EventDispatcher` instance | also bridged to the static facade (see [Events](#events)) |
Immediately after, **`XC_Bootstrap::assertContainerHealth()`** hard-requires that `events` is
present (plus `db`/`redis` when those were booted) and throws if not — a **fail-loud** guarantee
that later code can assume these services exist rather than null-checking each one.
> The `context`/`config`/`settings`/`servers`/`bouquets`/`categories` entries are plain data
> snapshots taken at boot, not lazy factories — they are read, not recomputed, for the life of the
> request.
---
## ServiceContainer reference
All setters are chainable (`return $this`).
| Method | Purpose |
| --- | --- |
| `set(id, value)` | Register a service. A **`Closure`** value becomes a **lazy singleton factory** (called once on first `get()`, then cached); anything else — a scalar, an object, or a `[Class, 'method']` array — is stored as a ready value. |
| `factory(id, callable)` | Register a factory that returns a **new instance on every `get()`** (no caching). |
| `register(array)` | Bulk `set()` from an `id => value` map. |
| `get(id)` | Resolve a service. Returns the cached singleton if present; otherwise runs the factory once, caches it, and applies decorators. Guards against cyclic factories and throws `CircularDependencyException`; throws `NotFoundException` for an unknown id. |
| `getOrDefault(id, default)` | Like `get()` but returns `default` instead of throwing when absent. |
| `has(id)` / `keys()` / `remove(id)` / `dump()` | Introspection and teardown. |
| `decorate(id, decorator, priority)` | Wrap an existing service, highest priority applied last. **Forbidden** on the protected services `['db', 'settings', 'config', 'auth']` — decorating one throws. See [Module Extension Points](module-extension-points.md). |
| `tag(id, tag)` / `getTagged(tag)` | Group services under a label for batch retrieval. |
> **Tags are currently unused by core wiring.** `tag()`/`getTagged()` exist, but the boot path
> collects module contributions by `instanceof` checks over the loaded-module list (see
> [`bootAll`](#moduleloaderbootall-the-orchestrator)), **not** by tag. A source docblock still
> describes tags as the collection mechanism for subscribers/cron/routes — that describes an
> intended design, not the current code. Don't rely on tag-based collection until it is actually
> implemented.
Resolving a `[Class, 'method']` handler (used by the Router) goes **through the container**, so
handler classes are DI-resolved when registered, falling back to `new` when not.
---
## `ModuleLoader::bootAll` — the orchestrator
After `ModuleLoader::loadAll()` has discovered, filtered and **topologically sorted** the modules
(see [Module Lifecycle](module-lifecycle.md)), `bootAll()` is the single place that pushes each
module's contributions into the core registries. It checks each **sub-interface** with `instanceof`
so a module implements only the hooks it needs (`ModuleInterface` is a composite of them).
Per-module order inside `bootAll(ServiceContainer $container, ?Router $router, ?StreamPipeline $pipeline)`:
1. **Core navbar first, once** — `(new CoreNavbarProvider())->registerNavbar(...)` before any module, so core menu nodes exist as parents.
2. `ServiceProviderInterface` → `boot($container)` **then** `registerEventSubscribers()` — services are registered before that module's listeners are wired.
3. `StreamMiddlewareProviderInterface` → `registerStreamMiddleware($pipeline)` — **only if a `$pipeline` was passed**.
4. `RouteProviderInterface` → `registerRoutes($router)` — **only if a `$router` was passed** (`$router !== null`).
5. `NavbarProviderInterface` → `registerNavbar(...)`.
6. `TopbarProviderInterface` → `registerTopbar(...)` — per-page action buttons (merged into `Topbar::config`).
7. `TableProviderInterface` → `registerTables(...)` — serverSide table handlers (looked up by `TableController`).
8. `PermissionProviderInterface` → `registerPermissions(...)` — sub-permission keys (merged into `PermissionReference`).
9. `QuickToolsProviderInterface` → `registerQuickTools(...)` — Quick Tools button + handler.
Steps 6–9 are the module-owned admin surface: their registries (`TopbarRegistry`,
`TableRegistry`, `PermissionRegistry`, `QuickToolsRegistry`) are `reset()` at the
start of `bootAll` and consulted later by the topbar/table/permission/quick-tools
code. They run regardless of `$router` (registries only, no routes), so a module's
table is reachable even on the REST API path, which boots modules without a router.
Two contributions are **separate passes, not part of `bootAll`**:
- `registerAllCommands($registry)` — CLI commands, each module wrapped in try/catch so one broken module can't brick the whole CLI.
- `collectCronEntries()` — crontab lines gathered from `CronProviderInterface::getCronEntries()`, consumed by the startup/status commands.
Because routes and stream-middleware are gated on the optional `$router`/`$pipeline` arguments,
**the same `bootAll()` does different work depending on the entry point** — the CLI passes neither,
so only services + event subscribers (and the harmless navbar) are wired there.
---
## Events
`EventDispatcher` (`src/Core/Events/EventDispatcher.php`) is a singleton with a static facade.
`populateContainer()` does `new EventDispatcher()` → `EventDispatcher::setInstance($d)` →
`$container->set('events', $d)`, so the container entry `events` and the static
`EventDispatcher::dispatch()/listen()` share **one** listener store. Modules register listeners
during step 2 of `bootAll` above, via `getEventSubscribers()` or the `#[ListensTo]` attribute.
Full details — registration forms, priorities, stoppable events, the built-in event catalogue — are
in [Event System](event-system.md); this page only covers *where in the boot sequence* listeners
get wired.
---
## CLI command registration
The CLI path (`src/console.php`) builds its command set in two steps:
1. **Core auto-discovery.** `new CommandRegistry()`, then glob `Cli/Commands/*.php` and
`Cli/CronJobs/*.php`, map each directory to its namespace, and via Reflection `register()` every
**non-abstract** class implementing `CommandInterface`. Adding a core command is therefore just
dropping a class in one of those directories — no manual registration.
2. **Module commands.** `ModuleLoader::registerAllCommands($registry)` calls each
`CommandProviderInterface::registerCommands()`.
`CommandRegistry` (`src/Cli/CommandRegistry.php`) is a plain `name → CommandInterface` map:
`register()`, `dispatch($argv)` (handles `--list`/`--help`, groups help by the `group:` prefix in a
command's name), `get($name)`, `getAll()`.
---
## End-to-end boot — Admin (web)
`src/Public/index.php`:
1. `XC_Bootstrap::boot(BootContext::Admin)` — creates the container; sets `context`/`options`/`config`; loads constants; runs flood/host checks; `bootAdmin()` (session, DB, `LegacyInitializer`, Redis, admin/reseller API, translator, shutdown handler, status constants); **`populateContainer()`**; **`assertContainerHealth()`**.
2. `Router::getInstance()`, then `require` the core route files `routes/{scope}.php` (+ `routes/api.php`).
3. Module boot block: `router->beginModuleRegistration()` → `new ModuleLoader; loadAll(); bootAll($container, $router)` → `router->endModuleRegistration()` → `drainRouteCollisions()`. Module-registration mode makes **core routes win** over any module route with the same path; collisions are captured, not silently overwritten.
4. `Router::dispatch()` / `dispatchApi()` handles the request, resolving `[Class, 'method']` handlers through the container.
See [Bootstrap Contexts](bootstrap-contexts.md) for exactly which subsystems each context initialises, and [HTTP Request Handling](http-request-handling.md) for the Router API and dispatch.
---
## End-to-end boot — CLI
`src/console.php`:
1. `require bootstrap.php`; `XC_Bootstrap::boot(BootContext::Cli)` — DB, `LegacyInitializer`, optional Redis, process title, then the **same** `populateContainer()` (so `events` and friends exist in CLI too).
2. `new CommandRegistry()`; auto-discover core `Cli/Commands` + `Cli/CronJobs` (glob + Reflection) → `register()`.
3. `new ModuleLoader; loadAll(); registerAllCommands($registry)` (module commands, **before** `bootAll`), then `bootAll(getContainer())` **with no router and no pipeline** — so routes and stream-middleware are skipped; only module services + event subscribers wire up.
4. `registry->dispatch($argv)` runs the requested command.
---
## What lives elsewhere
To avoid duplication, the author-side and per-subsystem detail live on their own pages:
| Topic | Page |
| --- | --- |
| Which subsystems each context initialises; `boot()` options; idempotency | [Bootstrap Contexts](bootstrap-contexts.md) |
| Event registration forms, priorities, stoppable events, event catalogue | [Event System](event-system.md) |
| Router API, `begin/endModuleRegistration`, dispatch, handler resolution | [HTTP Request Handling](http-request-handling.md) |
| Navbar item builder, visibility rules, rendering | [Navbar Rendering](navbar-rendering.md) |
| Module discovery, env filtering, topo-sort, enable/disable, install/update | [Module Lifecycle](module-lifecycle.md) |
| DI decoration, stream middleware, cron, migrations — the module author's hooks | [Module Extension Points](module-extension-points.md) |
| Writing a module (manifest, class contract, directory layout) | [Module Authoring](module-authoring.md) |
## Related files
| File | Role |
| --- | --- |
| `src/Core/Container/ServiceContainer.php` | The DI container: `set`/`factory`/`get`/`decorate`/`tag` |
| `src/bootstrap.php` | `XC_Bootstrap::boot`, `populateContainer`, `assertContainerHealth` |
| `src/Core/Module/ModuleLoader.php` | `bootAll`, `registerAllCommands`, `collectCronEntries` |
| `src/Core/Events/EventDispatcher.php` | Event dispatcher + static facade bridged into `events` |
| `src/Cli/CommandRegistry.php` | CLI command map + `dispatch` |
| `src/console.php` | CLI entry point: core command auto-discovery + module boot |
| `src/Public/index.php` | Web entry point: route files + module boot block |