Services¶
| Service ID | Class | Role |
|---|---|---|
frontend_editing.field_reference |
FieldReferenceHelper |
Resolves field lineage and builds the action links. Aliased to FieldReferenceHelperInterface. |
frontend_editing.revision_helper |
RevisionHelper |
Prepares revisions made by frontend editing and checks the moderation transition. Aliased to RevisionHelperInterface. See Revision helper. |
frontend_editing.htmx_availability |
HtmxAvailability |
Detects htmx and wires elements to use it. Aliased to HtmxAvailabilityInterface. See AJAX transports. |
frontend_editing.form_builder |
FrontendEditingFormBuilder |
Builds the entity form the sidebar loads |
frontend_editing.toolbar_item |
ToolbarItem |
Renders the toolbar toggle, used when the floating button is off |
frontend_editing.route_subscriber |
RouteSubscriber |
Route alterations |
Drupal\frontend_editing\Hook\ViewAlter |
ViewAlter |
Injects the action wrappers into entity render arrays and dispatches the rendering pipeline events |
Drupal\frontend_editing\Hook\FrontendEditingHooks |
FrontendEditingHooks |
Toolbar, theme, AJAX and extra field hooks |
Drupal\frontend_editing\Hook\FormAlter |
FormAlter |
Form alterations, including the add-item submit path |
Drupal\frontend_editing\Hook\ThirdPartySettings |
ThirdPartySettings |
The field formatter third-party settings form and summary |
Field reference helper¶
FieldReferenceHelperInterface is the one to know. It answers where a referenced entity sits, and
builds the links that act on it.
Access¶
accessAdd($parent_type, $parent, $parent_field_name, $current_item = NULL, $delta = NULL)
accessAddType($bundle, $parent_type, $parent, $parent_field_name, $current_item = NULL, $delta = NULL)
accessMove($parent_type, $parent, $parent_field_name, $current_item, $operation, $delta = NULL)
Mutations¶
move($parent_type, $parent, $parent_field_name, $current_item, $operation, $delta = NULL)
addItem($parent_type, $parent, $parent_field_name, $current_item, $before, EntityInterface $new_entity, $delta = NULL)
addItem() is the shared path behind both ways of adding: choosing a type and creating a new
entity, and referencing an existing one. Both resolve the delta the same way before splicing.
Resolving a delta¶
resolveDelta(FieldableEntityInterface $parent_entity, $reference_field_name, $entity_id, $delta = NULL)
Reconciles the delta from the request with the host entity's current field values. The item at that
delta must exist and must still reference the entity the link named. On a mismatch it returns
NULL and the caller refuses the action. Omitting the delta falls back to the first item
referencing that entity, so callers written against the older signature keep working.
Every mutation and its access check goes through this. A new action route that targets a specific field item should too.
Building links¶
buildCommonActions(EntityInterface $entity, string $viewMode, bool $addItems, bool $deleteItems, bool $moveUpDown, bool $ajaxContentUpdate, bool $duplicateItems = FALSE): array
attachActionCallbacks(array &$item, string $viewMode, EntityInterface $entity, bool $addItems, bool $deleteItems, bool $moveUpDown, bool $ajaxContentUpdate, bool $duplicateItems = FALSE): void
attachActionCallbacks() pushes the action builders onto an item's #pre_render, after checking
the host entity's update access and that it is the latest revision. This is what a rendering
pipeline subscriber calls.
buildCommonActions() builds the elements directly instead, for callers with no #pre_render
chain to attach to.
Warning
buildCommonActions() returns ['common_actions' => [...], 'add_items' => [...]]. The two are
siblings, not nested. Reading only common_actions silently drops the add links.
Neither method attaches the frontend_editing/frontend_editing library. Only the extra field's own
render does. See Display Builder.
Revisions and content moderation¶
getRevisionRoot(EntityInterface $entity): EntityInterface
getRevisionQuery(EntityInterface $entity): array
canSaveChanges(EntityInterface $entity): bool
isLatestRevision(EntityInterface $entity): bool
getRevisionRoot() is the extension point for composite entities. Such entities are revisioned
with their host, so their own revisions say nothing about whether the content is pending. The base
implementation returns the entity itself. ParagraphsReferenceHelper follows the host revision the
paragraph is rendered from, then the active root parent. A sub-module for another composite entity
type overrides it. The main module knows nothing about composite entities itself.
isLatestRevision(), getRevisionQuery() and canSaveChanges() all work on the root:
isLatestRevision()is TRUE when the root is the latest revision. Actions are only attached then.getRevisionQuery()returns['default_revision' => 1]when the root is the default revision, and an empty array otherwise. The constant isFieldReferenceHelperInterface::DEFAULT_REVISION_QUERY. The move, delete and duplicate links add it to their query.FrontendEditingController::updateContent()redirects to the draft only when the parameter is present and the root has a pending revision.canSaveChanges()is FALSE when the root host cannot be saved as a draft by the current user. Plain entities are saved through their own form, which validates the transition itself, so they always return TRUE.
When the root differs from the entity, attachActionCallbacks() adds the url.path cache context,
because an unchanged paragraph is shared by the live page and the draft, whose links differ.
Preview¶
getPreviewEntity(EntityInterface $entity): ?EntityInterface
applyPreviewValue(array &$fieldBuild, string $fieldName, EntityInterface $entity, EntityInterface $previewEntity, string $viewMode): void
getPreviewEntity() answers whether a preview applies at all: the preview module installed, the
type and bundle enabled, and a pending tempstore entry for this entity. Resolve it once per render
and reuse it rather than asking per field.
applyPreviewValue() swaps one field's render array, guarded against re-entrant recursion, since
rendering a preview value can trigger further render alters.
Both check for the preview module internally. The base module has no hard dependency on it.
Revision helper¶
RevisionHelperInterface makes frontend editing changes safe for content moderation.
isModerated(ContentEntityInterface $entity): bool
prepareRevision(ContentEntityInterface $entity): void
setRevisionMetadata(ContentEntityInterface $entity): void
applyDraftModerationState(ContentEntityInterface $entity): void
canSaveAsDraft(ContentEntityInterface $entity, ?AccountInterface $account = NULL): bool
canPublishDirectly(ContentEntityInterface $entity, ?AccountInterface $account = NULL): bool
setPublishDirectly(bool $publish_directly): void
isPublishingDirectly(): bool
canSaveRevision(ContentEntityInterface $entity): bool
prepareRevision()sets the metadata and moves a published entity into the draft state. Call it on the active revision just before saving a new revision.canSaveAsDraft()checks the workflow transition with content moderation's own validation. Entities that are not moderated, or have no state yet, always pass.applyDraftModerationState()throwsAccessDeniedHttpExceptionwhen the user may not make the transition, because saving programmatically skips the validation the entity form does.
canPublishDirectly() backs the Save and publish button. It is TRUE only for the live revision of
published, moderated content with no pending revision, for an account with the
frontend editing publish directly permission and a valid transition from the published state to
itself. While setPublishDirectly(TRUE) is active, applyDraftModerationState() keeps the state
instead of moving to draft, and throws AccessDeniedHttpException when canPublishDirectly() fails.
canSaveRevision() is the check that matches the current mode, and the one the paragraphs
revisioner runs before saving anything. The flag lives on the shared service for the length of the
request, so the button's submit handlers turn it on and off around the save.
The target state is the workflow's default moderation state when it is unpublished, draft
otherwise. The log message is fixed. To change either, replace the service in your own module.
FieldReferenceHelper receives the helper through setRevisionHelper(), called from the service
definition, so the constructor signature stays stable for subclasses. A service definition that
omits the call still works: the protected getRevisionHelper() falls back to the container and
triggers a silent E_USER_DEPRECATED, so moderated content is never published by accident. Add the
call to your own definition ([setRevisionHelper, ['@frontend_editing.revision_helper']]).
Decorating the helper¶
frontend_editing_paragraphs decorates FieldReferenceHelper with ParagraphsReferenceHelper,
which extends the class and calls parent::__construct() positionally.
A new constructor parameter on the base class therefore needs three edits, not one: the decorator's
constructor signature, its parent::__construct() call in the same position, and the decorator's
own arguments list in frontend_editing_paragraphs.services.yml.
Missing one does not break drush cr, because the container still compiles. It throws a type error
the first time anything resolves frontend_editing.field_reference.