Skip to content

Conditional Branching with Default

Version v3

Initialising viewer…

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
## 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

Purpose

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 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:

  1. 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.
  2. Let a matching branch overwrite it. Six conditioned links leave that same action. Each one compares field_select against a single expected value, and the branch that matches replaces the default with 3 or 4.
  3. 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_status still 2? 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.

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.

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. The only caveat is the one above: if two of the links can be true at the same time, the shared target executes once per matching link.

Dependencies

  • config
    • field.field.node.article.field_status
    • field.storage.node.field_status
    • node.type.article
  • module
    • eca_base
    • eca_content
    • modeler_api
    • node

Used plugins

Events

Conditions

Actions

Changelog

v1

Initial version

v2

Add the "no condition" for the default value

v3

Fix the six branch conditions. Each of them carried negate: true, which inverted the comparison so that a branch fired for every value except its own expected value. With a single-valued field_select, five of the six negated conditions were true on any given save, both branch targets ran, and the status ended up as 4 regardless of the selected value. All six now use negate: false, and their case setting has been aligned with the plugin default of false.

Rename the model from "Switch Case Default" to "Conditional Branching with Default", because the model never implemented a switch statement and the old name sent readers looking for one.

Rewrite the documentation to match. The previous PHP example was a switch block with no break statements, so it fell through every case and always assigned 2, describing nothing the model does; it has been replaced with the sentinel-default shape the model actually implements. The previous note on execution order claimed that ECA evaluates all successor conditions before executing any of them, which is not how ECA processes successors; the corrected note explains the positional mechanism instead.

Set save_entity to false on the three field-setting actions. The event is a presave, so the entity is already on its way to storage and ECA does not need to save it again. Saving from within presave re-entered the same event: updating an existing node logged "Recursion within configured ECA events detected", and creating a new one failed outright with "Update existing 'node' entity while changing the ID is not supported", so no article could be created at all while the model was enabled.