# Examples ## End-to-End: CMS + SEO Plugin A CMS app that declares extension points, and an SEO plugin that hooks into them. ### Source side — CMS app ```php use Latch\HookRegistry; class CmsApp { private HookSource $cms; public function __construct(private HookRegistry $registry) { $this->cms = $registry->registerSource('cms') ->filter('render-html', RenderPayload::class) ->filter('page-title', TitlePayload::class) ->action('page-published', PageEvent::class) ->collect('head-tags', PageContext::class); } public function renderPage(Page $page): string { $payload = new RenderPayload($page->html); $payload = $this->cms->apply('render-html', $payload); $title = new TitlePayload($page->title); $title = $this->cms->apply('page-title', $title); $headTags = $this->cms->collectFromHandlers( 'head-tags', new PageContext($page) ); return << {$title->value} {$this->renderTags($headTags)} {$payload->html} HTML; } public function publishPage(Page $page, User $actor): void { $page->publish(); $this->cms->dispatch( 'page-published', new PageEvent($page, $actor) ); } private function renderTags(array $tags): string { return implode("\n ", $tags); } } ``` ### Handler side — SEO plugin ```php use Latch\HookRegistry; class SeoPlugin { public function __construct(HookRegistry $registry) { // Register a named handler - all hooks auto-tagged with 'handler:seo' $seo = $registry->registerHandler('seo'); $seo->hook('cms', 'head-tags') ->priority(1) ->handle(fn (PageContext $ctx) => [ "page->excerpt}\">", "page->title}\">", ]); $seo->hook('cms', 'page-title') ->priority(50) ->handle(fn (TitlePayload $t) => $t->withValue( $t->value . ' | My Site' )); $seo->hook('cms', 'render-html') ->priority(90) ->handle(fn (RenderPayload $p) => $p->withHtml( $p->html . $this->jsonLdScript($p) )); $seo->hook('cms', 'page-published') ->handle(fn (PageEvent $e) => SitemapQueue::push($e->page->url)); } private function jsonLdScript(RenderPayload $payload): string { return ''; } } ``` ### Wiring ```php $registry = new HookRegistry(); $cms = new CmsApp($registry); $seo = new SeoPlugin($registry); echo $cms->renderPage($page); ``` ## Tag Filtering: Targeted Invocation When multiple handlers are registered for the same point, the source can use tag filtering to only invoke specific ones. Pass a list of tags to `apply()`, `dispatch()`, or `collectFromHandlers()` and only handlers with at least one matching tag will run. Using `registerHandler()`, each plugin gets auto-tagged with `handler:{name}`, making targeted invocation straightforward. ```php // Source registers with head-tags collect point $cms = $registry->registerSource('cms') ->collect('head-tags', PageContext::class); // SEO plugin registers with a named handler $seo = $registry->registerHandler('seo'); $seo->hook('cms', 'head-tags') ->handle(fn (PageContext $ctx) => [ "page->excerpt}\">", ]); // Analytics plugin registers with its own named handler $analytics = $registry->registerHandler('analytics'); $analytics->hook('cms', 'head-tags') ->handle(fn (PageContext $ctx) => [ '', ]); // Collect from all handlers (default) $allTags = $cms->collectFromHandlers('head-tags', $context); // Collect only from the SEO handler $seoTags = $cms->collectFromHandlers('head-tags', $context, ['handler:seo']); ``` ### Capability discovery Instead of hard-coding handler names, the source can discover handlers by capability tag and then target the chosen one. ```php // Handlers declare capabilities at registration $email = $registry->registerHandler('email', ['notification-delivery']); $push = $registry->registerHandler('push', ['notification-delivery']); // Both hook into the same points $cms = $registry->registerSource('cms') ->action('notify-subscribers', NotifyPayload::class); $email->hook('cms', 'notify-subscribers') ->handle(fn (NotifyPayload $p) => EmailService::send($p)); $push->hook('cms', 'notify-subscribers') ->handle(fn (NotifyPayload $p) => PushService::send($p)); // Step 1: Discover who can deliver notifications $providers = $registry->handlersByTag('notification-delivery'); // → [HandlerInfo('email', [...]), HandlerInfo('push', [...])] // Step 2: Present them to the user foreach ($providers as $p) { echo "{$p->name}\n"; // 'email', 'push' } // Step 3: Dispatch to the user's choice $chosen = $userChoice; // e.g. 'email' $cms->dispatch( 'notify-subscribers', $payload, ["handler:{$chosen}"] ); ``` This pattern lets the source app find compatible handlers at runtime without importing anything from the handler packages — only the capability tag string needs to be agreed upon. ### Handler discovering sources The reverse works too — a handler can discover sources by capability and hook into all of them: ```php // Sources declare capabilities $registry->registerSource('cms', null, ['content-management']) ->filter('render-html', RenderPayload::class); $registry->registerSource('blog', null, ['content-management']) ->filter('render-html', RenderPayload::class); // Handler discovers all content-management sources and hooks into each $minifier = $registry->registerHandler('minifier'); $sources = $registry->sourcesByTag('content-management'); foreach ($sources as $source) { $minifier->hook($source->id, 'render-html') ->handle(fn (RenderPayload $p) => $p->withHtml(minify($p->html))); } ``` ## Laravel Integration The service provider is auto-discovered. Inject `HookRegistryInterface` anywhere Laravel resolves dependencies. ### Source — CMS service provider ```php use Latch\Contracts\HookRegistryInterface; class CmsServiceProvider extends ServiceProvider { public function boot(HookRegistryInterface $registry): void { $cms = $registry->registerSource('cms') ->filter('render-html', RenderPayload::class) ->action('page-published', PageEvent::class) ->collect('nav-items', NavContext::class); } } ``` ### Handler — SEO service provider ```php class SeoServiceProvider extends ServiceProvider { public function boot(HookRegistryInterface $registry): void { $seo = $registry->registerHandler('seo'); $seo->hook('cms', 'render-html') ->priority(90) ->handle(fn (RenderPayload $p) => $p->withHtml( $p->html . '' )); } } ``` You can also resolve via the `latch` alias: ```php $registry = app('latch'); ``` ## Nextcloud Integration Each Nextcloud app bundles its own copy of Latch via `composer require`. Cross-app communication is handled transparently by `LatchBootstrap::registry()`, which returns a bridged registry that routes hooks through NC's shared `IEventDispatcher`. No shared autoloader or extra app needed. The API is identical to plain Latch — the only difference is using `LatchBootstrap::registry()` instead of `new HookRegistry()`. ### Source — app that owns the hooks ```php namespace OCA\CmsApp\AppInfo; use Latch\Integration\Nextcloud\LatchBootstrap; use Latch\Integration\Nextcloud\BridgedSource; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IRegistrationContext; class Application extends App implements IBootstrap { private BridgedSource $cms; public function __construct() { parent::__construct('cms'); } public function register(IRegistrationContext $context): void {} public function boot(IBootContext $context): void { $registry = LatchBootstrap::registry(); // Same API as plain Latch — declaration and invocation work the same way $this->cms = $registry->registerSource('cms') ->action('page-published', PageEvent::class) ->collect('head-tags', PageContext::class); } public function publishPage(Page $page, User $actor): void { // Dispatches to both local handlers and handlers in other apps $this->cms->dispatch( 'page-published', new PageEvent($page, $actor) ); } } ``` ### Handler — another Nextcloud app The handler app hooks into a remote source using the same `hook()` API. If the source doesn't exist in the local registry, it's treated as remote and the handler listens via NC events. ```php namespace OCA\SeoPlugin\AppInfo; use Latch\Integration\Nextcloud\LatchBootstrap; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IRegistrationContext; class Application extends App implements IBootstrap { public function __construct() { parent::__construct('seo'); } public function register(IRegistrationContext $context): void {} public function boot(IBootContext $context): void { $registry = LatchBootstrap::registry(); $handler = $registry->registerHandler('seo'); // Same API — hook into a source by name, even if it's in another app $handler->hook('cms', 'page-published') ->handle(fn ($payload) => SitemapQueue::push($payload->page->url)); } } ``` ### How it works Each app bundles its own copy of Latch. The bridged registry uses NC's `IEventDispatcher` (shared across all apps) with conventional event names like `latch:cms:page-published`. Payloads work across autoloader boundaries because PHP resolves methods on the actual object at runtime, not the type hint. All three hook types are supported through the bridge: - **Actions** — fire-and-forget through NC events - **Filters** — the event carries the payload; each listener mutates it and the source reads it back - **Collectors** — each listener adds items to the event; the source collects them after dispatch