Purpose

The contract every plugin satisfies. A class that implements this interface, pointed at from composer.json (extra.scriptor.plugin), is what the PluginManager instantiates and runs at boot. Three methods, all required: a name, a version, and the register() method where the framework hands control to plugin code.

register() is the only sanctioned boot-time entry point. Anything the plugin wants to influence (DI bindings, event subscriptions, editor modules, menu items, frontend nav) goes through the PluginContext argument. Plugins do not reach into framework internals directly.

FQCN + file path

When to use

You implement Plugin exactly once per plugin, in the class your composer.json points at via extra.scriptor.plugin. Outside of that single implementation, you never touch this interface.

The class lives in the plugin's src/ (whatever your PSR-4 autoload maps to), is the FQCN you put under extra.scriptor.plugin, and is what gets instantiated once per request by the PluginManager.

Surface

public function name(): string;

Human-readable plugin identifier. Surfaced in logs (e.g. [my-plugin] register failed: ...) and in the editor's InstalledPlugins module. Convention: same string as the composer package's name field (bigins/scriptor-markdown-pages) or the short slug (markdown-pages). Both are fine; pick something operators will recognise in a log line.

public function version(): string;

Semantic version of the plugin. Informational only. The loader does not enforce constraints, does not refuse to load a plugin on version mismatch, does not compare against anything. The string surfaces in the editor's InstalledPlugins module so operators see what they have installed.

Keep this in sync with your git tag (v0.1.0 tag ↔ '0.1.0' return value). Drift here is cosmetic but distracting.

public function register(PluginContext $context): void;

The one place the framework runs plugin code at boot. Called once per request, after the iManager container is wired and before any routing or rendering. Inside this method you:

  • Subscribe to frontend events ($context->subscribe(...))
  • Register editor modules ($context->registerEditorModule(...))
  • Add editor menu items ($context->addEditorMenuItem(...))
  • Contribute frontend nav builders ($context->contributeFrontendNav(...))
  • Bind your own services into the DI container ($context->container()->add(...))

What you do not do inside register(): hit the database, read files, make HTTP calls, or anything else with side effects. This method runs on every request; expensive work inside it taxes every page load. Defer work to the handlers / factories you register, not the registration itself.

Lifecycle

PluginManager discovers your plugin by scanning Composer's vendor/composer/installed.json for entries with "type": "scriptor-plugin", reads extra.scriptor.plugin to find the FQCN, instantiates it (no-arg constructor by default), and calls register($context). One instance per request.

If register() raises, the manager logs the error and continues with the remaining plugins; one bad plugin does not bring the whole site down. Your plugin still appears in the InstalledPlugins module, with the error visible in the log line.

The plugin instance is not retained after register() returns; only the side effects on the context (subscriptions, module factories, menu items, nav builders) survive. Plugin instance state therefore lasts only as long as register() is on the call stack; persistent state belongs in services bound into the container.

Common patterns

Minimal no-op plugin

namespace Acme\HelloScriptor;

use Scriptor\Boot\Plugin\Plugin;
use Scriptor\Boot\Plugin\PluginContext;

final class HelloPlugin implements Plugin
{
    public function name(): string    { return 'acme/hello-scriptor'; }
    public function version(): string { return '0.1.0'; }

    public function register(PluginContext $context): void
    {
        // Nothing yet. The plugin still appears in the editor's
        // InstalledPlugins list, useful for verifying discovery.
    }
}

Subscribing to a frontend event

public function register(PluginContext $context): void
{
    $context->subscribe(
        \Scriptor\Boot\Events\Frontend\PageResolved::class,
        function (\Scriptor\Boot\Events\Frontend\PageResolved $event): void {
            // Inspect $event->page, record a hit, prime a cache, ...
        },
    );
}

Binding your own service into the container

public function register(PluginContext $context): void
{
    $context->container()->add(
        \Acme\HelloScriptor\Greeter::class,
        static fn() => new \Acme\HelloScriptor\Greeter('Hello, Scriptor!'),
    );
}

See also