As of FiQueLa 3.0, custom functions are static-only utility classes that implement one of two interfaces and are registered with the global FunctionRegistry. Once registered, they are available everywhere — both in the fluent API and in FQL string queries — and dispatch goes through the same expression evaluator as the built-ins.
Function contracts
| Interface | Namespace | Use when |
|---|
ScalarFunction | FQL\Functions\Core\ScalarFunction | Your function runs once per row (e.g. LOWER, CONCAT, ROUND). |
AggregateFunction | FQL\Functions\Core\AggregateFunction | Your function accumulates state across a group (e.g. SUM, AVG, GROUP_CONCAT). |
Each class is a static-only container — no constructor, no __invoke. A scalar function exposes name() and a static execute(...). An aggregate function exposes name() plus static initial(), accumulate(...), and finalize(...) methods.
Example: scalar function
The following example appends a _custom suffix to any string value.
use FQL\Functions\Core\ScalarFunction;
final class CustomSuffix implements ScalarFunction
{
public static function name(): string
{
return 'CUSTOM_SUFFIX';
}
public static function execute(mixed $value): string
{
return ((string) $value) . '_custom';
}
}
Example: aggregate function
The following example computes a population standard deviation across a group.
use FQL\Functions\Core\AggregateFunction;
final class StdDev implements AggregateFunction
{
public static function name(): string
{
return 'STDDEV';
}
public static function initial(): array
{
return ['count' => 0, 'sum' => 0.0, 'sumSquares' => 0.0];
}
public static function accumulate(array $state, mixed $value): array
{
if ($value === null) {
return $state;
}
$value = (float) $value;
$state['count']++;
$state['sum'] += $value;
$state['sumSquares'] += $value * $value;
return $state;
}
public static function finalize(array $state): ?float
{
if ($state['count'] === 0) {
return null;
}
$mean = $state['sum'] / $state['count'];
return sqrt($state['sumSquares'] / $state['count'] - $mean * $mean);
}
}
Registering your function
Register the class once at application bootstrap — typically right after the Composer autoloader. The registry is process-global and bootstraps from src/Functions/functions.neon for built-ins.
use FQL\Functions\FunctionRegistry;
FunctionRegistry::register(CustomSuffix::class);
FunctionRegistry::register(StdDev::class);
The registry exposes a small public API:
| Method | Description |
|---|
register(class-string $class) | Register a new scalar or aggregate function. |
override(class-string $class) | Replace an existing function (built-in or user-registered) with the same name. |
unregister(string $name) | Remove a previously registered function. |
loadConfig(string $path) | Bulk-load function classes from a NEON config file. |
has(string $name) | Check whether a function is registered. |
isAggregate(string $name) | Check whether a registered function is an aggregate. |
all() | Return the full registry map. |
reset() | Reset the registry to the built-in defaults. |
setCacheDir(string $path) | Configure the resolved class-map cache directory. |
If you try to register a class that is missing one of the contracts, the registry throws FQL\Exception\FunctionRegistrationException. Calling an unknown function at runtime throws FQL\Exception\UnknownFunctionException.
Using a registered function
Once registered, the function is available by name in both APIs.
FQL string syntax
SELECT
id,
CUSTOM_SUFFIX(name) AS suffixed_name
FROM json(./data/products.json).data.products
Fluent API
The fluent helpers parse SQL expression strings, so you can call your custom function inline anywhere a built-in works:
use FQL\Query\Provider;
$query = Provider::fromFile('./data/products.json')
->from('data.products')
->select('id, CUSTOM_SUFFIX(name) AS suffixed_name')
->groupBy('category')
->select('STDDEV(price) AS price_stddev');
Custom functions are dispatched through Sql\Provider::parseExpression() together with built-ins. The same code path powers both the fluent helpers and the FQL string parser, so a registered function behaves identically in either form.
Migrating from 2.x
If you wrote custom functions for FiQueLa 2.x, the following pieces have been removed and replaced:
| 2.x | 3.0 replacement |
|---|
BaseFunction, SingleFieldFunction, MultipleFieldsFunction, NoFieldFunction, abstract AggregateFunction, BaseFunctionByReference, SingleFieldFunctionByReference, SingleFieldAggregateFunction | Implement Functions\Core\ScalarFunction or Functions\Core\AggregateFunction directly. |
Interface\Invokable, InvokableAggregate, InvokableByReference, InvokableNoField, IncrementalAggregate | New static-only contracts in Functions\Core. |
Interface\Query::custom(Function $fn) | FunctionRegistry::register(MyFn::class) at bootstrap. |
applyValue() / applyValues() instance methods | A single static execute(...) method. |