Purpose

Fires after the page resolution dance is over and Site has picked the page it is about to render. Read-only by design: listeners cannot rewrite the result here. They observe, react, or interrupt by throwing.

The event exists so plugins have a single, late-enough hook for "the page exists, the URL is real, do something now". Typical use is logging a page view, populating a request-scoped cache, or running an authorisation check that raises when the authenticated visitor is not allowed to see this page.

FQCN + file path

When to use

Subscribe inside Plugin::register() when you want a hook that runs:

  • Once per request, but only when a page was actually resolved (no 404).
  • After any plugin's PageResolving listener and the built-in fall-throughs have finished.
  • Before the template renders. You can still throw, return a redirect, or call $site->throw404() from here.

If you need to mutate the rendered HTML, this is the wrong event; use ContentRendering. If you need to handle the no-match case, listen to RouteNotFound.

Surface

Public properties

Property Type Purpose
page Page (readonly) The resolved page. Always non-null when this event fires

The class is final readonly. No mutable state, no methods.

Constructor

public function __construct(public Page $page)

Site::execute() constructs the event with the resolved page. You do not construct it yourself.

Lifecycle

Fires at most once per request, from inside Scriptor\Boot\Frontend\Site::execute(), immediately after a non-null Page lands in $site->page. Both resolution paths trigger it:

  • PageResolving produced a page → PageResolved fires.
  • PageResolving produced nothing but RouteNotFound did → PageResolved fires.
  • Both produced nothing → Site::throw404(); PageResolved does not fire.

Listeners run in registration order. Any uncaught exception from a listener propagates up; subsequent listeners do not run, and the rendering pipeline never reaches template.php.

Common patterns

Logging a page view

$context->subscribe(
    \Scriptor\Boot\Events\Frontend\PageResolved::class,
    function (\Scriptor\Boot\Events\Frontend\PageResolved $event) use ($logger): void {
        $logger->info('page resolved: {slug}', [
            'slug' => $event->page->slug,
            'id'   => $event->page->id(),
        ]);
    },
);

Authorisation gate that throws

$context->subscribe(
    PageResolved::class,
    function (PageResolved $event): void {
        $page = $event->page;
        if ($page->pagetype === 'members-only' && ! $this->session->isAuthenticated()) {
            // Bail out before any template renders.
            $this->site->redirect('/login/?next=' . urlencode($_SERVER['REQUEST_URI']), 303);
        }
    },
);

Priming a request-scoped cache

$context->subscribe(
    PageResolved::class,
    function (PageResolved $event) use ($cache): void {
        $cache->prime(
            'related-posts.' . $event->page->id(),
            fn() => $this->findRelated($event->page),
        );
    },
);

See also