Purpose
The contract a plugin satisfies to mount its own admin surface
under /editor/<slug>/.... One method: execute(). The router
instantiates the module per request via its registered factory,
calls execute(), and trusts the module to write into the
editor's layout slots.
The interface is intentionally tiny. A module is a per-request
controller that reads $editor->input, writes
$editor->pageContent, and is done. Anything richer (sub-routes,
controllers, middleware) is the module's own internal structure.
FQCN + file path
- FQCN:
Scriptor\Boot\Editor\Module - Source:
boot/Editor/Module.php
When to use
You implement Module once per editor surface a plugin adds.
The class lives in your plugin's src/; you register its
factory (not the class) via
PluginContext::registerEditorModule($slug, $factory) from
inside Plugin::register().
Plugins usually pair a Module with a MenuItem so the new
slug also appears in the sidebar. The two are independent: a
module without a menu item is reachable by URL but invisible in
the chrome; a menu item without a module is a dead link.
Surface
interface Module
{
public function execute(): void;
}
One method, no return value. Side effects on the Editor
instance the module received (through its constructor or
factory) are the contract.
A module typically writes to:
$editor->pageTitle:<title>and H1 text.$editor->pageContent: the rendered HTML body.$editor->breadcrumbs: the breadcrumb HTML.$editor->addMsg()/flashMsg(): message queue.$editor->redirect(): when a POST succeeded.$editor->addResource(): module-specific CSS / JS.
Lifecycle
PluginContext::registerEditorModule($slug, $factory) records
the factory in ModuleRegistry. On every request,
EditorRouter parses the URL, looks up the slug, calls the
factory once the auth gate has passed, and dispatches
execute().
The factory signature:
function(Container $container, Editor $editor): Module
The router constructs a fresh module per request. Anything you need to persist across requests goes through a service bound into the container (and read from there in the factory), not through a static property on the module.
Re-registering an existing slug replaces the previous
factory. Last writer wins. In practice plugins use unique slugs
(bigins/blog registers 'blog').
Common patterns
Minimal module + factory + plugin registration
namespace Acme\Hello\Editor;
use League\Container\Container;
use Scriptor\Boot\Editor\Editor;
use Scriptor\Boot\Editor\Module;
final class HelloModule implements Module
{
public function __construct(
private Container $container,
private Editor $editor,
) {}
public function execute(): void
{
$this->editor->pageTitle = 'Hello';
$this->editor->pageContent = '<p>Hello from the Hello plugin.</p>';
}
}
// In your Plugin::register():
$context->registerEditorModule(
'hello',
static fn(Container $c, Editor $editor) => new HelloModule($c, $editor),
);
Module with a POST handler + flash + redirect
public function execute(): void
{
$this->editor->pageTitle = 'Settings';
if ($this->editor->input->method() === 'POST') {
$name = $this->editor->sanitizer->text(
$this->editor->input->post('name', ''),
);
if ($name === '') {
$this->editor->addMsg('error', 'Name is required.');
} else {
$this->settings->save('name', $name);
$this->editor->flashMsg('success', 'Settings saved.');
$this->editor->redirect($this->editor->siteUrl . '/settings/', 303);
}
}
$this->editor->pageContent = $this->renderForm();
}
Sub-routes inside one module
The framework only routes the first segment to a module. Deeper
paths land in $editor->urlSegments and the module dispatches
them itself:
public function execute(): void
{
$action = $this->editor->urlSegments->at(1) ?? 'index';
match ($action) {
'index' => $this->renderIndex(),
'edit' => $this->renderEdit(),
'delete' => $this->handleDelete(),
default => $this->editor->redirect($this->editor->siteUrl . '/my-module/', 303),
};
}
See also
Editor: the instance every module receives and writes throughMenuItem: how to pin a sidebar link to a module's slugPluginContext: theregisterEditorModule()entry point and signature- Concept: Editor extensions: module + menu + container walkthrough