mirror of
https://github.com/chenasraf/latch.git
synced 2026-05-17 17:28:08 +00:00
136 lines
4.4 KiB
Markdown
136 lines
4.4 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
composer require chenasraf/latch
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
```php
|
|
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
|
|
|
|
| Type | Source invokes | Handler receives | Handler returns |
|
|
| ------- | -------------- | -------------------------- | ------------------------------- |
|
|
| Filter | `apply()` | The payload object | A modified payload (chained) |
|
|
| Action | `dispatch()` | The payload object | Nothing (return value ignored) |
|
|
| Collect | `collectFromHandlers()`| An optional context object | An array of items (merged flat) |
|
|
|
|
## Handler Options
|
|
|
|
```php
|
|
$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:
|
|
|
|
```php
|
|
$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.
|
|
|
|
## Capability Discovery
|
|
|
|
Both sources and handlers can declare capability tags at registration. The registry can then be
|
|
queried to find either side by capability — without knowing names in advance.
|
|
|
|
```php
|
|
// Sources declare what they provide
|
|
$registry->registerSource('cms', tags: ['content-management', 'publishing']);
|
|
|
|
// Handlers declare what they provide
|
|
$registry->registerHandler('seo', ['content-enhancement']);
|
|
$registry->registerHandler('minifier', ['content-enhancement']);
|
|
$registry->registerHandler('analytics', ['tracking']);
|
|
|
|
// Discover handlers by capability
|
|
$enhancers = $registry->handlersByTag('content-enhancement');
|
|
// → [HandlerInfo('seo'), HandlerInfo('minifier')]
|
|
|
|
// Discover sources by capability
|
|
$cmsSources = $registry->sourcesByTag('content-management');
|
|
// → [SourceStore('cms')]
|
|
```
|
|
|
|
This enables a discovery pattern where either side can find compatible counterparts at runtime,
|
|
present them to the user, and target the chosen one.
|
|
|
|
## Existence Checks
|
|
|
|
```php
|
|
$registry->hasSource('cms'); // Does this source exist?
|
|
$cms->hasHandlers('render-html'); // Does anyone handle this point?
|
|
```
|
|
|
|
## Framework Integration
|
|
|
|
| Framework | Setup |
|
|
| --------- | ----- |
|
|
| Laravel | Auto-discovered — inject `HookRegistryInterface` anywhere |
|
|
| Nextcloud | Each app bundles Latch; use `LatchBootstrap::registry()` for cross-app hooks via NC events |
|
|
| Plain PHP | `HookRegistry::getInstance()` for shared singleton, or `new HookRegistry()` |
|
|
|
|
## Documentation
|
|
|
|
- **[Guide](docs/guide.md)** — Full API reference for sources, handlers, introspection, and error handling
|
|
- **[Examples](docs/examples.md)** — End-to-end examples and framework integration walkthroughs
|
|
|
|
## Development
|
|
|
|
```bash
|
|
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
|