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 typestring, 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 APIregisterMethod('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.