Purpose
The single argument every plugin sees at boot time. It bundles together every framework surface a plugin is sanctioned to touch, plus a tracking side-channel that lets the editor's InstalledPlugins module show "this plugin contributed N events, M modules, K menu items, J nav builders".
Five public methods that do work: container() (DI access),
subscribe() (PSR-14 events), registerEditorModule(),
addEditorMenuItem(), contributeFrontendNav(). Plus
registrations() for read-back by the editor.
FQCN + file path
- FQCN:
Scriptor\Boot\Plugin\PluginContext - Source:
boot/Plugin/PluginContext.php
When to use
Inside your Plugin::register() method. The argument is the
context; everything happens through it. You do not construct it
yourself, do not pass it around, do not store it. It lives for
exactly the duration of register().
PluginManager constructs a separate context per plugin
(rather than sharing one) so each plugin's registrations are
isolated and the per-plugin breakdown in the editor stays
accurate.
Surface
Read-only properties
| Property | Type | Purpose |
|---|---|---|
pluginName |
string |
Human-readable name of the plugin this context belongs to. Same as Plugin::name(). Passed in by the manager |
container()
public function container(): Container
Returns the League DI container. Use this to bind your own
services, decorate framework services, or pull dependencies
straight out (for the rare case where you need one inside
register() itself, not inside a deferred factory).
The container is shared across the whole request. Bindings you add stay reachable from other plugins, from the theme, and from editor code. Do not use it to mutate framework bindings unless you have a specific reason; that is a way to break other plugins silently.
subscribe()
public function subscribe(string $eventClass, callable $handler): void
Subscribe to a PSR-14 event. The handler runs for every
dispatched event that is an instance of $eventClass (or a
subclass), in registration order. The class string is the
event's FQCN; the callable signature is
function(EventType $event): void.
Frontend events the framework dispatches: PageResolving,
PageResolved, ContentRendering, RouteNotFound. Subscribe
order matters when multiple plugins subscribe to the same event
and any of them mutate a slot (e.g. PageResolving::$resolution):
first writer wins by convention, and "first" is the first
plugin that registered.
registerEditorModule()
public function registerEditorModule(
string $slug,
callable $factory,
): void
Mount an editor module under /editor/<slug>/.... The factory
signature is function(Container $c, Editor $editor): Module.
It is called per request once the auth gate (if any) has
passed; defer expensive setup into the factory rather than into
register().
Re-registering an existing $slug replaces the previous factory.
Last writer wins.
addEditorMenuItem()
public function addEditorMenuItem(MenuItem $item): void
Add an entry to the editor chrome. Where it lands (sidebar
proper or the profile cluster) depends on $item->displayType.
Items render in ascending position order, with ties broken by
registration order.
contributeFrontendNav()
public function contributeFrontendNav(callable $builder): void
Register a nav-builder callable with the FrontendNavRegistry.
The builder's signature is
function(UrlSegments $url): list<NavItem>. It runs once per
request the theme asks for the merged nav; the registry sorts
the combined entries by NavItem::$position then registration
order.
Use this for plugin-owned content trees (markdown pages, blog posts, external links) instead of asking themes to walk plugin-owned directories. The FrontendNavRegistry concept doc covers the why in depth.
registrations()
public function registrations(): array
Read back what this plugin registered through the context:
[
'events' => list<string>, // event class FQCNs
'modules' => list<string>, // editor module slugs
'menuItems' => list<MenuItem>, // the items themselves
'navBuilders' => int, // count, not the builders
]
Used by the editor's InstalledPlugins module to render a per-plugin breakdown. Plugins do not normally call this.
Lifecycle
Constructed once per plugin per request, by PluginManager,
right before it calls your Plugin::register(). The context is
passed in as the single argument; you call methods on it; the
call stack unwinds and the framework moves on.
The tracking arrays (events, modules, menuItems,
navBuilderCount) are populated as you call the registration
methods. They are read back later by the InstalledPlugins
module. The actual registrations go through the framework
registries (SubscriberListenerProvider, ModuleRegistry,
MenuRegistry, FrontendNavRegistry); the context is the
narrow surface plugins see, the registries are the implementation
detail.
Common patterns
A plugin that does a bit of everything
public function register(PluginContext $context): void
{
// Bind a service of our own.
$context->container()->add(
\Acme\BlogReader\PostStore::class,
\Acme\BlogReader\FilesystemPostStore::class,
);
// Subscribe to a frontend event.
$context->subscribe(
\Scriptor\Boot\Events\Frontend\PageResolving::class,
function ($event): void {
// Maybe claim a /blog/* URL for a virtual page.
},
);
// Register an editor module under /editor/blog/.
$context->registerEditorModule(
'blog',
static fn($c, $editor) => new \Acme\BlogReader\Editor\BlogModule($c, $editor),
);
// Pin a link to it in the editor sidebar.
$context->addEditorMenuItem(new \Scriptor\Boot\Editor\Menu\MenuItem(
slug: 'blog',
label: 'Blog',
url: '/editor/blog/',
position: 50,
displayType: 'sidebar',
));
// Contribute a frontend nav entry pointing at the blog index.
$context->contributeFrontendNav(static function ($url): array {
return [new \Scriptor\Boot\Frontend\Nav\NavItem(
url: '/blog/',
label: 'Blog',
position: 40,
)];
});
}
Inspecting the plugin's own registrations (rare)
public function register(PluginContext $context): void
{
// ... a long list of subscribe / register / add calls ...
$context->container()
->get(\Psr\Log\LoggerInterface::class)
->debug('blog-reader registrations: {snapshot}', [
'snapshot' => json_encode($context->registrations()),
]);
}
See also
Plugin: the interface whoseregister()receives this contextPluginManager: constructs the context and passes it inFrontendNavRegistry/NavItem: whatcontributeFrontendNav()ends up touchingModule/MenuItem: the editor surfaceregisterEditorModule()/addEditorMenuItem()hook into- Build a Module: Skeleton:
the no-op
register()you start from - Concept: PluginContext: design rationale (why a per-plugin context, why a registration side-channel)