Subscriber Manager
Who is this for? Package developers and advanced applications that need fine-grained, repeatable event subscriptions in Capell Core—particularly for validation gates and stateful listeners.
TL;DR: Use
SubscriberManagerto register class-based subscribers that listen to core events. CallnotifySubscribers()to invoke all listeners, orvalidateWithSubscribers()to short-circuit on validation failures.
When to use this
Section titled “When to use this”SubscriberManager is a lower-level subscription API intended for core domain events—those that fire during domain operations (page saves, site replication, package installation).
-
Use
SubscriberManagerwhen you need:- Validation gates: Subscribers that return
falseto block an operation. - Repeatable subscriptions: Multiple independent subscribers listening to the same event.
- Class-based subscribers: Structured, testable listener classes (not one-off closures).
- State tracking: Subscribers that need to accumulate results or side effects.
- Validation gates: Subscribers that return
-
Use the simpler
CapellAdminevent registry (seeextending-capell.md§5) when you need:- Lightweight admin-panel lifecycle hooks (
afterSave,beforeDelete, etc.). - One-off callback functions.
- No validation logic.
- Lightweight admin-panel lifecycle hooks (
How it’s wired
Section titled “How it’s wired”Registration & Discovery
Section titled “Registration & Discovery”SubscriberManager is bound in the container by CapellServiceProvider:
$this->singleton(SubscriberManager::class);There is no automatic discovery; subscribers are registered explicitly via package service providers or CapellCore::subscriberManager()->subscribe(SubscriberClass::class).
Dispatch
Section titled “Dispatch”Events are instantiated as simple data objects (e.g., PageSaved, SiteCreated) and passed to:
$manager->notifySubscribers('page.saved', $eventInstance);// or$manager->validateWithSubscribers('page.saving', $eventInstance);This happens inside Actions and domain code, not Laravel’s event dispatcher. SubscriberManager is orthogonal to Laravel’s events.
File Paths
Section titled “File Paths”- Manager:
packages/core/src/Support/Subscriber/SubscriberManager.php - Contracts:
packages/core/src/Support/Subscriber/Contracts/{Subscriber,ValidatingSubscriber}.php - Events:
packages/core/src/Events/*.php
Public API
Section titled “Public API”public function subscribe(string $subscriber): voidRegister a subscriber class. Must implement Subscriber or ValidatingSubscriber.
public function unsubscribe(string $subscriber): voidDeregister a subscriber class.
public function getSubscribers(): arrayReturn array of registered subscriber class names.
public function hasSubscriber(string $subscriber): boolCheck if a subscriber is registered.
public function notifySubscribers(string|BackedEnum $event, object $context): voidInvoke all subscribers with the event name and context object. Resolves each subscriber from the container and calls handle().
public function validateWithSubscribers(string|BackedEnum $event, object $context): boolInvoke validation subscribers. Returns false if any ValidatingSubscriber returns false; short-circuits and returns true otherwise. Plain Subscriber instances are skipped.
Available Events
Section titled “Available Events”| Event Class | Context Type | When Fired |
|---|---|---|
PageSaved | Pageable | After a page is created or updated |
PageDeleted | Pageable | After a page is deleted |
SiteCreated | Site | After a site is created |
SiteReplicated | Site (both) | After a site and all its pages are cloned (includes mappings) |
PackageInstalled | PackageData | After a package is installed |
PackageUninstalled | PackageData | After a package is uninstalled |
ThemeColorsUpdated | Theme | After theme color settings are updated |
ServingCapell | (empty) | When the frontend boots (used to signal Capell is running) |
Event names are passed as strings (e.g., 'page.saved') or as backed enum values.
Example: Class-Based Subscriber
Section titled “Example: Class-Based Subscriber”Step 1: Implement the Subscriber interface
Section titled “Step 1: Implement the Subscriber interface”<?php
declare(strict_types=1);
namespace App\Subscribers;
use Capell\Core\Events\PageSaved;use Capell\Core\Support\Subscriber\Contracts\Subscriber;
final class LogPageSaveSubscriber implements Subscriber{ public function handle(string $event, object $context): void { if (! $context instanceof PageSaved) { return; }
logger()->info('Page saved', [ 'page_id' => $context->page->id, 'slug' => $context->page->slug, ]); }}Step 2: Register in a service provider
Section titled “Step 2: Register in a service provider”<?php
declare(strict_types=1);
namespace App\Providers;
use App\Subscribers\LogPageSaveSubscriber;use Capell\Core\Support\Subscriber\SubscriberManager;use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider{ public function boot(): void { $manager = resolve(SubscriberManager::class); $manager->subscribe(LogPageSaveSubscriber::class); }}Step 3: Use a validating subscriber to block operations
Section titled “Step 3: Use a validating subscriber to block operations”<?php
declare(strict_types=1);
namespace App\Subscribers;
use Capell\Core\Events\PageSaved;use Capell\Core\Support\Subscriber\Contracts\ValidatingSubscriber;
final class EnforcePageSlugPrefixSubscriber implements ValidatingSubscriber{ public function handle(string $event, object $context): void { // Called after validation passes; perform side effects here. }
public function validate(string $event, object $context): bool { if ($event !== 'page.saving' || ! $context instanceof PageSaved) { return true; }
// Block the save if slug doesn't match our convention. return str_starts_with($context->page->slug, 'policy-'); }}When validateWithSubscribers('page.saving', $pageSaved) is called, if this subscriber returns false, the action halts without proceeding.
Gotchas
Section titled “Gotchas”-
Event name != class name. Event instances are classes (e.g.,
PageSaved), but event names are strings (e.g.,'page.saved'). Your subscriber must check both the string and optionally type-guard the object. -
No automatic injection. Subscribers are resolved from the container but do not receive constructor dependencies automatically in
handle(). Inject them in__construct()and store as properties, or resolve them manually insidehandle(). -
Validation subscribers only.
ValidatingSubscriberis the only interface that short-circuits. PlainSubscriberinstances are called byvalidateWithSubscribers()but their return value is ignored. -
Order is insertion order. Subscribers execute in the order they were registered. If you need a specific order, register them in that order.
-
Enum values. Event names can be
BackedEnumconstants (e.g.,PageEvent::Saved->value). The manager extracts the string value automatically.
Related
Section titled “Related”- Extending Capell: Event Registry (§5) — simpler callback-style API for admin lifecycle events.
- Admin event registry — admin-layer event subscriptions (higher-level).
- Content management — domain context where
SubscriberManageris typically dispatched from.