Purpose

The first event in the page-resolution pipeline. Gives plugins a shot at claiming the request URL before Scriptor's built-in lookup runs. Listeners cooperate via the mutable resolution slot: the first listener to assign a Page to it wins.

This is the event the bundled DbPagesResolverPlugin (Scriptor's own DB-backed slug resolver) listens to, and the event scriptor-markdown-pages listens to so a /docs/concepts/... URL can resolve to a virtual page generated from a markdown file instead of a DB row.

FQCN + file path

When to use

Subscribe inside Plugin::register() when you want to:

  • Resolve a URL pattern to a virtual page (markdown files, generated archive pages, on-the-fly product detail pages).
  • Override the default DB resolution for a specific URL shape (e.g. a redirect rule that produces a temporary "stub" page whose template handles the redirect).

You do not use this event for read-only side effects after resolution; that is what PageResolved is for.

Surface

Public properties

Property Type Purpose
urlSegments readonly UrlSegments Parsed segments of the current request URL. Pass to your own resolver
resolution ?Page (mutable) The slot listeners fill. Starts null. First non-null wins

No methods. The DTO is just data plus the writable slot.

Constructor

public function __construct(public readonly UrlSegments $urlSegments)

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

Lifecycle

Constructed and dispatched once per request at the top of Scriptor\Boot\Frontend\Site::execute(). After the event has run through every listener, Site reads resolution:

  • Non-null: the page is taken, PageResolved fires with that page, execute() returns.
  • Null: Site dispatches RouteNotFound as a last-chance resolver; if that also leaves the slot empty, throw404().

Listeners run in registration order (whichever plugin called PluginContext::subscribe() first). The "first writer wins" contract is enforced by convention, not by the event: a misbehaving listener can overwrite a previous resolution. The conventional listener body checks the slot before writing.

Common patterns

Resolving a virtual page from a custom source

public function register(PluginContext $context): void
{
    $context->subscribe(
        \Scriptor\Boot\Events\Frontend\PageResolving::class,
        function (\Scriptor\Boot\Events\Frontend\PageResolving $event): void {
            if ($event->resolution !== null) {
                return;   // someone already resolved
            }
            $page = $this->mySource->find($event->urlSegments);
            if ($page !== null) {
                $event->resolution = $page;
            }
        },
    );
}

The two-line guard at the top (!== null → return) is the canonical body. Without it, two plugins racing for the same URL would silently overwrite each other and the slot would always end up with whichever subscribed last.

Claiming a URL prefix (markdown-pages-style)

$context->subscribe(
    PageResolving::class,
    function (PageResolving $event): void {
        if ($event->resolution !== null) {
            return;
        }
        $path = $event->urlSegments->path();
        if (! str_starts_with($path, 'docs/')) {
            return;     // not our prefix
        }
        $markdownFile = __DIR__ . '/content/' . substr($path, 5) . '.md';
        if (is_file($markdownFile)) {
            $event->resolution = $this->virtualPageFactory->fromFile($markdownFile);
        }
    },
);

Generating a virtual archive page

$context->subscribe(
    PageResolving::class,
    function (PageResolving $event): void {
        if ($event->resolution !== null) {
            return;
        }
        $segments = $event->urlSegments;
        if ($segments->first() !== 'archive') {
            return;
        }
        $year  = (int) ($segments->at(1) ?? 0);
        $month = (int) ($segments->at(2) ?? 0);
        if ($year > 0 && $month > 0) {
            $event->resolution = $this->archiveFactory->build($year, $month);
        }
    },
);

See also

  • PageResolved: fires next when this event produced a page
  • RouteNotFound: fires next when this event did not produce a page
  • Page: the DTO listeners assign to resolution
  • Site: the execute() method that dispatches this event
  • Concept: Frontend Events: walks the pipeline shape (resolving → resolved or not-found → 404)