Collectors are the mechanism through which modules contribute data to components during the engine lifecycle. Each component that needs module input defines a collector type and a handler, then triggers collection itself when it needs the data. Collection is pull-based — components decide when their data is gathered.
Contracts
Collector is a marker interface implemented by all concrete collectors. It carries no methods of its own — it is a type constraint that allows the system to identify and route collector instances.
CollectorHandler is a generic interface that owns the full collection lifecycle for a given collector type:
collects(): class-string<Collector> — identifies which collector type this handler works with
create(ModuleManifest $manifest, ?PanelContext $context, bool $scoped): Collector — creates a fresh collector instance for a given module
process(Collector $collector, ModuleManifest $manifest, ?PanelContext $context, bool $scoped): void — extracts data from a populated collector and accumulates it
finalise(): void — called once after all modules have been processed; typically where the handler registers or stores accumulated data
Attributes
#[Collect] marks a public non-static method on a registrar as a collector method. It accepts an optional PanelContext value to scope the method to a specific context. The method must declare a single parameter typed to the relevant Collector implementation. There are no restrictions on how many #[Collect] methods a registrar declares.
#[Unscoped] marks a collector method as operating outside the module's implicit scope. What scoping means and whether unscoped methods are permitted at all is entirely the handler's decision. A handler may throw if it encounters an unscoped method regardless of the module's capabilities — the permissions handler is a concrete example of this, as every permission is always scoped to its module with no exceptions. Handlers that do permit unscoped methods check the module's capabilities accordingly.
PanelContext
PanelContext is a string-backed enum with three cases: Account, Server, and Platform. It is used throughout the engine to scope routes, permissions, collector methods, and tokens to a specific part of the panel. Server is a subcontext of Account in the routing layer but is a first-class value in PanelContext for the purposes of collector scoping. For some collectors such as permissions, PanelContext is organisational — it groups permissions by context without affecting how they are stored or enforced.
Collection
Collection is triggered by a component passing a CollectorHandler instance to ModuleRegistry::collect(CollectorHandler $handler, ?PanelContext $context). The registry iterates all enabled modules and for each one:
- Uses the handler's
collects() return value and the ModuleRegistrar metadata to find the matching collector method for the given PanelContext, if any
- Calls
CollectorHandler::create() to get a fresh collector instance
- Calls the registrar method with the collector instance
- Calls
CollectorHandler::process() with the populated collector
After all modules are processed, CollectorHandler::finalise() is called once. If a module has no matching collector method for the given handler and context, it is silently skipped.
Tasks
Collectors are the mechanism through which modules contribute data to components during the engine lifecycle. Each component that needs module input defines a collector type and a handler, then triggers collection itself when it needs the data. Collection is pull-based — components decide when their data is gathered.
Contracts
Collectoris a marker interface implemented by all concrete collectors. It carries no methods of its own — it is a type constraint that allows the system to identify and route collector instances.CollectorHandleris a generic interface that owns the full collection lifecycle for a given collector type:collects(): class-string<Collector>— identifies which collector type this handler works withcreate(ModuleManifest $manifest, ?PanelContext $context, bool $scoped): Collector— creates a fresh collector instance for a given moduleprocess(Collector $collector, ModuleManifest $manifest, ?PanelContext $context, bool $scoped): void— extracts data from a populated collector and accumulates itfinalise(): void— called once after all modules have been processed; typically where the handler registers or stores accumulated dataAttributes
#[Collect]marks a public non-static method on a registrar as a collector method. It accepts an optionalPanelContextvalue to scope the method to a specific context. The method must declare a single parameter typed to the relevantCollectorimplementation. There are no restrictions on how many#[Collect]methods a registrar declares.#[Unscoped]marks a collector method as operating outside the module's implicit scope. What scoping means and whether unscoped methods are permitted at all is entirely the handler's decision. A handler may throw if it encounters an unscoped method regardless of the module's capabilities — the permissions handler is a concrete example of this, as every permission is always scoped to its module with no exceptions. Handlers that do permit unscoped methods check the module's capabilities accordingly.PanelContext
PanelContextis a string-backed enum with three cases:Account,Server, andPlatform. It is used throughout the engine to scope routes, permissions, collector methods, and tokens to a specific part of the panel.Serveris a subcontext ofAccountin the routing layer but is a first-class value inPanelContextfor the purposes of collector scoping. For some collectors such as permissions,PanelContextis organisational — it groups permissions by context without affecting how they are stored or enforced.Collection
Collection is triggered by a component passing a
CollectorHandlerinstance toModuleRegistry::collect(CollectorHandler $handler, ?PanelContext $context). The registry iterates all enabled modules and for each one:collects()return value and theModuleRegistrarmetadata to find the matching collector method for the givenPanelContext, if anyCollectorHandler::create()to get a fresh collector instanceCollectorHandler::process()with the populated collectorAfter all modules are processed,
CollectorHandler::finalise()is called once. If a module has no matching collector method for the given handler and context, it is silently skipped.Tasks
Collectormarker interfaceCollectorHandlerinterface withcollects(),create(),process(), andfinalise()PanelContextenum withAccount,Server, andPlatformcases#[Collect]attribute accepting an optionalPanelContext#[Unscoped]attributeModuleRegistry::collect(CollectorHandler $handler, ?PanelContext $context)collection loopfinalise()being called once after all modules