Skip to content

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 SubscriberManager to register class-based subscribers that listen to core events. Call notifySubscribers() to invoke all listeners, or validateWithSubscribers() to short-circuit on validation failures.


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 SubscriberManager when you need:

    • Validation gates: Subscribers that return false to 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.
  • Use the simpler CapellAdmin event registry (see extending-capell.md §5) when you need:

    • Lightweight admin-panel lifecycle hooks (afterSave, beforeDelete, etc.).
    • One-off callback functions.
    • No validation logic.

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).

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.

  • 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 function subscribe(string $subscriber): void

Register a subscriber class. Must implement Subscriber or ValidatingSubscriber.

public function unsubscribe(string $subscriber): void

Deregister a subscriber class.

public function getSubscribers(): array

Return array of registered subscriber class names.

public function hasSubscriber(string $subscriber): bool

Check if a subscriber is registered.

public function notifySubscribers(string|BackedEnum $event, object $context): void

Invoke 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): bool

Invoke validation subscribers. Returns false if any ValidatingSubscriber returns false; short-circuits and returns true otherwise. Plain Subscriber instances are skipped.


Event ClassContext TypeWhen Fired
PageSavedPageableAfter a page is created or updated
PageDeletedPageableAfter a page is deleted
SiteCreatedSiteAfter a site is created
SiteReplicatedSite (both)After a site and all its pages are cloned (includes mappings)
PackageInstalledPackageDataAfter a package is installed
PackageUninstalledPackageDataAfter a package is uninstalled
ThemeColorsUpdatedThemeAfter 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.


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,
]);
}
}
<?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.


  1. 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.

  2. 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 inside handle().

  3. Validation subscribers only. ValidatingSubscriber is the only interface that short-circuits. Plain Subscriber instances are called by validateWithSubscribers() but their return value is ignored.

  4. Order is insertion order. Subscribers execute in the order they were registered. If you need a specific order, register them in that order.

  5. Enum values. Event names can be BackedEnum constants (e.g., PageEvent::Saved->value). The manager extracts the string value automatically.