Verified against Semitexa Ultimate 2026.09.19.1020
Discovery Contributors
Attribute discovery is extensible. A package that defines its own attribute registers a contributor that says which attribute to watch and what to do with each hit — the same mechanism core uses for pipeline listeners, server lifecycle hooks and resource metadata.
#[AsDiscoveryContributor(priority: 50)]
final class SlotHandlerContributor implements DiscoveryContributor
{
public function attribute(): string
{
return AsSlotHandler::class;
}
public function scopedToActiveModules(): bool
{
return true;
}
public function contribute(string $className, object $attribute, BootDiagnostics $diagnostics): void
{
// register $className against the registry this package owns
}
}
That is Semitexa\Ssr\Application\Service\Discovery\SlotHandlerContributor. The class must implement DiscoveryContributor and be instantiable; discovery rejects an #[AsDiscoveryContributor] that is not, by name.
The three methods
attribute() returns the attribute class to watch. Returning a class that is not loadable is not an error — it means the owning package is not installed, and discovery skips the contributor. That is the one place class_exists() behaviour is intentional, and it lives here rather than being repeated at every call site.
scopedToActiveModules() decides visibility. Return true — the usual answer — and hits only count when they come from an active module or the project's own src/, so a slot declared by a module the current tenant has not enabled stays unregistered. Return false for framework-level contributions that apply regardless of tenant configuration.
contribute() runs once per attribute occurrence, so a class carrying the attribute repeatably is visited once per declaration. Throwing is safe and expected for a genuinely invalid declaration: discovery records it against BootDiagnostics and continues, so one malformed class cannot abort a boot. Validate loudly rather than skipping quietly.
Priority
priority orders contributors against each other, highest first. The four in semitexa/ssr show the reasoning:
| Priority | Contributor | Why there |
|---|---|---|
| 300 | LayoutSlotContributor |
layouts declare the slots |
| 200 | DataProviderContributor |
providers feed them |
| 100 | SlotResourceContributor |
resources bind to declared slots |
| 50 | SlotHandlerContributor |
a handler attaches to a slot, so it runs last |
The rule of thumb: whatever declares a thing must run before whatever attaches to it.
When you need one
Only when your package defines an attribute that needs a boot-time registry. Consuming existing attributes needs nothing — declare the attribute and let its owner's contributor do the work.