Purpose

The "is anyone willing to handle this URL?" event that fires after PageResolving produced nothing. Listeners can still claim the request by filling the same resolution slot PageResolving exposes. If no listener does, Site::throw404() runs and the 404 page renders.

This event exists so a plugin that handles deep dynamic URLs (per-item detail pages, parametric report URLs, generated gallery views) does not have to listen on PageResolving and return early for every URL it does not care about. By subscribing to RouteNotFound instead, the plugin's listener runs only after every other resolver has passed.

FQCN + file path

When to use

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

  • Runs only when nothing else claimed the URL (cheap by design: the hot path skips you).
  • Can claim the URL after all by filling resolution.
  • Lets you opt into the existing PageResolved pipeline once you do (if you fill the slot, PageResolved fires next as if PageResolving had produced the page).

You do not use this event to render a custom 404 page; that is a theme concern ($config['404page'] + the theme's 404.php). You also do not use it for blanket interception of 404s for logging; subscribe normally and pair with a sentinel-page check.

Surface

Public properties

Property Type Purpose
urlSegments readonly UrlSegments Parsed segments of the unresolved URL
resolution ?Page (mutable) The slot listeners fill to claim the URL. Starts null. First non-null wins

No methods.

Constructor

public function __construct(public readonly UrlSegments $urlSegments)

Site::execute() constructs the event with the request's UrlSegments and dispatches it after PageResolving produced nothing. You do not construct it yourself.

Lifecycle

Fires at most once per request, after PageResolving ran and left resolution empty. Two outcomes:

  • A listener writes resolution: Site accepts the page, PageResolved fires next, the request flows normally into rendering.
  • The slot stays empty: Site::throw404() runs. PageResolved does not fire.

The cooperative convention from PageResolving carries over: the canonical listener body starts with if ($event->resolution !== null) return; to keep ordering between multiple RouteNotFound subscribers sane.

Common patterns

Claiming a dynamic detail-page URL after a miss

$context->subscribe(
    \Scriptor\Boot\Events\Frontend\RouteNotFound::class,
    function (\Scriptor\Boot\Events\Frontend\RouteNotFound $event): void {
        if ($event->resolution !== null) {
            return;
        }
        $segments = $event->urlSegments;
        if ($segments->first() !== 'product') {
            return;
        }
        $sku = $segments->at(1);
        if ($sku === null) {
            return;
        }
        $product = $this->products->find($sku);
        if ($product !== null) {
            $event->resolution = $this->virtualPageFactory->fromProduct($product);
        }
    },
);

Redirecting away from a known old URL

$context->subscribe(
    RouteNotFound::class,
    function (RouteNotFound $event) use ($redirects, $site): void {
        if ($event->resolution !== null) {
            return;
        }
        $target = $redirects->lookup((string) $event->urlSegments->path());
        if ($target !== null) {
            $site->redirect($target, 301);   // never returns
        }
    },
);

Note: redirect() exits, so this never falls through to a resolution write. The pattern is "produce a side effect, do not let the 404 path run".

Logging the miss before the 404 renders

$context->subscribe(
    RouteNotFound::class,
    function (RouteNotFound $event) use ($logger): void {
        if ($event->resolution !== null) {
            return;
        }
        $logger->info('404: {path}', ['path' => (string) $event->urlSegments->path()]);
    },
);

Pure observation; the slot stays empty so the normal 404 still runs.

See also

  • PageResolving: the first event in the resolution pipeline; uses the same resolution-slot shape
  • PageResolved: fires next when this event produces a page
  • Page: the DTO listeners assign to resolution
  • Site: the execute() method that dispatches this event after PageResolving
  • Concept: Frontend Events: pipeline overview