Model Owner Plugin Manager¶
The Model Owner plugin manager discovers and manages plugins that own the configuration entities being modeled. A Model Owner defines what components exist, how they map to Drupal plugins, and how models are stored and manipulated.
Plugin manager details¶
| Property | Value |
|---|---|
| Service ID | plugin.manager.modeler_api.model_owner |
| Class | Drupal\modeler_api\Plugin\ModelOwnerPluginManager |
| Discovery | PHP attribute (#[ModelOwner]) |
| Plugin namespace | Plugin\ModelerApiModelOwner |
| Interface | Drupal\modeler_api\Plugin\ModelerApiModelOwner\ModelOwnerInterface |
| Base class | Drupal\modeler_api\Plugin\ModelerApiModelOwner\ModelOwnerBase |
| Attribute | Drupal\modeler_api\Attribute\ModelOwner |
| Alter hook | hook_modeler_api_model_owner_info_alter() |
| Cache tag | modeler_api_model_owner_plugins |
Attribute definition¶
The #[ModelOwner] attribute accepts the following parameters:
#[ModelOwner(
id: "my_owner",
label: new TranslatableMarkup("My Owner"),
description: new TranslatableMarkup("Manages my config entities."),
uiLabelNewModel: new TranslatableMarkup("Add new model"),
uiLabelNewModelWithModeler: new TranslatableMarkup("Add new model with modeler"),
)]
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique plugin ID |
label |
TranslatableMarkup |
No | Human-readable name |
description |
TranslatableMarkup |
No | Brief description |
uiLabelNewModel |
TranslatableMarkup |
No | Label for the "Add" button |
uiLabelNewModelWithModeler |
TranslatableMarkup |
No | Label for "Add with modeler" button |
ModelOwnerInterface¶
The interface defines the full contract a Model Owner must fulfill. Methods are grouped by responsibility:
Identity and entity mapping¶
| Method | Return | Description |
|---|---|---|
label() |
string |
Plugin label |
description() |
string |
Plugin description |
componentLabels() |
array |
Human-readable singular labels for supported component types (e.g. ['start' => 'Event', 'element' => 'Action']) |
componentLabelsPlural() |
array |
Human-readable plural labels for supported component types (e.g. ['start' => 'Events', 'element' => 'Actions']). Used for grouping headings in the component panel, quick-add popups, and constraint validation error messages. |
modelIdExistsCallback() |
array |
Callback to check if a model ID already exists (e.g. [MyEntity::class, 'load']) |
configEntityProviderId() |
string |
Module name providing the config entity type |
configEntityTypeId() |
string |
Config entity type ID (e.g. eca) |
configEntityBasePath() |
?string |
Admin base path without leading/trailing slash, or NULL if routing is self-managed |
Settings and form customization¶
| Method | Return | Description |
|---|---|---|
settingsForm() |
?string |
FQCN of a settings form class, or NULL |
modelConfigFormAlter(array &$form) |
void |
Alter the default model metadata config form |
Model lifecycle¶
| Method | Return | Description |
|---|---|---|
isEditable($model) |
bool |
Whether the model can be edited |
isExportable($model) |
bool |
Whether the model can be exported |
enable($model) |
void |
Enable the model (final in base) |
disable($model) |
void |
Disable the model (final in base) |
clone($model) |
ConfigEntityInterface |
Clone the model (final in base) |
export($model) |
Response |
Export as .tar.gz archive (final in base) |
Metadata accessors¶
All metadata methods follow a get/set pattern and are declared final in
the base class. They store values in the config entity's third-party settings
under the modeler_api namespace.
| Property | Getter | Setter |
|---|---|---|
| Label | getLabel($model) |
setLabel($model, $label) |
| Status | getStatus($model) |
setStatus($model, $status) |
| Version | getVersion($model) |
setVersion($model, $version) |
| Template | getTemplate($model) |
setTemplate($model, $template) |
| Storage | getStorage($model) |
setStorage($model, $storage) |
| Documentation | getDocumentation($model) |
setDocumentation($model, $doc) |
| Tags | getTags($model) |
setTags($model, $tags) |
| Changelog | getChangelog($model) |
setChangelog($model, $log) |
| Annotations | getAnnotations($model) |
setAnnotations($model, $annotations) |
| Colors | getColors($model) |
setColors($model, $colors) |
| Swimlanes | getSwimlanes($model) |
setSwimlanes($model, $swimlanes) |
| Model data | getModelData($model) |
setModelData($model, $data) |
| Modeler ID | getModelerId($model) |
setModelerId($model, $id) |
Component management¶
These methods define how the Model Owner maps its domain concepts to the generic component type system.
| Method | Return | Description |
|---|---|---|
supportedOwnerComponentTypes() |
array |
Map of COMPONENT_TYPE_* constants to domain-specific names |
modelConstraints() |
array |
Cardinality constraints for component types and their successors. See Model constraints below. |
availableOwnerComponents($type) |
PluginInspectionInterface[] |
All available plugins for a component type |
favoriteOwnerComponents() |
array |
Preferred plugin IDs grouped by type |
componentLabels() |
array |
Human-readable labels for supported component types (e.g. ['start' => 'Event', 'element' => 'Action']) |
ownerComponentId($type) |
string |
Generate a new component ID for a type |
ownerComponentDefaultConfig($type, $id) |
array |
Default configuration for a component |
ownerComponent($type, $id, $config) |
?PluginInspectionInterface |
Instantiate a component plugin |
ownerComponentEditable($plugin) |
bool |
Whether a component's config can be edited in the UI |
ownerComponentPluginChangeable($plugin) |
bool |
Whether a component's plugin type can be swapped |
buildConfigurationForm($plugin, $modelId, $modelIsNew) |
array |
Build the config form for a component plugin |
skipConfigurationValidation($type, $id) |
bool |
Skip validation for specific components |
Save cycle methods¶
These are called by the Api service during model save:
| Method | Return | Description |
|---|---|---|
usedComponents($model) |
Component[] |
Return all components currently in the model (implement this) |
getUsedComponents($model) |
Component[] |
Public accessor that also adds swimlane components (final, call this) |
resetComponents($model) |
$this |
Clear all components before re-adding from raw data |
addComponent($model, $component) |
bool |
Add a single component to the model |
finalizeAddingComponents($model) |
void |
Called after all components are added successfully |
updateComponent($model, $component) |
bool |
Update a single existing component |
usedComponentsInfo($model) |
string[] |
Summary strings about used components |
Storage¶
| Method | Return | Description |
|---|---|---|
storageMethod($model) |
?string |
Resolved storage method for this model |
storageId($model) |
string |
Unique ID for separate storage entities |
defaultStorageMethod() |
string |
Default storage method for this owner |
enforceDefaultStorageMethod() |
bool |
Whether the user can change the storage method |
Template support¶
| Method | Return | Default | Description |
|---|---|---|---|
supportsTemplate() |
bool |
auto-detected | Whether the entity type has a template key |
applyTemplate($templateId, $componentId, $target, $hiddenConfig, $config) |
void |
no-op | Apply a template to a model, creating a new model if it does not exist |
The applyTemplate() method is called by the Templates controller when a
user applies a template token. It receives the template model ID, the component
ID within that template, the CSS selector target, and optional hidden/visible
configuration values.
Optional features¶
| Method | Return | Default | Description |
|---|---|---|---|
supportsStatus() |
bool |
auto-detected | Whether the entity type has a status key |
supportsReplayData() |
bool |
false |
Whether replay/debug data is available |
getReplayData($hash) |
array |
[] |
Replay data for a hash |
getReplayDataByComponent($modelId, $componentId) |
array |
[] |
Replay data for a specific component |
supportsTesting() |
bool |
false |
Whether in-modeler testing is supported |
startTestJob($modelId, $componentId) |
string\|TranslatableMarkup |
error message | Start a test job |
pollTestJob($jobId) |
array\|null\|TranslatableMarkup |
error message | Poll test job status |
Documentation support¶
| Method | Return | Description |
|---|---|---|
docBaseUrl() |
?string |
Base URL for external documentation |
pluginDocUrl($plugin, $pluginType) |
?string |
URL for a specific plugin's documentation |
prepareFormFieldForValidation(&$value, &$replacement, $element) |
?string |
Pre-validation hook for form fields |
Model constraints¶
Model Owners can declare cardinality constraints on component types via
modelConstraints(). These constraints control both the number of components
of a given type allowed in a model and the number of outgoing connections
(successors) each component of that type may have.
Constraints are enforced in two places:
- Server-side -- during the save cycle, the
Apiservice callsvalidateModelConstraints()and adds error messages for any violations. - Client-side -- constraints are delivered to the frontend via
drupalSettings.modeler_api.model_constraintsso that the modeler UI can prevent violations before the save request is sent.
Return format¶
modelConstraints() returns an associative array keyed by COMPONENT_TYPE_*
constants. Each value is an array with optional min and max keys for the
component count, and an optional successors key with its own min/max for
the number of outgoing connections per component of that type.
public function modelConstraints(): array {
return [
Api::COMPONENT_TYPE_START => [
'min' => 1,
'max' => 1,
'successors' => ['min' => 1, 'max' => 1],
],
Api::COMPONENT_TYPE_ELEMENT => [
'min' => 1,
'successors' => ['max' => 0],
],
Api::COMPONENT_TYPE_GATEWAY => [
'successors' => ['min' => 2],
],
];
}
Constraint keys¶
| Key | Type | Description |
|---|---|---|
min |
int |
Minimum number of components of this type required in the model |
max |
int |
Maximum number of components of this type allowed in the model |
successors |
array |
Nested constraint for outgoing connections per component |
successors.min |
int |
Minimum number of successors each component of this type must have |
successors.max |
int |
Maximum number of successors each component of this type may have (0 = no successors allowed) |
successors.requireConditionWhenParallel |
bool |
Opt-in. When TRUE, any group of two or more successors of the same component that share the same target must each carry a non-empty conditionId. Defaults to FALSE. |
All keys are optional. Omitting a key means "no constraint" for that dimension.
Returning an empty array (the default in ModelOwnerBase) means no constraints
at all.
Parallel successors with conditions (opt-in)¶
By default, the modeler API permits multiple successors between the same
source and target component. Each successor carries an optional conditionId
(see Drupal\modeler_api\ComponentSuccessor). For some model owners --
notably ECA -- multiple parallel successors only make sense when every one of
them carries a condition. Without conditions on all of them, the runtime
semantics are degenerate (the same target would be invoked multiple times
unconditionally) and the model is structurally meaningless.
To enable this rule for a given component type, set
successors.requireConditionWhenParallel to TRUE:
public function modelConstraints(): array {
return [
Api::COMPONENT_TYPE_ELEMENT => [
'successors' => [
'max' => 10,
'requireConditionWhenParallel' => TRUE,
],
],
];
}
When the flag is set, Api::validateModelConstraints() groups each
component's successors by target ID and requires every group of size > 1
to have a non-empty conditionId on every member. Other model owners are
unaffected -- the default value is FALSE, so existing behavior does not
change.
The flag is forwarded to the frontend as part of the model_constraints
settings payload, so the modeler UI can mirror the check in
validateBeforeSave() for fast pre-save feedback.
Validation error messages¶
The Api service generates human-readable, translatable error messages for
constraint violations. The messages use singular labels from
componentLabels() and plural labels from componentLabelsPlural() depending
on the constraint value:
"A model requires at least one @label."(whenminis 1)"A model requires at least @min @label_plural."(whenmin> 1)"A model allows at most one @label."(whenmaxis 1)"A model allows at most @max @label_plural."(whenmax> 1)"@label "@name" requires at least @min successor(s)."(successor min)"@label "@name" must not have any successors."(when successormaxis 0)"@label "@name" allows at most @max successor(s)."(successor max > 0)'@label "@name" has parallel successors to "@target" without a condition on every edge. When multiple edges connect the same source and target, each edge must carry a condition.'(parallel successors opt-in)
ComponentWrapperPlugin¶
When a Model Owner's components are not Drupal plugins themselves (e.g. AI
Agent sub-agents), they can be wrapped using ComponentWrapperPlugin:
use Drupal\modeler_api\Api;
use Drupal\modeler_api\Plugin\ComponentWrapperPlugin;
$wrapped = new ComponentWrapperPlugin(
type: Api::COMPONENT_TYPE_SUBPROCESS,
id: 'my_subprocess_id',
configuration: ['key' => 'value'],
label: 'My Subprocess',
);
This implements ComponentWrapperPluginInterface (which extends
ConfigurableInterface) and provides a getType() method.
Existing implementations¶
AI Agents (ai_agents_agent)¶
The ai_agents module implements a Model Owner for AI agent configuration:
- Config entity type:
ai_agent - Supported types:
START(agent metadata),SUBPROCESS(sub-agents),ELEMENT(function call tools),LINK(connections) - Storage:
STORAGE_OPTION_NONE(enforced -- agents don't store raw data) - Uses
ComponentWrapperPluginfor sub-agents and start components - Function call tools are actual Drupal plugins from the AI module
ECA (eca)¶
The eca module (via eca_ui submodule) provides a Model Owner for ECA
workflow configuration:
- Config entity type:
eca - Supported types:
START(events),ELEMENT(actions),LINK(conditions),GATEWAY(gateways) - Supports status, templates, replay data, and testing