phug/event
phug/event provides a lightweight event emitter and dispatcher for the Phug template engine, letting you register listeners and trigger events around parsing, compilation, and rendering. Simple to integrate and framework-agnostic.
phug/event is a tiny, dependency-free event emitter (the one Phug's compiler/renderer uses internally). It is not a Laravel replacement for the dispatcher — it is what you reach for when you want decoupled hooks inside a package or a Phug-powered renderer without dragging illuminate/events or symfony/event-dispatcher into something that must also run outside a Laravel app.
composer require phug/event
Where to look first, in order:
src/EventEmitterTrait.php — the whole API surface (on, once, off, trigger, hasListener). This is the file you will actually read; everything else is a thin interface.src/Event.php / EventInterface — the payload object (getName, getTarget, getParams, setParams).src/EventEmitterInterface.php — type-hint against this, not the trait, so you can swap implementations later.First use case — an in-process hook on a class you own:
use Phug\Event\Event;
use Phug\Event\EventEmitterTrait;
class InvoiceImporter
{
use EventEmitterTrait;
public function import(string $path): int
{
$count = 0;
foreach ($this->rows($path) as $row) {
$this->trigger('row.parsed', $row, $count);
$count++;
}
return $count;
}
}
$importer = new InvoiceImporter();
$importer->on('row.parsed', function (Event $event) {
Log::debug('row', $event->getParams());
});
If you are here because of Phug itself: the Renderer already uses this emitter, so $renderer->on('renderer.render', ...) works out of the box with no extra wiring.
Payload object, not return values. Listener return values are discarded; the only way to communicate back is a mutable Event payload. Pass an array or a small DTO by reference inside the event, or mutate via setParams():
$emitter->on('markdown.before', function (Event $e) {
$params = $e->getParams();
$params['html'] = Str::markdown($params['text']);
$e->setParams($params);
});
$emitter->trigger('markdown.before', $text);
Order is registration order. There are no priorities (unlike Symfony's dispatcher). Treat registration order as API: register listeners in boot() in a deliberate sequence, and never assume a listener registered by a third party runs before yours.
Bridging into Laravel. The common production pattern is one-directional forwarding in a service provider — Phug events become Laravel events so the rest of your app (queues, broadcasting, Event::fake() in tests) keeps working:
public function boot(): void
{
$renderer = $this->app->make(Renderer::class);
$renderer->on('renderer.render', function (Event $event) {
event(new TemplateRendered($event->getParams()));
});
}
Bind the Renderer as a singleton in register() so the listener registrations in boot() land on the same instance that renders.
once for bootstrapping. Cache priming, one-shot deprecation warnings, and "first render only" setup are cleaner than a boolean flag:
$renderer->once('renderer.render', fn () => $this->warmCache());
Guard expensive work.
if ($emitter->hasListener('renderer.render')) {
$event = $this->buildExpensivePayload();
}
Testing. There is no Event::fake() equivalent — spy on the boundary instead:
$emitter->on('row.parsed', $spy = Mockery::mock(\stdClass::class));
or, more simply, push into a captured array in the test and assert counts/order.
Keep it behind your own interface. Given the effectively zero dependent count on Packagist, define interface TemplateHooks { public function onRender(callable $cb): void; } and implement it with phug/event today, Symfony's dispatcher tomorrow. One file of indirection buys you an exit ramp.
EventDispatcherInterface, no stoppable-event contract unless the shipped Event class implements one — check EventInterface before you rely on propagation control. If you need PSR-14 compliance, wrap it; do not advertise the class as PSR-14.EventEmitterTrait declares its own storage property. If your class (or a parent) already has a similarly named property, PHP fatals on conflicting definitions. Use composition — an EventEmitter instance behind a getter — when your class already has a busy property set.$this and everything it references. Under Octane or long-running queue workers this is a slow memory bleed; call off() for your listener in a terminate hook, or scope listeners to per-request instances.row.parsed → row.parsed.internal).fn ($e) => $this->app->make(Foo::class)->handle($e)), not by expecting the emitter to inject anything.ShouldQueue do its job. Same for anything needing DB::transaction() boundaries; the emitter knows nothing about transactions or rollbacks.Event objects. They may carry a target reference (your renderer, your importer); put only scalar arrays on the wire.new) before anything else — that is the cause roughly nine times out of ten. eventNames() / listener inspection on the trait is your only visibility.How can I help you explore Laravel packages today?