Purpose

The data shape a plugin uses to add a link to the editor's chrome. Whether the link lands in the left-rail sidebar, the top-right profile cluster, or a plugin-introduced slot of its own is decided by displayType.

Final readonly DTO. You construct one per entry inside Plugin::register() and hand it to PluginContext::addEditorMenuItem(). The editor's templates walk the registry, group entries by displayType, sort by position, render the markup.

FQCN + file path

When to use

You construct MenuItem instances inside Plugin::register() and pass them to PluginContext::addEditorMenuItem(). You do not receive MenuItem from any other API surface; the registry is the consumer.

A MenuItem is independent of the corresponding Module. You typically register both side by side (so the link does something when clicked), but the framework does not enforce the pairing.

Surface

Constructor

public function __construct(
    public string $slug,
    public string $label,
    public string $icon = '',
    public string $displayType = 'sidebar',
    public int $position = 0,
    public ?string $href = null,
)

Six public readonly fields:

Field Type Purpose
slug string URL slug. When href is null, the layout builds $siteUrl . '/' . $slug . '/'; also the convention key for matching against the current Editor::$urlSegments to mark the item active
label string Displayed text. Plain text; the renderer escapes it
icon string Icon class name (theme-defined; the bundled editor uses Material-Icons-style strings)
displayType string Layout slot: 'sidebar' (left rail), 'profile' (top-right cluster), or a plugin-introduced slot. Default 'sidebar'
position int Sort key inside its slot. Lower wins. Ties broken by registration order. Default 0
href ?string Optional literal URL. When null, derived from slug. Pass a literal when you need a suffix (e.g. logout with ?tokenName=…&tokenValue=…)

No methods.

Lifecycle

Constructed by your plugin, handed to PluginContext::addEditorMenuItem($item). The context records it on its own registration snapshot and forwards to MenuRegistry::add(). The editor's template fragments walk the registry per request to render the chrome.

final readonly, so once constructed the entry is frozen. Re-registering an item with the same slug does not replace; the registry just appends. Plugins should ensure unique slugs.

Common patterns

$context->addEditorMenuItem(new \Scriptor\Boot\Editor\Menu\MenuItem(
    slug:        'blog',
    label:       'Blog',
    icon:        'article',
    displayType: 'sidebar',
    position:    50,
));

Renders in the sidebar. Clicks land at /editor/blog/, matching the 'blog' slug a sibling registerEditorModule('blog', …) claims.

Profile-cluster entry with a literal href

$context->addEditorMenuItem(new MenuItem(
    slug:        'docs',
    label:       'Docs',
    icon:        'help_outline',
    displayType: 'profile',
    position:    10,
    href:        'https://scriptor-cms.dev/developer-guide/',
));

Links to an external URL. The slug field is still required (it is used for the DOM id and the registry key) but not for URL building, since href overrides.

Introducing your own slot

$context->addEditorMenuItem(new MenuItem(
    slug:        'gallery-quick',
    label:       'New gallery',
    icon:        'add_photo_alternate',
    displayType: 'page-actions',     // plugin-introduced slot
    position:    10,
));

By itself, this entry never renders: no template reads 'page-actions'. The plugin pairs it with a small template fragment its own module includes, which iterates MenuRegistry::displayItems('page-actions') and emits the markup. The framework is content-agnostic; it just stores entries by displayType and hands them back on demand.

See also

  • Module: the corresponding URL handler the menu entry's slug typically points at
  • Editor: holds the request and is what the module writes through
  • PluginContext: the addEditorMenuItem() entry point
  • Concept: Editor extensions: module + menu + container walkthrough