Context and paths
The context is the data an expression reads from. You pass it in as
the second argument to evaluate(), resolve(), resolveArray(), or
run(), usually as a plain associative array:
$engine->evaluate('trigger.user.email', [
'trigger' => [
'user' => ['email' => 'ada@example.com'],
],
]);
// → "ada@example.com"
Any top-level key becomes a name the expression can reference. There is
no fixed schema — trigger is a convention from workflow-automation
use, not a requirement. order, form, record, whatever you put in
the array is what expressions can read.
Dot-paths
A path walks the context one segment at a time. Segments are separated by
. and can be array keys or numeric indexes:
trigger.user.email
trigger.items.0.name
nodes.step_2.output
$engine->evaluate('trigger.items.0.name', [
'trigger' => ['items' => [['name' => 'First'], ['name' => 'Second']]],
]);
// → "First"
Missing paths
By default, a path that can't be resolved returns null rather than
erroring:
$engine->evaluate('trigger.user.nickname', ['trigger' => ['user' => []]]);
// → null
This is usually what you want for low-code surfaces, where data may legitimately be absent. To make missing paths an error instead, turn on strict mode.
Safe navigation: ?.
?. stops a chain the moment the value to its left is null, and the
whole expression resolves to null instead of continuing:
$engine->evaluate('trigger.user?.address.city', [
'trigger' => ['user' => null],
]);
// → null (no error, even though .address.city continues past the null)
Without ?., calling a method on null raises an error. With it, the
rest of the chain short-circuits:
$engine->evaluate('trigger.items?.first().name.upper()', [
'trigger' => ['items' => null],
]);
// → null
?. works both for property access (user?.email) and method calls
(items?.first()).
Null-coalescing: ??
?? supplies a fallback when the left side is null:
$engine->evaluate('trigger.user.nickname ?? trigger.user.name ?? "friend"', $context);
It pairs naturally with ?.:
$engine->evaluate('trigger.user?.address?.city ?? "Unknown"', $context);
Strict mode
When strict mode is on, an unresolved path throws
UnresolvedPathException instead of returning
null:
$engine = new Engine([...config('expression-engine'), 'strict' => true]);
$engine->evaluate('trigger.user.email', ['trigger' => []]);
// throws UnresolvedPathException: Path 'trigger.user' could not be resolved from context.
?. still protects against a missing base even in strict mode — an
optional segment is explicitly allowed to resolve to null:
$engine->evaluate('trigger.user?.email', ['trigger' => []]);
// → null, even with strict mode on
A non-optional path with no ?. still throws:
$engine->evaluate('trigger.user.email', ['trigger' => []]);
// throws UnresolvedPathException
Set strict mode globally in config, or
per instance by passing 'strict' => true to the Engine constructor.
Supplying your own context
Every method that takes context accepts either an array or an object
implementing Qanna\ExpressionEngine\Contracts\ContextContract. Provide
your own implementation when you want reads to be backed by something
other than a static array — for example a lazy provider that fetches a
workflow node's output on demand, or a context that persists across
several evaluate() calls in one request.
namespace Qanna\ExpressionEngine\Contracts;
interface ContextContract
{
public function get(string $path, bool $strict = false): mixed;
public function resolve(array $segments, bool $strict = false): mixed;
public function set(string $path, mixed $value): void;
public function has(string $path): bool;
public function withCurrent(mixed $value): ContextContract;
public function all(): array;
}
withCurrent() must return a new context with the given value exposed
as current, without mutating the original — this is what powers
list operations. The bundled Context class is a
straightforward array-backed implementation you can use as a reference.