Detect a changed field value¶
Version v1
You can apply this model as a recipe (Drupal 10.3 or later) to your own Drupal site:
1 2 3 4 5 6 7 8 9 10 11 | |
Purpose¶
Drupal has no token for the previous value of a field. The three spellings people reach for first — [user:unchanged:field_user_networks], [user-unchanged:field_user_networks] and [user:unchanged:field_user_networks:unchanged] — were all guessed in issue #3317070, and none of them resolves to anything. There is no unchanged token type, and the token system has no route of its own to the version of an entity that was in storage before the save.
That absence is easy to mistake for "detecting a change is hard in ECA". It is not. ECA ships a condition that answers the question directly, and this model uses it on the title field of an Article node.
Two different questions¶
Keeping them apart is the point of this model.
Did this field change? is answered by Entity: field value changed. The condition takes a field name and nothing else. It loads no entity, sets no token, and needs no preparation anywhere else in the model.
What was the previous value? is a different question, and only that one needs the original entity in hand.
A model that has to react to a change and no more should stop after the first question. Loading the original costs a storage read and an extra component, and buys nothing unless the old value is actually used. This model asks both questions, because its message quotes the old title, so the cheap check and the step it does not require are visible side by side.
The token that does exist¶
A token for a pre-save entity exists only once a model creates it. Entity: load with "Load unchanged values" enabled writes the original into a token whose name you choose, and from that point [<your_name>:<field>] resolves like any other entity token. That is what [original_article:title] is in this model.
The guesses in the issue fail for a different reason than "not implemented". user and node are token types Drupal registers; unchanged is not, and no amount of nesting turns it into one. Applied to the original report, the working equivalent of [user:unchanged:field_user_networks] is an Entity: load action storing the original under, say, unchanged_user, followed by [unchanged_user:field_user_networks].
Entity: get field value is the alternative when the old value has to be processed rather than printed. It copies one field off any entity token into a token of its own, which is what a following list, loop or comparison step needs. Detect user role changes uses it exactly that way.
How it works¶
The model reacts to Update content entity, restricted to node article.
The event has one outgoing sequence flow, and that flow carries the condition "Entity: field value changed" with the field name title. When the title is unchanged the flow does not open and nothing else in the model runs. That one line of configuration is the whole of the change detection.
When the title did change, two actions run:
- "Load the original article" — Entity: load with
unchangedenabled, storing the pre-save article in the tokenoriginal_article. - "Report the change" — Display a message to the user, rendering
Title changed from "[original_article:title]" to "[entity:title]".
[entity:title] is the current value and comes from the event itself, so nothing has to be prepared for it. See Tokens for how a model's own tokens sit alongside the ones an event provides.
Where the unchanged entity comes from¶
"Load unchanged values" does not mean "read the database again". ECA asks the entity for its original first — the copy Drupal attaches to an entity for the duration of a save — and only falls back to a fresh loadUnchanged() read when no original is attached and the entity is not new.
That order matters on the Update event. Update fires after the row has been written, so a fresh read would hand back the new values and the message would report the same title twice. The original is still attached at that point, so ECA returns the pre-save version.
ECA reads it through EntityInterface::getOriginal(). Drupal core deprecated the public $entity->original property in favor of that method, and ECA routes every original lookup through one helper, so the condition and the load action follow core's supported access path together.
Which event to use¶
The condition compares the entity against its original, so it needs an event where an original exists.
On Presave content entity the original is attached and the entity has not been written yet. That is the event to pick when the model reacts by changing the entity itself. On Update the original is still attached and the write has already happened, which is the event to pick for notifications and follow-up work, as here.
On Insert there is no original at all, so the condition is FALSE for every field. That is correct rather than surprising: nothing changed, because nothing was there before.
Testing this needs two saves, not one. Create the article and save it, then change the title and save again. A single save of a new article proves nothing. See Testing a transition needs two saves, and Entity events for the order the entity events fire in.
Related conditions¶
Three more conditions in ECA Content cover neighboring questions, and none of them needs the original loaded into a token either:
- Entity: original has field value compares the previous value against an expected one, with the usual comparison operator and type. This is the condition for "was it unpublished before?", and it answers that with no Entity: load step.
- Entity: compare field value compares the current value against an expected one. Combined with the previous entry it expresses a specific transition, from one named value to another.
- Entity: compare compares two entities rather than one field.
Conditions describes how several of these combine on one sequence flow.
Why the other library models look harder¶
Two existing models detect a change the long way, and one of them is often the first thing a reader finds.
User network changes and Detect user role changes each load the original entity, compare two encoded scalars and then loop. They have to. Both fields are multi-value, and both models report which individual items were added and which were removed, not merely that the field changed. The loops serve that item-level diff.
The conclusion to avoid is that the loops belong to the change detection. They do not. If either model only had to know that the field changed, one condition would replace the load, the encoding and both loops. The tutorials Building B-Town and Mike Herchel, part 2 walk through the long form step by step, and remain the right reading for the multi-value case.
Requirements¶
The recipe installs eca, eca_content and modeler_api, and applies core's article_content_type recipe. From Drupal 11.4 the Standard profile no longer creates the Article content type, so without that step the model would listen for a bundle that does not exist.
Applying a recipe from within another one is not a no-op on an existing site. Configuration that is already present is never overwritten, but the included recipe also runs its own config actions, and those reset the components of the Article form and view displays to core's defaults. On a site whose Article displays have been customized, apply this recipe to a copy first.
Trying it out¶
Create an article and save it. Edit it, change the title and save again: the message reports the old title and the new one. Save once more without touching the title, and no message appears, because the condition closed the only flow leaving the event.
Dependencies¶
- config
- node.type.article
- module
- eca_content
- modeler_api