Event Hooks
A lightweight observational event system for reacting to app lifecycle stages without coupling to framework internals.
Overview
$app exposes on() and emit() for registering and firing lifecycle events. Listeners are observational only, they receive typed arguments but return values are ignored, and there's no return false-to-block convention. A listener that wants to stop something throws or redirects directly, the same as any other guard in the codebase.
API
// Register a listener
$app->on('app.route:after', function (Page $page) use ($app) {
// observe the resolved page
});
// Emit an event (framework internals only)
$app->emit('app.route:after', $this->page);
$app->onEmit(callable $observer) registers a separate kind of observer, notified after every hook listener runs with (string $event, string $source, float $ms), for building tooling like a request-timeline panel. It's also observational only, never for control flow.
Naming convention
Event names follow domain.action:timing, e.g. page.update:after. Every event name begins with a domain; there's no qualifier syntax, the full string is matched exactly against what a listener registered.
Current hooks
| Event | When | Args |
|---|---|---|
app.resolve:after |
The deepest matching page for the request has resolved, before routing | Page $page |
app.route:after |
$page->route() has resolved the routed page |
Page $page |
app.addons:after |
All addons have finished loading | — |
app.exception |
A Throwable was caught while handling the request |
Throwable $e, int $code |
app.shutdown |
Once per request, at the very end | — |
page.controllers:after |
The page's controllers have run, just before render | Page $page |
page.render:before / page.render:after |
Around template rendering | Page $page |
page.update:after |
After Page::save() writes |
Page $page |
field.update:after |
After a global field definition (site/fields/{name}/index.json) is saved |
Field $field |
file.update:after |
After a page file is stored or its metadata updated | PageFile $file |
file.delete:after |
After a page file is deleted | string $pageUuid, string $filename |
image.save:after |
After an image variant is encoded and written | string $variantPath |
item.update:after |
After a collection item is saved | Item $item |
item.delete:before |
Before a collection item is deleted | Item $item |
user.login:before / user.login:after |
Around a login attempt | string $username / User $user |
user.logout:before / user.logout:after |
Around logout | ?User $user / — |
Addons
Event hooks are the primary way addons observe the request lifecycle without requiring changes to framework code. Register listeners in your addon's index.php:
$app->on('app.route:after', function (Page $page) use ($app) {
// populate a Clockwork panel, log to a file, etc.
});
Listeners run in registration order. Because addons load before routing, app.route:after is the earliest point at which the resolved Page is available.
<?php
use Phlat\App;
use Phlat\Page;
// In an addon's index.php, $app is injected
// Observe the resolved page after routing
$app->on('app.route:after', function (Page $page) use ($app) {
// Correct: read and observe
$title = $page->title;
error_log("Page resolved: {$title}");
// Wrong: do not mutate or redirect from a listener
// Res::redirect('/somewhere'); // violates the observational contract
});
// Multiple listeners on the same event are all called
$app->on('app.route:after', function (Page $page) {
// also fires
});