Extending the engine
You can add your own functions (called by name: myFunc(...)) and
your own chainable methods (value.myMethod(...)) without modifying
the package. Both are registered on an Engine instance.
Register a function
$engine->registerFunction('discount', function (float $amount, float $percent): float {
return round($amount - ($amount * $percent / 100), 2);
});
$engine->evaluate('discount(trigger.order.total, 10)', $context);
The handler receives the call's arguments, already evaluated. Whatever it returns becomes the expression's value and can be chained onto.
A function can close over application state — this is how you expose lookups that aren't in the context:
$engine->registerFunction('setting', fn (string $key) => config("app.$key"));
$engine->evaluate('setting("timezone")', []);
Register a method
A method is bound to a value type — string, int, double,
bool, array, Carbon\CarbonImmutable, or any to make it available
on every value. The handler receives the value as its first argument and
the call's arguments after that.
$engine->registerMethod('initials', 'string', function (string $name): string {
return collect(explode(' ', $name))
->map(fn ($p) => Str::substr($p, 0, 1))
->implode('');
});
$engine->evaluate('trigger.user.name.initials()', [
'trigger' => ['user' => ['name' => 'Ada Lovelace']],
]);
// → "AL"
Registering a name/type pair that already exists overrides it.
Registering for any
$engine->registerMethod('dump', 'any', function (mixed $value) {
logger()->debug('expression value', ['value' => $value]);
return $value;
});
A method registered for a specific type takes precedence over one
registered for any when both match.
Where to register in Laravel
The container binding is a singleton, so register once in a service provider so every caller sees your extensions:
public function boot(): void
{
$engine = $this->app->make(\Qanna\ExpressionEngine\Engine::class);
$engine->registerFunction('discount', /* ... */);
$engine->registerMethod('initials', 'string', /* ... */);
}
Custom methods and introspection
Anything you register shows up in the
introspection API — registerMethod('initials', 'string', ...) makes initials appear in availableMethods('string')
and in suggestMethods() results for string values, so editor
autocomplete picks it up for free.
Providing a custom context
Registering functions and methods covers most extension needs. If you
need reads themselves to behave differently — lazy loading, a persistent
context across calls — implement
ContextContract
and pass your instance instead of an array.