Validate inline block field values before saving
# Problem/Motivation
Follow-up to #3.
Inline blocks created or updated by the tools are saved without any validation. Field values supplied by the caller are passed straight to `$block->set()` and then `$block->save()`:
- `LayoutBuilderHelper::createInlineBlockComponent()`, used by `set_entity_layout` and by the `add_block` operation of `edit_entity_layout`.
- The `update_block` operation in `EditEntityLayout::doExecute()`.
The host entity is then saved with its new `layout_builder__layout` value, also without validation.
The callers are typically AI agents, so invalid input is expected, not exceptional. Because nothing is validated, invalid values are either stored silently or only fail at the storage layer with an unhelpful exception, and the agent gets no actionable feedback to correct its input.
The Tool API does not cover this: `TypedInputsTrait::validateInputValues()` validates the declared inputs (here: a list of sections or operations), not the field values nested inside them. Elsewhere in the ecosystem, `tool_belt:field_set_value` validates the field it has just set with `$entity->get($field_name)->validate()` and returns `ExecutableResult::failure()` listing the violations. This module should follow the same pattern.
Text formats are a specific case, and are not covered by field validation alone. Core enforces a field's `allowed_formats` setting only in the `text_format` form element, not through a validation constraint. The `format` property's `AllowedValues` constraint only checks that the format exists and that the current user may use it. Tested on Drupal 11.4.6, for a `text_long` field restricted to `basic_html`:
| `format` value | Violations from validation |
|----------------|----------------------------|
| `does_not_exist` | 1 (`The value you selected is not a valid choice.`) |
| `plain_text` | 0 |
| `webform_default` | 0 |
So even with validation, a format that the field does not allow is accepted, while #3 now tells the caller which formats are allowed.
Required fields are not enforced either. Validating only the fields the caller supplied (as `field_set_value` does) does not catch a required field that was left out, so new blocks can be saved without values for required fields.
There is also a consistency problem. Blocks are saved one by one while the layout is being built, and the host entity is saved last. If a later block or the host entity fails, the blocks saved before it remain as unused non-reusable `block_content` entities.
# Steps to reproduce
1. Take a block type with a `text_long` field whose allowed formats are restricted to `basic_html`.
2. Call `set_entity_layout` with a block of that type and `{"value": "<p>Test</p>", "format": "full_html"}` for that field (or any other format the field does not allow).
3. The layout is saved successfully, and the block is stored with a format the field does not allow.
Similarly, a non-existent format such as `does_not_exist` is stored as-is, and a block of a type with a required field is saved when that field is omitted.
# Proposed resolution
For inline blocks, after setting field values and before saving:
- Validate each field the caller supplied with `$block->get($field_name)->validate()`, following `tool_belt:field_set_value`. This applies to new blocks and to `update_block`.
- For formatted text fields (`text`, `text_long`, `text_with_summary`), check that each item's format is in the field's `allowed_formats` when that setting is not empty. Ideally this reuses the logic of the `format` / `allowed_formats` discovery added in #3, so discovery and validation cannot disagree.
- For new blocks, check that every required field exposed by `get_layout_builder_blocks` has a non-empty value. For `update_block`, reject emptying a required field (covered by the field validation above for supplied fields).
For the host entity:
- Validate only the field this module changes, `$entity->get('layout_builder__layout')->validate()`, rather than the whole entity. This catches Layout Builder's own constraints without rejecting a layout change because of an unrelated invalid field on the host entity.
In general:
- Validate all blocks of the request, and the layout field, before saving anything, so that invalid input does not leave orphaned `block_content` entities behind.
- Do not save inline blocks directly. Store the validated, unsaved block in the component's `block_serialized` configuration, the same way the Layout Builder UI does. Layout Builder's `InlineBlockEntityOperations::handlePreSave()` then saves the blocks when the host entity is saved, inside the host entity's save transaction. This also means:
- usage is recorded in `inline_block_usage`, which the direct saves skipped (on a test site, 132 of 240 non-reusable blocks had no usage record), so unused blocks are cleaned up and access dependencies resolve;
- `update_block` creates a new block revision and updates the component's `block_revision_id`, instead of overwriting the revision that earlier host entity revisions still reference.
- On failure, return `ExecutableResult::failure()` listing each violation with the block position (section index, block index), field property path and message, so the caller can correct its input and retry.
- As a small related fix, set the `info` base field on new inline blocks, as core's `InlineBlock` plugin does (from the block label, falling back to the block type label). It is required but currently never set (224 of 240 non-reusable blocks on the same test site had no `info`).
# Remaining tasks
- Implement and review.
- Add kernel test coverage for: a non-existent format, a format not allowed by the field, a missing required field on a new block, an invalid value on `update_block`, and no orphaned blocks being saved when one block in the request is invalid.
# User interface changes
None.
# API changes
Tool calls with invalid block field values, or missing required fields, that previously succeeded now fail with a message listing the violations. Callers relying on invalid values being stored need to send valid values.
`LayoutBuilderHelper::createInlineBlockComponent()` keeps its signature, but no longer saves the block: the component holds it in `block_serialized` until the host entity is saved, and it throws an `InlineBlockValidationException` when the values do not validate. `LayoutBuilderHelper::buildLayout()` throws the same exception, with the violations of all blocks. New public methods: `updateInlineBlockComponent()` and `validateLayout()`.
# Data model changes
None.
---
AI Assisted Issue: this issue was generated with AI assistance, but was reviewed and refined by the creator.
issue
GitLab AI Context
Project: project/tool_layout_builder
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/tool_layout_builder/-/raw/1.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/tool_layout_builder
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD