drupal-eca-recipe / eca_lib_0004
Demonstrates how to express an if/elseif/else decision in an ECA model, the pattern people usually reach for when they want a PHP switch statement. ECA has neither a switch construct nor a working exclusive gateway, so the model uses the sentinel-default idiom instead: it assigns the default value u
Package info
gitlab.lakedrops.com/drupal/recipes/eca_lib_0004
Type:drupal-recipe
pkg:composer/drupal-eca-recipe/eca_lib_0004
Requires
- drupal/eca: *
- drupal/modeler_api: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
ID: eca_lib_0004
PHP has a switch statement. ECA does not, and this model is often mistaken for one. What it really implements is an if/elseif/else decision: a single value is tested against a set of alternatives, a matching alternative wins, and a default applies when nothing matches. Everything below is about how that shape is assembled from ECA's flat list of successors, because ECA offers no branching construct to build it with.
The model reacts to the presave event of an Article node. It reads field_select and writes field_status: the values a1, a2, a3 and c7 produce a status of 3, the values b1 and b2 produce a status of 4, and anything else leaves the status at its default of 2. A message action then reports the resulting status.
The PHP equivalent
The honest equivalent is not a switch block. It is an assignment followed by a chain of conditions:
// Assign the default up front.
$field_status = 2;
// A matching branch overwrites it.
if (in_array($field_select, ['a1', 'a2', 'a3', 'c7'], TRUE)) {
$field_status = 3;
}
elseif (in_array($field_select, ['b1', 'b2'], TRUE)) {
$field_status = 4;
}
// One step later, ask whether anything matched at all.
if ($field_status === 2) {
// Nothing matched, so the default still stands.
}
That final test is the part a switch statement never needs, and it is the part this model exists to teach.
The sentinel-default idiom
ECA cannot ask "did any of my branches match?" There is no join, no else link and no flag that a successor can raise for its siblings to read. The sentinel-default idiom works around that in three steps:
- Assign the default first. The action "Set default Status to 2" runs unconditionally, straight after the event. At this point every node has a status of
2, whether or not it deserves one. - Let a matching branch overwrite it. Six conditioned links leave that same action. Each one compares
field_selectagainst a single expected value, and the branch that matches replaces the default with3or4. - Test for the sentinel one hop downstream. A seventh, unconditioned link leaves the same action and reaches a gateway. The gateway's outgoing link carries the only remaining condition: is
field_statusstill2? If it is, no branch matched, and the no-match path continues.
The sentinel value has to be one that no branch can ever produce. Here that holds, because the branches write 3 and 4. If a branch could legitimately assign 2, the downstream test would misread a successful match as a no-match, and the idiom would quietly break.
Note that the two branch targets and the no-match path all converge on the same message action. As a result that message runs exactly once: either through the branch that matched, or through the sentinel path when nothing did.
Why the "no condition" link works
Earlier versions of this page claimed that ECA first checks the conditions of all successors and only afterwards executes the actions behind the ones that returned TRUE. That is not what happens, and it is not what makes this model work.
ECA walks successors in the order they are stored. For each link in turn it evaluates that link's condition, and if the condition returns FALSE it skips the target action together with everything behind it. There is no pre-pass over the sibling links.
The reason the sentinel test is not reached too early is purely positional. The unconditioned link to the gateway is the seventh successor of "Set default Status to 2", so the six branch links have already been walked by the time control arrives at the gateway. More importantly, the sentinel condition does not sit on that seventh link at all. It sits on the gateway's outgoing link, one hop further downstream, so it is evaluated only after the gateway itself has been reached. That extra hop is what buys the delay. Put the sentinel condition directly on the link into the gateway and the timing still happens to work here, but the intent becomes much harder to read, and the pattern stops generalizing to models where more work precedes the test.
ECA has no exclusive gateway
It is tempting to read the fan-out as an exclusive choice enforced by the gateway. It is not. ECA's configuration schema restricts gateways.*.type to the single value 0, and the documentation is explicit that despite the "exclusive gateway" or "x-gateway" label, a gateway behaves inclusively. A gateway passes control to every one of its successors, subject only to the condition on each outgoing link. It gives you neither an exclusive choice nor an AND join.
Mutual exclusivity in this model therefore rests on the data, not on the diagram. field_select is single-valued, so at most one of the six comparisons can be true. Point the same model at a multi-value field and both branches fire: the a-group branch sets the status to 3, the b-group branch then overwrites it with 4, and the message action runs twice. If you adapt this pattern to a field that can hold more than one value, you have to make the conditions exclusive yourself.
Combining values with the four-links-to-one-target idiom
The a-group is worth studying on its own. The values a1, a2, a3 and c7 all map to a status of 3, which in PHP is a run of fall-through case labels. ECA expresses it differently: a link carries at most one condition, and there is no way to attach a second condition to the same link.
The solution is to draw four separate links from "Set default Status to 2" to "Set Status to 3", each carrying its own equality condition. Because ECA walks each link independently and skips only the target behind a link whose condition is FALSE, any single matching link is enough to run the shared target. Several conditioned links into one target is ECA's OR.
This is a genuinely reusable technique, and it composes in the opposite direction to chaining. Chaining conditions one behind the other gives you AND; fanning several conditioned links into a common target gives you OR.
Overlapping links are safe. ECA keeps a list of the successors it has already executed for the predecessor it is walking, and refuses to execute the same one twice, logging Prevent duplicate execution when it declines. All four a-group links leave "Set default Status to 2", so even if two of their conditions were true at once, "Set Status to 3" would still run once.
That guard is scoped to a single predecessor, which is what makes the multi-value case above different. There the a-group branch and the b-group branch are two separate predecessors, and both link to the message action, so the message really does run twice.
Installation
## Import recipe
composer require drupal-eca-recipe/eca_lib_0004
# Apply recipe with Drush (requires version 13 or later):
drush recipe ../recipes/eca_lib_0004
# Apply recipe without Drush:
cd web && php core/scripts/drupal recipe ../recipes/eca_lib_0004
# Rebuilding caches is optional, sometimes required:
drush cr