Event Hooks
Observe app lifecycle events without touching framework code.
Overview
$app->on() registers a listener and $app->emit() fires an event. Listeners are observational only: they receive typed arguments, their return values are ignored, and there is no return false to block. A listener that wants to stop something throws or redirects, like any other guard.
// 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);
Listeners run in registration order. Addons load before routing, so app.route:after is the earliest point where the resolved Page is available.
$app->onEmit(callable $observer) registers a separate observer, called after every listener with (string $event, string $source, float $ms). It exists for tooling such as a request timeline and is not for control flow.
Naming
Events are named domain.action:timing, such as page.update:after. The full string is matched exactly.
Events
| Event | When | Args |
|---|---|---|
app.request:before |
The first event of each request, before the page is resolved | none |
app.resolve:after |
The deepest matching page has resolved, before routing | Page $page |
app.route:after |
$page->route() has resolved the routed page |
Page $page |
app.addons:after |
All addons have loaded | none |
app.exception |
A Throwable was caught while handling the request |
Throwable $e, int $code |
app.shutdown |
Once per request, at the very end | none |
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.create:after |
After Page::new() has saved a new page |
Page $page |
page.delete:before |
Before a page is deleted, once for each descendant and then the page | Page $page |
page.delete:after |
After a page is deleted | Page $page |
page.update:after |
After Page::save() writes |
Page $page |
page.move:after |
After a page is moved | Page $old, Page $new |
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 written | string $variantPath |
item.create:after |
After a collection item is created | Item $item |
item.update:after |
After a collection item is saved | Item $item |
item.delete:before |
Before a collection item is deleted | Item $item |
item.delete:after |
After 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 / none |
user.create:after |
After a user is created | User $user |
user.update:after |
After a user is saved | User $user |
user.delete:before |
Before a user is deleted | User $user |
A create event fires once, when the new record has been saved, and carries the record as saved. Every later save fires update.
Example
Register listeners in an addon's index.php:
$app->on('app.route:after', function (Page $page) use ($app) {
// read and observe
error_log("Page resolved: {$page->title}");
// don't mutate or redirect here: that breaks the observational contract
});