Skip to main content

Expressions and templates

There are two ways to run an expression, and the difference is entirely about what comes back.

evaluate() — one expression, one typed value

evaluate() takes a raw expression string with no delimiters and returns whatever it resolves to, with its type intact:

$engine->evaluate('trigger.order.total', ['trigger' => ['order' => ['total' => 49.5]]]);
// → 49.5 (float)

$engine->evaluate('trigger.order.paid', ['trigger' => ['order' => ['paid' => true]]]);
// → true (bool)

$engine->evaluate('trigger.order.lines', $context);
// → [ ... ] (array)

Use evaluate() whenever you need the value itself — a condition to branch on, a number to store, a list to iterate.

resolve() — a string with {{ }} placeholders

resolve() scans a string for expressions wrapped in the configured delimiters ({{ }} by default) and replaces each one:

$engine->resolve('Total due: {{ trigger.order.total }}', $context);
// → "Total due: 49.5"

The single-expression exception

If the string is nothing but one placeholder — no surrounding text — resolve() returns the raw typed value instead of a string:

$engine->resolve('{{ trigger.order.lines }}', $context);
// → [ ... ] (the array, not its JSON string)

$engine->resolve('{{ trigger.order.paid }}', $context);
// → true (bool)

This lets you store one template string in a config field and use it both for display ("Total: {{ ... }}") and for producing a real value ("{{ ... }}") without a separate code path.

As soon as there is any other text or a second placeholder, every result is converted to a string and spliced in:

  • null becomes an empty string
  • true / false become "true" / "false"
  • arrays become compact JSON
  • everything else is cast with (string)
$engine->resolve('Paid: {{ trigger.order.paid }}', $context);
// → "Paid: true"

$engine->resolve('Value: {{ trigger.order.missing }}', $context);
// → "Value: "

resolveArray() — resolve every string in a structure

resolveArray() walks an array recursively and runs resolve() on every string value it finds. Non-string values (ints, bools, null) pass through untouched. Nested arrays are walked in place.

$engine->resolveArray([
'to' => '{{ trigger.user.email }}',
'subject' => 'Welcome, {{ trigger.user.name }}',
'meta' => [
'item_count' => '{{ trigger.cart.items.count() }}',
'flagged' => true,
],
], $context);

Because the array's string values each go through resolve(), the single-expression exception applies per value: 'item_count' above comes back as an integer, while 'subject' comes back as an interpolated string.

Passing context

All three methods take the data as their second argument. It can be a plain associative array (the common case) or a ContextContract instance if you need custom resolution behaviour.

$engine->evaluate('trigger.user.name', ['trigger' => ['user' => ['name' => 'Ada']]]);

Caching within a call

Within a single resolve(), resolveArray(), or run() call, identical expressions evaluated against identical context are computed once and reused. Parsed expressions (the AST) are also cached on the engine instance and reused across calls. Both behaviours are on by default and can be turned off — see Configuration.