Skip to main content

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.