Skip to content

Engine: Collectors #10

@ollieread

Description

@ollieread

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:

  1. Uses the handler's collects() return value and the ModuleRegistrar metadata to find the matching collector method for the given PanelContext, if any
  2. Calls CollectorHandler::create() to get a fresh collector instance
  3. Calls the registrar method with the collector instance
  4. 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

  • Define Collector marker interface
  • Define CollectorHandler interface with collects(), create(), process(), and finalise()
  • Define PanelContext enum with Account, Server, and Platform cases
  • Implement #[Collect] attribute accepting an optional PanelContext
  • Implement #[Unscoped] attribute
  • Implement ModuleRegistry::collect(CollectorHandler $handler, ?PanelContext $context) collection loop
  • Write tests for collection loop including modules with and without matching methods
  • Write tests for scoped and unscoped method handling
  • Write tests for handler rejection of unscoped methods where not permitted
  • Write tests for finalise() being called once after all modules

Metadata

Metadata

Assignees

Labels

layer: engineBase framework and engine work

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions