FunctionRegistry
Manages all template functions (built-in and custom).
Namespace
namespace Qoliber\Tsuku\Function;Class Definition
class FunctionRegistry
{
public function __construct();
public function register(string $name, callable $handler): void;
public function execute(string $name, array $arguments): mixed;
public function has(string $name): bool;
}That is the whole public API: there are no methods to fetch, list or remove functions.
A Tsuku instance creates its own registry unless you pass one to its constructor, and it does not expose it. To inspect or share a registry, create it yourself:
use Qoliber\Tsuku\Tsuku;
use Qoliber\Tsuku\Function\FunctionRegistry;
$registry = new FunctionRegistry();
$tsuku = new Tsuku(functionRegistry: $registry); // 4th constructor argument
$tsuku->registerFunction('badge', fn(string $status): string => strtoupper($status));
var_dump($registry->has('badge')); // bool(true) - it is the same registryConstructor
__construct()
Creates a new FunctionRegistry with all built-in functions registered.
public function __construct()Example:
use Qoliber\Tsuku\Function\FunctionRegistry;
$registry = new FunctionRegistry();Built-in Functions Registered (28):
- String functions:
upper,lower,capitalize,trim,substr,replace,concat - Number functions:
number,number_format(alias ofnumber),round,ceil,floor,abs - Array functions:
join,length,first,last,split,contains - Escaping functions:
html,xml,json,url,csv,escape - Date functions:
date,strtotime - Utility functions:
default
Methods
register()
Register a function, or replace one that has the same name (built-ins included).
public function register(string $name, callable $handler): voidParameters:
$name(string) - Function name, used as@name(...)in templates$handler(callable) - Function implementation
Returns:
- void
The name is not validated. Names that the template lexer reads as something else - names starting with end, else or default, the built-in directive names, names with - or . - can be registered but never called from a template; see Function Names.
Example:
$registry->register('currency', function(float $amount, string $code = 'USD'): string {
return match($code) {
'USD' => '$' . number_format($amount, 2),
'EUR' => '€' . number_format($amount, 2),
'GBP' => '£' . number_format($amount, 2),
default => $code . ' ' . number_format($amount, 2),
};
});Tsuku::registerFunction($name, $handler) does exactly this on the instance's registry (and returns the Tsuku instance).
has()
Check if a function is registered.
public function has(string $name): boolParameters:
$name(string) - Function name (case-sensitive)
Returns:
- (bool) - True if function exists, false otherwise
Example:
if ($registry->has('currency')) {
echo "Currency function is registered";
}
if (!$registry->has('unknown')) {
echo "Unknown function not found";
}execute()
Call a registered function with arguments.
public function execute(string $name, array $arguments): mixedParameters:
$name(string) - Function name$arguments(array) - Function arguments (indexed array), passed to the handler as they are
Returns:
- (mixed) - The handler's return value, unchanged (templates cast it to string;
execute()does not)
Throws:
Qoliber\Tsuku\Exception\FunctionNotFoundException- If the function is not registered- Anything the handler throws (e.g. a
TypeError)
Example:
// Call built-in function
$result = $registry->execute('upper', ['hello']);
// Returns: "HELLO"
// Call with multiple arguments
$result = $registry->execute('substr', ['Hello World', 0, 5]);
// Returns: "Hello"
// Call custom function
$registry->register('add', function(int $a, int $b): int {
return $a + $b;
});
$result = $registry->execute('add', [5, 3]);
// Returns: 8 (an int)Complete Example
Using FunctionRegistry Directly
use Qoliber\Tsuku\Function\FunctionRegistry;
$registry = new FunctionRegistry();
// Register custom functions
$registry->register('currency', function(float $amount, string $code = 'USD'): string {
return match($code) {
'USD' => '$' . number_format($amount, 2),
'EUR' => '€' . number_format($amount, 2),
default => $code . ' ' . number_format($amount, 2),
};
});
$registry->register('slug', function(string $text): string {
return strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', trim($text)));
});
// Check if functions exist
if ($registry->has('currency')) {
$result = $registry->execute('currency', [99.99, 'EUR']);
echo $result; // €99.99
}Using with Tsuku
Pass the registry to the Tsuku constructor (named argument functionRegistry, or the 4th positional argument):
use Qoliber\Tsuku\Tsuku;
use Qoliber\Tsuku\Function\FunctionRegistry;
$registry = new FunctionRegistry();
// Register function
$registry->register('badge', function(string $status): string {
return match($status) {
'active' => '✓ Active',
'pending' => '⏳ Pending',
'inactive' => '✗ Inactive',
default => '? Unknown'
};
});
$tsuku = new Tsuku(functionRegistry: $registry);
// Use in template
$template = 'Status: @badge(user.status)';
$result = $tsuku->process($template, ['user' => ['status' => 'active']]);
// Output: Status: ✓ ActiveThe registry is shared, not copied: functions registered on $registry later are available to $tsuku (and to every other instance created with the same registry).
Extending with Multiple Custom Functions
use Qoliber\Tsuku\Tsuku;
use Qoliber\Tsuku\Function\FunctionRegistry;
class CustomFunctions
{
public static function register(FunctionRegistry $registry): void
{
// Phone formatting
$registry->register('phone', function(string $phone): string {
return preg_replace('/(\d{3})(\d{3})(\d{4})/', '($1) $2-$3', $phone);
});
// SKU formatting
$registry->register('format_sku', function(string $sku): string {
return strtoupper(trim($sku));
});
// Percentage
$registry->register('percent', function(float $value, int $decimals = 0): string {
return number_format($value * 100, $decimals) . '%';
});
// File size
$registry->register('filesize', function(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < 4; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
});
}
}
// Usage
$registry = new FunctionRegistry();
CustomFunctions::register($registry);
$tsuku = new Tsuku(functionRegistry: $registry);
echo $tsuku->process('@phone(p) @format_sku(s) @percent(v, 1) @filesize(b)', [
'p' => '5551234567', 's' => ' ab-1 ', 'v' => 0.256, 'b' => 1536000,
]);
// Output: (555) 123-4567 AB-1 25.6% 1.46 MBFunction Signatures
Return Values
A return type is not required - any callable works. In a template the return value is cast to string (null/false print nothing, true prints 1, arrays print Array with a PHP warning); execute() returns it unchanged.
// ✅ Works
$registry->register('test', function($x) {
return $x;
});
// ✅ Clearer - with types
$registry->register('test', function(string $x): string {
return $x;
});Parameter Type Hints
Recommended but not required. In templates, quoted literals and number literals arrive as strings and variables keep their PHP type; typed parameters are coerced by PHP as usual ("5" → 5):
// ✅ Best practice - full type hints
$registry->register('add', function(int $a, int $b): int {
return $a + $b;
});
// ⚠️ Works but not recommended
$registry->register('add', function($a, $b): int {
return $a + $b;
});Default Parameters
Supported:
$registry->register('greet', function(string $name, string $greeting = 'Hello'): string {
return "{$greeting}, {$name}!";
});
// Call with 1 argument
$result = $registry->execute('greet', ['John']);
// Returns: "Hello, John!"
// Call with 2 arguments
$result = $registry->execute('greet', ['John', 'Hi']);
// Returns: "Hi, John!"Variadic Parameters
Supported:
$registry->register('glue', function(string ...$parts): string {
return implode('', $parts);
});
$result = $registry->execute('glue', ['Hello', ' ', 'World']);
// Returns: "Hello World"Built-in Functions List
All built-in functions are registered automatically. Signatures as implemented: handlers typed mixed cast their input; substr, replace and strtotime pass it straight to PHP, so an integer from your data (e.g. @substr(order.id, 0, 3) with 'id' => 12345) is a TypeError - in a template a function error, i.e. empty output in SILENT mode. Cast such values to string in your data.
String Functions:
upper(mixed $str): string-strtoupper(), byte-based (no multibyte case mapping)lower(mixed $str): stringcapitalize(mixed $str): string-ucfirst()trim(mixed $str): stringsubstr(string $str, int $start, ?int $length = null): stringreplace(string|array $str, mixed $search, mixed $replace): string|array-str_replace()concat(mixed ...$parts): string
Number Functions:
number(mixed $num, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): stringnumber_format(...)- same asnumberround(mixed $num, int $precision = 0): floatceil(mixed $num): floatfloor(mixed $num): floatabs(mixed $num): float
Array Functions:
join(mixed $array, string $glue = ','): stringlength(mixed $value): int-count()for arrays, string length in bytes otherwisefirst(mixed $array): mixed-nullfor an empty or non-array valuelast(mixed $array): mixedsplit(mixed $value, string $delimiter = ','): array-""/nullgive[], an array is returned unchanged, an empty delimiter splits into characterscontains(mixed $haystack, mixed $needle): bool- same rules asneedle in haystackin conditions
Escaping Functions:
html(mixed $str): string-htmlspecialchars(ENT_QUOTES | ENT_HTML5)xml(mixed $str): string-htmlspecialchars(ENT_QUOTES | ENT_XML1)json(mixed $str): string- a quoted JSON string;",',<,>,&become",',<,>,&url(mixed $str): string-rawurlencode()csv(mixed $str): string- quotes the value (doubling") when it contains,,"or a line breakescape(mixed $str, string $type = 'html'): string-html,xml,json,urlorcsv
Date Functions:
date(string $format, mixed $timestamp = null): string- the timestamp is cast toint; pass a Unix timestamp, not a date stringstrtotime(string $str): int|false
Utility Functions:
default(mixed $value, mixed $default): mixed-$defaultwhenempty($value)(so also for"0"and0)
Error Handling
Function Not Found
use Qoliber\Tsuku\Exception\FunctionNotFoundException;
try {
$result = $registry->execute('nonexistent', ['arg']);
} catch (FunctionNotFoundException $e) {
echo "Error: " . $e->getMessage();
// Error: Function 'nonexistent' not found
echo $e->getFunctionName(); // nonexistent
}FunctionNotFoundException extends TsukuException. In templates, an unknown function in output follows the strictness mode; an unknown function in a condition always throws FunctionNotFoundException.
Invalid Function Name
There is no name check, so this does not throw:
$registry->register('invalid-name', fn() => 'test');
var_dump($registry->has('invalid-name')); // bool(true)Calling it from a template fails, because @invalid-name(...) is not a function call: the lexer throws ParseException ("Invalid @ syntax ... (write @@ for a literal @)").
Type Errors
$registry->register('add', function(int $a, int $b): int {
return $a + $b;
});
try {
// Passing strings instead of integers
$result = $registry->execute('add', ['foo', 'bar']);
} catch (\TypeError $e) {
echo "Type error: " . $e->getMessage();
}In a template the same TypeError is a function error: removed in SILENT, a warning in WARNING, a TsukuException in STRICT.
Performance Considerations
Function Lookup
Function lookup is O(1) using array access:
// Fast - single array lookup
$registry->execute('upper', ['text']);Avoid Reregistration
Register functions once, reuse many times:
// ✅ Good - register once
$registry->register('format', fn($x) => strtoupper($x));
for ($i = 0; $i < 1000; $i++) {
$registry->execute('format', ["text{$i}"]);
}
// ❌ Bad - registering in loop
for ($i = 0; $i < 1000; $i++) {
$registry->register('format', fn($x) => strtoupper($x));
$registry->execute('format', ["text{$i}"]);
}Shared State
A FunctionRegistry is an ordinary mutable object:
- ✅ Safe: any number of
execute()/ template calls - ⚠️ A registry shared between
Tsukuinstances is shared state - a function registered or replaced through one instance changes all of them - Recommendation: register all functions at initialization
Memory Usage
A new registry with the 28 built-in closures takes about 12 KB (measured with memory_get_usage() on PHP 8.3); each custom function adds the size of its closure.
Next Steps
- Tsuku Class → - Main API class
- Built-in Functions → - Function reference
- Custom Functions → - Creating functions