Readme

Latch

A cross-package hook/filter registry system for PHP. Apps register themselves as “hook sources” and declare typed extension points. Other packages attach handlers to those points. The source app queries the registry at runtime to collect, transform, or broadcast through those handlers.

Requirements

  • PHP ^8.2

Installation

composer require chenasraf/latch

Quick Start

use Latch\HookRegistry;

$registry = new HookRegistry();

// 1. Source declares extension points
$cms = $registry->registerSource('cms')
    ->filter('render-html', RenderPayload::class)
    ->action('page-published', PageEvent::class)
    ->collect('nav-items', NavContext::class);

// 2. Register a named handler and attach to those points
$seo = $registry->registerHandler('seo');

$seo->hook('cms', 'render-html')
    ->priority(5)
    ->handle(fn (RenderPayload $p) => $p->withHtml(minify($p->html)));

$seo->hook('cms', 'nav-items')
    ->when(fn (NavContext $ctx) => $ctx->user->isAdmin())
    ->handle(fn (NavContext $ctx) => [new NavItem('Admin', '/admin')]);

// 3. Source invokes at runtime
$payload = $cms->apply('render-html', new RenderPayload($html));
$cms->dispatch('page-published', new PageEvent($page, $user));
$navItems = $cms->collectFromHandlers('nav-items', new NavContext($user));

Hook Types

TypeSource invokesHandler receivesHandler returns
Filterapply()The payload objectA modified payload (chained)
Actiondispatch()The payload objectNothing (return value ignored)
CollectcollectFromHandlers()An optional context objectAn array of items (merged flat)

Handler Options

$handler = $registry->registerHandler('my-plugin');

$handler->hook('source', 'point')
    ->priority(5)           // Lower runs first (default: 10)
    ->exclusive()           // Short-circuits remaining handlers after this one
    ->when(fn ($p) => ...)  // Skip handler if condition returns false
    ->tag('admin', 'ui')   // Additional tags for introspection and filtering
    ->handle(fn ($p) => ...);

All hooks are automatically tagged with handler:{name}. Sources can target specific handlers:

$cms->dispatch('page-published', $event, ['handler:seo']);

Each handler name can only be registered once. Pass the HookHandler instance via DI to reuse it across your package.

Existence Checks

$registry->hasSource('cms');          // Does this source exist?
$cms->hasHandlers('render-html');    // Does anyone handle this point?

Framework Integration

FrameworkSetup
LaravelAuto-discovered — inject HookRegistryInterface anywhere
NextcloudInstall Latch as a Nextcloud app, use LatchBootstrap::registry()
Plain PHPHookRegistry::getInstance() for shared singleton, or new HookRegistry()

Documentation

  • Guide — Full API reference for sources, handlers, introspection, and error handling
  • Examples — End-to-end examples and framework integration walkthroughs

Development

make install       # Install dependencies
make install-hooks # Set up lefthook git hooks
make test          # Run tests (Pest)
make analyze       # Static analysis (PHPStan level 8)
make fix           # Code style (Laravel Pint)

License

MIT