Skip to content

Combined Conditions

Version v2

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_0002

# Apply recipe with Drush (requires version 13 or later):
drush recipe ../recipes/eca_lib_0002

# Apply recipe without Drush:
cd web && php core/scripts/drupal recipe ../recipes/eca_lib_0002

# Rebuilding caches is optional, sometimes required:
drush cr

Purpose

ECA has no AND operator and no OR operator. It has one rule about links, and every way of combining conditions follows from that rule. A successor link carries at most one condition. In the stored configuration, condition holds a single component ID rather than a list, so there is no second slot to put another condition into.

Everything below is built on top of that constraint. The model reads three values, evaluates five conditions, and reports which of three independent rules matched. Each rule uses a different combination idiom, so this is really three worked examples sharing one set of inputs.

Running the model

The model starts from a custom event, which means it needs no content, no form, and no browser. Set the three inputs, then trigger it.

1
2
3
4
drush state:set eca_lib_0002_channel web
drush state:set eca_lib_0002_amount 250
drush state:set eca_lib_0002_vip 0
drush eca:trigger:custom_event eca_lib_0002

The inputs deliberately live outside the model, in Drupal's persistent state. Three eca_keyvaluestore_read actions load them into the tokens [channel], [amount], and [vip] before any condition runs. Keeping them outside is what makes the model worth running more than once. Change one value, trigger again, and watch a different set of paths report.

Those read actions use the collection state, which is Drupal core's own state, the store that drush state:set writes to. ECA also ships an action called eca_state_read, labeled as a persistent state read, and that one is not the same thing. It reads a private key value collection belonging to ECA, so a value placed with drush state:set is invisible to it. Use eca_keyvaluestore_read with the collection state whenever you want a value that the command line can set.

AND, by chaining through a pass-through action

Because a link carries one condition, an AND needs a second link to hang the second condition on. ECA ships an action for exactly this. Chain action for AND condition takes no configuration and does nothing at all. Its only job is to be a node in the middle, so that another conditioned link can leave it.

Path A is that shape. One link leaves Report the inputs carrying "channel is web" and reaches the AND action. One link leaves the AND action carrying "amount is at least 100" and reaches the message. The message runs only when both conditions returned TRUE, because a link whose condition is FALSE skips its target and everything behind it. Add one more AND action for every further condition you need.

An OR needs no helper action. Draw one link per condition from the same source to the same target. Any single link whose condition returns TRUE is enough to run the shared target.

Path B is that shape. Two links leave Report the inputs, one carrying "channel is phone" and one carrying "channel is partner", and both point at the same message action.

ECA walks every outgoing link rather than stopping at the first match, so it is fair to ask what happens when two of them are true at once. Within one predecessor, nothing surprising happens. ECA keeps a list of the successors it has already executed for that predecessor and refuses to execute the same one twice, logging Prevent duplicate execution when it declines. The guard is scoped to a single predecessor, though. If two different actions both link to the same target, that target does run twice, once per predecessor, and the guard never sees the second call.

Nesting the two into (A or B) and (C or D)

The two idioms compose, and composing them is the point of this model. Fan several conditioned links into an AND action to get the left side of the expression, then fan several conditioned links out of it to get the right side.

Path C is that shape. Two links leave Report the inputs carrying "channel is phone" and "channel is partner" and both reach the same AND action, which is the (phone or partner) half. Two links leave that AND action carrying "amount is at least 100" and "vip flag is 1" and both reach the same message, which is the (at least 100 or vip) half. The message runs when one condition from each half is true.

What each input set produces

Four input sets cover the interesting behavior of the model.

channel amount vip Reported
web 250 0 the inputs, then path A
web 50 0 the inputs only
phone 20 0 the inputs, then path B
partner 500 1 the inputs, then path B, then path C

The second row is the one to dwell on. Path A does not report, because the AND chain stopped at its second link. The Report the inputs message still appears, which is how you tell a model that ran and matched nothing from a model that never ran at all.

The fourth row is the other one to dwell on. Two independent rules matched the same input, and both reported. Rules in ECA do not compete, and nothing makes them exclusive unless you make them exclusive yourself.

Gateways are not a branching primitive

It is tempting to reach for a gateway to express a choice. Do not. ECA's configuration schema restricts gateways.*.type to the single value 0, and a gateway passes control to all of its successors, subject only to the condition on each outgoing link. Despite the "exclusive gateway" label that modelers show, the behavior is inclusive. A gateway is useful for merging flows and for keeping a diagram readable, and it adds no logic of its own. The combining is done by the links, exactly as described above.

Three things that will otherwise cost you an hour

Drupal's messenger drops a repeated identical string. Every branch in this model therefore reports a different sentence. If you copy this model and give two branches the same message text, one of them will appear to have been skipped when it actually ran. Give each outcome its own wording, or read the ECA log instead of the messages.

After applying the recipe, run drush eca:subscriber:rebuild. ECA listens only to the events named in a cached list of enabled models, and an import that leaves that list stale produces a model that looks perfect in the interface and never fires. The command is cheap and idempotent, so run it after every import rather than working out whether this one needed it.

Triggered from Drush, the model runs as the anonymous user. Nothing here depends on that, because the model touches no entities and checks no access. It matters as soon as you extend the model, so keep it in mind before concluding that a newly added action is broken.

Dependencies

  • module
    • eca_base
    • modeler_api

Used plugins

Events

Conditions

Actions

Changelog

v1

Initial version. It never ran on any site, for the reasons recorded under v2.

v2

Rebuild the model, because v1 could not fire and never had been able to. Its events list was empty, so nothing subscribed it to anything and no trigger could reach it. Its only action, a "Chain action for AND condition" labeled AND, named a successor Activity_05w316n that existed in no pool, so even a reachable model would have stopped at the first link. There were no conditions in the model at all, which left the documentation describing three combinations that the configuration did not contain.

Replace it with a model that demonstrates the combinations rather than describing them. The new model reads three inputs from Drupal's persistent state, evaluates five Compare two scalar values conditions, and reports which of three rules matched. The rules are an AND, an OR, and a nested (A or B) and (C or D), so each of the three combination idioms appears once as working configuration.

Start the model from a custom event so that it can be driven entirely from a shell. The operator sets the inputs with drush state:set, triggers with drush eca:trigger:custom_event eca_lib_0002, and reads the result from the messages Drush prints. Changing one input and triggering again is the exercise the model is built around.

Report each outcome with its own distinct message, because Drupal's messenger drops a repeated identical string and a shared message would hide a branch that really did run.