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
- FQCN:
Scriptor\Boot\Plugin\Plugin - Source:
boot/Plugin/Plugin.php
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
PluginContext: every registration method you call fromregister()PluginManager: how Scriptor discovers and instantiates yourPluginimplementationPluginManifest: the composer metadata the manager parses to find yourPluginFQCN- Build a Module: Skeleton: walks the no-op plugin from chapter 2 against a real Scriptor
- Concept: Plugin Discovery: the rules the manager applies to your composer.json