Purpose

The collection point for navigation a plugin wants to contribute to a theme. Plugins register a builder callable; the registry runs every registered builder once per request, merges the returned NavItem lists, sorts by position, and hands the flat top-level list back to the theme. The theme decides how to render it.

The registry stays small on purpose: no event system, no priority beyond the explicit position field, no active-state flags on the NavItems. Builders are callables so a plugin can shape its contribution based on the current request URL (for example, expanding the active section but collapsing the rest) without the registry having to expose a request hook.

FQCN + file path

When to use

Two angles:

  • A plugin registers a builder via PluginContext::contributeFrontendNav($builder) from inside Plugin::register(). The builder is called once per request with the live UrlSegments; it returns a list<NavItem>.
  • A theme pulls the registry out of the container at render time and calls collect($urlSegments) to get the merged list to render.

If you need cross-request state or want to react to events, this is the wrong tool; this is a registry for static-shaped, request-aware nav contributions.

Surface

public function contribute(callable $builder): void

Register a builder. The callable's signature is function(UrlSegments $url): array<int, NavItem>. The registry keeps the registration order so ties on position are stable.

Plugins do not call this directly; they use PluginContext::contributeFrontendNav() from inside Plugin::register(). The context forwards to this method.

public function collect(UrlSegments $url): array

Build the merged, sorted top-level NavItem list for the given request URL. Walks every registered builder, asks it for its contribution, filters out non-NavItem entries (silently dropped, not raised), then sorts by ascending position. Ties on position are broken by registration order, so the first plugin to register a position = 0 item lands first.

Return type is list<NavItem>: top-level entries only. Each entry may carry nested children; rendering nested levels is the theme's job.

public function contributorCount(): int

How many builders are registered. Used by the editor's PluginsModule to report per-plugin counts in the admin UI. Not something a theme typically calls.

Lifecycle

Container-bound singleton, request-scoped. One instance lives in the container; both PluginContext::contributeFrontendNav() and $site->container->get(FrontendNavRegistry::class) reach the same instance.

Builders register at plugin-boot time (Plugin::register(), runs once per request before the theme's _ext.php). The first call to collect() runs every builder; subsequent calls re-run them. The registry does not memoise: if the theme calls collect() more than once per request, every builder runs again. In practice themes call it once per render pass.

Common patterns

Plugin contributes a nav builder

use Imanager\Http\UrlSegments;
use Scriptor\Boot\Frontend\Nav\NavItem;
use Scriptor\Boot\Plugin\Plugin;
use Scriptor\Boot\Plugin\PluginContext;

final class DocsNavPlugin implements Plugin
{
    public function register(PluginContext $context): void
    {
        $context->contributeFrontendNav(static function (UrlSegments $url): array {
            return [
                new NavItem(
                    url:   '/docs/getting-started/',
                    label: 'Getting Started',
                    position: 10,
                ),
                new NavItem(
                    url:   '/docs/api/',
                    label: 'API',
                    position: 20,
                ),
            ];
        });
    }
}

Theme consumes the merged nav

$registry = \Scriptor\Boot\App::container()
    ->get(\Scriptor\Boot\Frontend\Nav\FrontendNavRegistry::class);

$items = $registry->collect($site->urlSegments);

echo '<ul class="plugin-nav">';
foreach ($items as $item) {
    $isActive = str_starts_with('/' . $site->urlSegments->path(), $item->url);
    printf(
        '<li class="%s"><a href="%s">%s</a></li>',
        $isActive ? 'is-active' : '',
        htmlspecialchars($item->url,   \ENT_QUOTES),
        htmlspecialchars($item->label, \ENT_QUOTES),
    );
}
echo '</ul>';

URL-aware builder (expand the active section only)

$context->contributeFrontendNav(function (UrlSegments $url) {
    $path = '/' . $url->path();
    $tracks = ['guide', 'reference', 'cookbook'];
    $items  = [];
    foreach ($tracks as $i => $slug) {
        $isActive = str_starts_with($path, "/$slug/");
        $children = $isActive ? $this->loadTrackChildren($slug) : [];
        $items[] = new NavItem(
            url:      "/$slug/",
            label:    ucfirst($slug),
            position: ($i + 1) * 10,
            children: $children,
        );
    }
    return $items;
});

See also