CEL-PHP implements CEL proposal 246 (Optional Values).
Optionals make it possible to express "a value that may or may not be present": conditionally
provided variables, field selections that may be absent, and fields that are only set when a value
exists, without deeply nested has() / ternary expressions.
An optional(T) either holds a value (optional.of(x)) or is empty (optional.none()). Optional
support is enabled by default.
- Creating Optionals
- Inspecting Optionals
- Optional Field Selection and Indexing
- Viral Propagation
- Optional Construction
- Combining Optionals
- Transforming Optionals
- List Helpers
- Equality
- PHP API
| Expression | Result |
|---|---|
optional.of(v) |
An optional holding v (any value, including null). |
optional.ofNonZeroValue(v) |
optional.of(v) unless v is a zero value, in which case optional.none(). |
optional.none() |
An empty optional. |
A zero value is the default empty value for a type: null, false, numeric zero (0, 0u,
0.0), the empty string "", empty bytes b"", the empty list [], the empty map {}, and a
zero duration. Timestamps and optionals are never zero values. Messages are never zero values
unless the underlying message implements Cel\Message\ZeroValueInterface and reports itself as zero
(the bundled decimal type does this, so decimal("0") is a zero value).
Cel\evaluate('optional.of(42).value()'); // 42
Cel\evaluate('optional.ofNonZeroValue("").hasValue()'); // false
Cel\evaluate('optional.ofNonZeroValue("hi").hasValue()'); // true| Method | Result |
|---|---|
opt.hasValue() |
true if the optional holds a value. |
opt.value() |
The contained value, or an error (optional.none() dereference) if empty. |
Cel\evaluate('optional.of(1).hasValue()'); // true
Cel\evaluate('optional.none().hasValue()'); // false
Cel\evaluate('optional.of(1).value()'); // 1Prefix a selection or index with ? to get an optional instead of an error when the field, key, or
index is absent.
| Syntax | Meaning |
|---|---|
msg.?field |
optional.of(msg.field) if present, else optional.none(). |
map[?key] |
The value at key if present, else optional.none(). |
list[?index] |
The element at index if in bounds, else optional.none(). |
Cel\evaluate('{"a": 1}.?a.value()', []); // 1
Cel\evaluate('{"a": 1}.?b.hasValue()', []); // false
Cel\evaluate('[10, 20][?5].hasValue()', []); // false
Cel\evaluate('m[?"key"].orValue("none")', ['m' => []]); // "none"Optional selection is viral: once a chain produces an optional, every subsequent selection and index is treated as optional too, and an empty optional short-circuits the rest of the chain. These are equivalent:
msg.?field.subfield
msg.?field.?subfield
// short-circuits to optional.none() if `a` is missing, otherwise selects `b`
Cel\evaluate('{"a": {"b": 2}}.?a.b.value()', []); // 2
Cel\evaluate('{}.?a.b.hasValue()', []); // falsePrefix a map key, message field, or list element with ? to include it only when the right-hand
side is a present optional. An empty optional is skipped. The right-hand side must be an
optional(T).
// map: the "nickname" entry is omitted because the optional is empty
Cel\evaluate('{"name": "Jane", ?"nickname": optional.none()}', []);
// => ["name" => "Jane"]
// list: only present optionals contribute elements
Cel\evaluate('[1, ?optional.of(2), ?optional.none(), 3]', []);
// => [1, 2, 3]
// message: set an optional field only when present (see Custom Types / Messages)
// Profile{name: "Jane", ?nickname: user.?nickname}| Method | Result |
|---|---|
opt.or(other) |
opt if it holds a value, otherwise other (another optional). |
opt.orValue(fallback) |
The contained value, otherwise fallback (a plain value). |
or and orValue are short-circuiting: the alternative is only evaluated when opt is empty,
so a.or(b).or(c) stops at the first present optional.
Cel\evaluate('optional.none().or(optional.of(9)).value()', []); // 9
Cel\evaluate('m[?"key"].orValue("default")', ['m' => []]); // "default"| Macro | Result |
|---|---|
opt.optMap(v, expr) |
If present, binds the value to v, evaluates expr, and wraps the result: optional.of(expr). Otherwise optional.none(). |
opt.optFlatMap(v, expr) |
Like optMap, but expr must itself return an optional(T), which is returned as-is (flattened). |
Cel\evaluate('optional.of(42).optMap(n, n + 1).value()', []); // 43
Cel\evaluate('{"k": {"n": "v"}}.?k.optFlatMap(m, m.?n).value()', []); // "v"| Function | Result |
|---|---|
list.first() |
The first element as an optional, or optional.none() if the list is empty. |
list.last() |
The last element as an optional, or optional.none() if the list is empty. |
optional.unwrap(list) |
A list of the values of all present optionals in list (empty optionals dropped). |
list.unwrapOpt() |
Postfix form of optional.unwrap. |
Cel\evaluate('[1, 2, 3].first().value()', []); // 1
Cel\evaluate('[1, 2, 3].last().value()', []); // 3
Cel\evaluate('[].first().hasValue()', []); // false
Cel\evaluate('optional.unwrap([optional.of(1), optional.none(), optional.of(3)])', []); // [1, 3]
Cel\evaluate('[optional.of(1), optional.none()].unwrapOpt()', []); // [1]Two optionals are equal when both are empty, or both hold equal values.
Cel\evaluate('optional.none() == optional.none()'); // true
Cel\evaluate('optional.of(1) == optional.of(1)'); // true
Cel\evaluate('optional.of(1) == optional.none()'); // falseThe runtime type of an optional is optional_type:
Cel\evaluate('type(optional.of(1))'); // the `optional_type` type valueOptionals are represented at runtime by Cel\Value\OptionalValue:
use Cel\Value\IntegerValue;
use Cel\Value\OptionalValue;
$present = OptionalValue::of(new IntegerValue(1));
$empty = OptionalValue::none();
$present->hasValue(); // true
$present->value; // IntegerValue(1) (null when empty)
$empty->getRawValue(); // null