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
});