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 — 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) |
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/categoriesentries 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. |
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 byinstanceofchecks over the loaded-module list (seebootAll), 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), 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):
- Core navbar first, once —
(new CoreNavbarProvider())->registerNavbar(...)before any module, so core menu nodes exist as parents. ServiceProviderInterface→boot($container)thenregisterEventSubscribers()— services are registered before that module's listeners are wired.StreamMiddlewareProviderInterface→registerStreamMiddleware($pipeline)— only if a$pipelinewas passed.RouteProviderInterface→registerRoutes($router)— only if a$routerwas passed ($router !== null).NavbarProviderInterface→registerNavbar(...).
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 fromCronProviderInterface::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; 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:
- Core auto-discovery.
new CommandRegistry(), then globCli/Commands/*.phpandCli/CronJobs/*.php, map each directory to its namespace, and via Reflectionregister()every non-abstract class implementingCommandInterface. Adding a core command is therefore just dropping a class in one of those directories — no manual registration. - Module commands.
ModuleLoader::registerAllCommands($registry)calls eachCommandProviderInterface::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:
XC_Bootstrap::boot(BootContext::Admin)— creates the container; setscontext/options/config; loads constants; runs flood/host checks;bootAdmin()(session, DB,LegacyInitializer, Redis, admin/reseller API, translator, shutdown handler, status constants);populateContainer();assertContainerHealth().Router::getInstance(), thenrequirethe core route filesroutes/{scope}.php(+routes/api.php).- 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. Router::dispatch()/dispatchApi()handles the request, resolving[Class, 'method']handlers through the container.
See Bootstrap Contexts for exactly which subsystems each context initialises, and HTTP Request Handling for the Router API and dispatch.
End-to-end boot — CLI¶
src/console.php:
require bootstrap.php;XC_Bootstrap::boot(BootContext::Cli)— DB,LegacyInitializer, optional Redis, process title, then the samepopulateContainer()(soeventsand friends exist in CLI too).new CommandRegistry(); auto-discover coreCli/Commands+Cli/CronJobs(glob + Reflection) →register().new ModuleLoader; loadAll(); registerAllCommands($registry)(module commands, beforebootAll), thenbootAll(getContainer())with no router and no pipeline — so routes and stream-middleware are skipped; only module services + event subscribers wire up.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 |
| Event registration forms, priorities, stoppable events, event catalogue | Event System |
Router API, begin/endModuleRegistration, dispatch, handler resolution |
HTTP Request Handling |
| Navbar item builder, visibility rules, rendering | Navbar Rendering |
| Module discovery, env filtering, topo-sort, enable/disable, install/update | Module Lifecycle |
| DI decoration, stream middleware, cron, migrations — the module author's hooks | Module Extension Points |
| Writing a module (manifest, class contract, directory layout) | Module Authoring |
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 |