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
- FQCN:
Scriptor\Boot\Frontend\Nav\FrontendNavRegistry - Source:
boot/Frontend/Nav/FrontendNavRegistry.php
When to use
Two angles:
- A plugin registers a builder via
PluginContext::contributeFrontendNav($builder)from insidePlugin::register(). The builder is called once per request with the liveUrlSegments; it returns alist<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
NavItem: the DTO every builder returnsPluginContext: how plugins register builders without touching the registry directly- Concept: FrontendNavRegistry: walk-through of the design (why callables, why no active flag)