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

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