Purpose
The contract for plugins that own database schema. A plain
Plugin runs register() per request and nothing
else, which suits stateless plugins (event subscribers, editor
modules, nav builders). A LifecyclePlugin adds install() and
uninstall() for one-shot work that must run exactly once on
install and reverse on removal: declaring iManager category
fields, seeding default rows, dropping schema entries.
The framework never calls these hooks itself. The operator drives
them through bin/scriptor plugin:install and
bin/scriptor plugin:uninstall, and Scriptor records installed
state in data/plugin-states.json. Added in Scriptor 2.1.0.
FQCN + file path
- FQCN:
Scriptor\Boot\Plugin\LifecyclePlugin - Source:
boot/Plugin/LifecyclePlugin.php
When to use
Implement LifecyclePlugin (instead of plain Plugin) only when
the plugin needs real iManager fields: keys that must be queryable,
FTS-indexed, or visible in the editor's field filter. Those fields
have to be created on install and removed on uninstall.
When per-page values can live as JSON-blob keys in the item's
data bag (the common case), a plain Plugin is enough and you do
not need this interface at all. The trade-off between the two is
covered in the cookbook recipe linked below.
Surface
LifecyclePlugin extends Plugin, so it also requires name(),
version(), and register() (see Plugin). It adds
two methods:
public function install(PluginContext $context): void
One-shot setup. Invoked by bin/scriptor plugin:install <package>.
Should be idempotent: use FieldRepository->ensure() and
CategoryRepository->ensure() so a re-run (plugin:install --force)
adds new schema without disturbing existing entries.
public function uninstall(PluginContext $context): void
The inverse. Invoked by bin/scriptor plugin:uninstall <package>.
Convention: remove the schema entries install() created but leave
row values in items.data alone, so a later reinstall finds them.
Read $context->purgeDataRequested (set by the CLI's
--purge-data flag) to decide whether to also strip those values.
Tolerate already-missing entries; a field already gone is a
successful no-op.
Lifecycle
register() runs on every request, the same as for a plain
Plugin. install() and uninstall() run only when the operator
invokes the CLI, never automatically. So a freshly
composer required lifecycle plugin is discovered and its
register() fires, but its fields do not exist until
plugin:install runs. Document that step in the plugin's README.
install() runs after the iManager container is up, in isolation.
It must not subscribe to events or register modules or menu items
(those belong in register(), which runs per request); a
subscription made in install() would never fire afterward. On
success the CLI records the package in data/plugin-states.json;
if install() throws, the state file is left unchanged so the
operator can fix and retry. An operator who runs composer remove
without plugin:uninstall first leaves an orphan state entry,
cleared by bin/scriptor plugin:cleanup-orphan.
Common patterns
Register and remove category fields
use Imanager\Domain\Field;
use Imanager\Storage\CategoryRepository;
use Imanager\Storage\FieldRepository;
use Scriptor\Boot\Plugin\LifecyclePlugin;
use Scriptor\Boot\Plugin\PluginContext;
final class Plugin implements LifecyclePlugin
{
public function name(): string { return 'acme/products'; }
public function version(): string { return '1.0.0'; }
public function register(PluginContext $context): void
{
// per-request wiring only (form events, nav, ...)
}
public function install(PluginContext $context): void
{
$cats = $context->container()->get(CategoryRepository::class);
$fields = $context->container()->get(FieldRepository::class);
$pagesId = $cats->findBySlug('pages')->id;
$fields->ensure(Field::text($pagesId, 'price', 'Price')->position(20));
$fields->ensure(Field::text($pagesId, 'deposit', 'Deposit')->position(21));
}
public function uninstall(PluginContext $context): void
{
$cats = $context->container()->get(CategoryRepository::class);
$fields = $context->container()->get(FieldRepository::class);
$pagesId = $cats->findBySlug('pages')->id;
foreach (['price', 'deposit'] as $name) {
$field = $fields->findByName($pagesId, $name);
if ($field !== null && $field->id !== null) {
$fields->delete($field->id);
}
}
}
}
The operator workflow
composer require acme/products
bin/scriptor plugin:install acme/products # runs install()
bin/scriptor plugin:uninstall acme/products # runs uninstall()
composer remove acme/products
See also
Plugin: the base interface this one extends; read it firstPluginContext: the handle both hooks receive, exposingcontainer()and thepurgeDataRequestedflagPluginManager: discovery and boot; the CLI commands build on it for install-state trackingPageSaving: where the real fields get their values, the same as for JSON-blob storage- Cookbook: Register schema with a LifecyclePlugin: the worked recipe, including the JSON-blob-versus-real-fields decision
- Concept: Plugin discovery: how Scriptor finds plugins and where install state lives