Publish an interface
Define the contract in your package, and document it:src/Domain/NotificationChannelInterface.php
getLookupKey() and an isEnabled(), the same shape the core’s extension points use. It keeps registration, configuration and admin listings predictable.
Register implementations
Shared\Infrastructure\Collections\ServiceCollectionInterface is a keyed registry that works with any interface, not just the core’s:
src/Plugin.php
Services are resolved from the container when they’re first used, so registering is cheap even if the implementation is expensive to build.
Consume implementations
get() throws Shared\Infrastructure\Collections\ServiceNotFoundException when the key isn’t registered, so fall back to a default rather than letting a stale setting break the page.
Let others configure their implementation
Copy the convention the core uses for its own extension points: the registration key doubles as the settings URL. If your plugin lists channels at/admin/settings/alerts, link each one to /admin/settings/alerts/{key}, and let the implementing plugin declare that route and point its extra.default_url there. See Admin settings pages.
Load order
Plugins boot in the order they’re discovered, so don’t assume another plugin has registered before you. Resolve implementations when you use them, not inboot(), and treat an empty collection as normal.
Custom AI capabilities
The AI service factory works the same way. Define a capability interface that extends the base AI contract, then register services for it:src/Domain/SummaryServiceInterface.php
supportsModel() and getSupportedModels() from Ai\Domain\Services\AiServiceInterface, which is how the factory picks one.
Document the contract
An extension point is an API. Ship it like one:- Publish the interface’s signatures and semantics in your README.
- Version the contract, and follow semantic versioning when it changes.
- Say which key namespace implementers should use, such as
acme-alerts.*. - Provide one reference implementation inside your own package.