Risma PHP Documentation¶
A lightweight, zero-dependency string template engine and interpreter for PHP >= 7.4.
The logic lives inside the string itself.
📑 Table of Contents¶
- 1. Overview & Philosophy
- 2. Requirements
- 3. Installation
- 4. Quick Start
- 5. Core Concepts
- Variable Placeholders
- Pipeline Chaining (
.) - Smart Argument Parsing
- Argument Routing (
$) - Direct Execution (
@) - Deep Nesting & Recursion
- Escaping Placeholders (
!) - Shared Global Variables (v2)
- Custom Delimiters (v2)
- Static Quick Helpers (v2)
- Template Introspection (v2)
- Configurable Recursion Depth (v2)
- Safe Array & Object Handling (v2)
- 6. Built-in Arsenal (
Functions.php) - Logic & Conditionals
- Multilingual Pluralization
- Multibyte UTF-8 Text
- String Manipulation & CMS
- Arrays & Collections
- Global PHP Functions
- 7. Extending Risma
- Custom Functions (
addFunc,addFuncs) - Class Registration (
addClass,addClasses) - Object Registration (
addObject,addObjects) - Resolution Order
- 8. Engine API Reference
- 9. Production Recipes
- 10. Testing
1. Overview & Philosophy¶
Traditional PHP template engines require developers to implement view models, controllers, or preprocessing steps before strings are rendered.
Risma shifts this paradigm: the logic lives inside the string itself.
This architecture turns plain text into smart, autonomous templates. It is ideal for:
- 🌐 Dynamic Translations: Store strings with built-in pluralization, gender, and fallbacks directly in translation files.
- 📧 Notification & Email Templates: Allow non-developers to edit messages with formatting pipelines without touching PHP code.
- 🧩 Safe CMS Sandboxing: Safe execution of user-authored templates with a deterministic lexer.
- ⚙️ Config-Driven Messages: Dynamic system logs, alert banners, and CLI outputs.
Security: Strictly Zero eval()¶
Risma executes transformations using recursive regular expressions and a custom lookahead-based argument lexer. It never uses eval() or temporary file generation. Templates run safely even when sourced from untrusted user input or external databases.
2. Requirements¶
- PHP Version:
>= 7.4(fully tested and verified on PHP 7.4, 8.0, 8.1, 8.2, 8.3, and 8.4) - Runtime Dependencies:
0(Zero external packages required) - Dev Dependencies: Pest PHP v4 (for testing)
3. Installation¶
Install via Composer:
4. Quick Start¶
use Nabeghe\Risma\Risma;
// Instant static execution
$template = 'Hi {user.trim.title}! You have {count} unread {count.plural("message", "messages")}. '
. 'Generated at {@date("H:i")} | Status: {@phpversion.if_gte("8.0", "Modern 🚀", "Legacy ⚠️")}';
echo Risma::quick($template, [
'user' => ' alex murphy ',
'count' => 3,
]);
// Output: Hi Alex Murphy! You have 3 unread messages. Generated at 14:30 | Status: Modern 🚀
5. Core Concepts¶
Variable Placeholders¶
Placeholders are wrapped in braces {variable}. Whitespace inside the braces is automatically trimmed:
$risma = new Risma();
echo $risma->render('Hello { name }!', ['name' => 'Arman']);
// Result: Hello Arman!
Pipeline Chaining (.)¶
Chain functions using dot notation. The output of each function automatically flows as the first parameter of the next:
$template = '{title.trim.lower.title}';
echo $risma->render($template, ['title' => ' ADVANCED PHP ']);
// Result: Advanced Php
Smart Argument Parsing¶
Risma features a custom argument lexer that supports integers, floats, booleans (true/false), null, unquoted identifiers, and quoted strings. It safely handles internal quotes without breaking:
$template = '{@sprintf("%s", "He said "Welcome" to me")}';
echo $risma->render($template);
// Result: He said "Welcome" to me
Argument Routing ($)¶
By default, the piped value is passed as the first argument to a function. When calling a function that expects the target value in a different position (such as PHP's native str_replace($search, $replace, $subject)), use $ as a placeholder for the piped value:
$template = '{slug.str_replace("-", " ", "$")}';
echo $risma->render($template, ['slug' => 'clean-code-in-php']);
// Result: clean code in php
Direct Execution (@)¶
Execute functions directly without requiring a variable placeholder by prefixing the function name with @. You can also chain pipelines onto direct calls:
// Standalone direct execution
echo $risma->render('Year: {@date("Y")}');
// Output: Year: 2026
// Direct execution with chained pipelines
$template = 'PHP {@phpversion.if_gte("8.0", "is modern (%s)", "is legacy (%s)")}';
echo $risma->render($template);
// Output: PHP is modern (8.3.0)
// Direct utility
echo $risma->render('Line 1{@line}Line 2');
// Output: Line 1\nLine 2
Deep Nesting & Recursion¶
Placeholders can be nested inside the arguments of other functions. Risma evaluates expressions recursively from the inside out:
$template = '{@sprintf("Full Name: %s %s", "{@ucfirst("{first}")}", "{@ucfirst("{last}")}")}';
echo $risma->render($template, [
'first' => 'nima',
'last' => 'yushij',
]);
// Output: Full Name: Nima Yushij
Escaping Placeholders (!)¶
Escape any placeholder by prefixing it with an exclamation mark !:
echo $risma->render('Use !{token} to inject your API key.', []);
// Output: Use {token} to inject your API key.
Shared Global Variables (v2)¶
Register application-wide shared variables that persist across multiple renders:
$risma = new Risma();
$risma->share('app_name', 'CloudPulse')
->shareMany([
'support_email' => 'support@cloudpulse.io',
'copyright_year' => 2026,
]);
// Local variables merge with and override shared globals
echo $risma->render('{app_name} — Help: {support_email} (User: {user})', ['user' => 'Ali']);
// Output: CloudPulse — Help: support@cloudpulse.io (User: Ali)
Custom Delimiters (v2)¶
To avoid syntax conflicts with frontend frameworks (like Vue, React, or Blade) or when generating JSON, configure custom opening and closing delimiters:
$risma = new Risma();
$risma->setDelimiters('[:', ':]');
echo $risma->render('Welcome [:user.trim.title:]!', ['user' => 'hadi']);
// Output: Welcome Hadi!
Static Quick Helpers (v2)¶
// Instant one-line render
echo Risma::quick('Hello {user}!', ['user' => 'Sara']);
// Fluent factory instantiation
echo Risma::make()
->share('tenant', 'Acme Corp')
->setDelimiters('[[', ']]')
->render('Tenant: [[tenant]]');
Template Introspection (v2)¶
Extract all variable names used in a template before rendering:
$risma = new Risma();
$vars = $risma->extractVariables('Hi {name}, order #{order_id} is {status.upper}. Server: {@date("Y")}');
// Returns: ['name', 'order_id', 'status'] (ignores direct function calls like @date)
Configurable Recursion Depth (v2)¶
Prevent infinite loops from malicious or recursive user inputs by configuring maximum recursion depth:
Safe Array & Object Handling (v2)¶
When an expression evaluates to an array or non-stringable object, Risma safely converts it into a JSON string instead of triggering PHP notices or fatal errors.
6. Built-in Arsenal (Functions.php)¶
All methods in Nabeghe\Risma\Functions are automatically registered and receive the piped value as their first parameter.
Logic & Conditionals¶
| Helper | Signature | Description |
|---|---|---|
ok |
{val.ok} |
Returns '1' if truthy, '0' if falsy. |
exists |
{val.exists} |
Returns '1' if not null and not empty string "", else '0'. |
or |
{val.or('fallback')} |
Returns fallback value if subject is an empty string. |
coalesce |
{val.coalesce('fb1', 'fb2')} |
Returns the first non-empty value among arguments. |
ternary |
{val.ternary('yes', 'no')} |
Returns 'yes' if truthy, 'no' if falsy. |
and |
{val.and('suffix')} |
Appends suffix only if value is non-empty. |
if_empty |
{val.if_empty('yes', 'no')} |
Checks emptiness; supports %s token for original value. |
if_not_empty |
{val.if_not_empty('yes', 'no')} |
Inverse of if_empty; supports %s. |
if_blank |
{val.if_blank('yes', 'no')} |
Checks for empty or invisible unicode whitespaces (ZWNJ, zero-width chars). |
if_not_blank |
{val.if_not_blank('yes', 'no')} |
Inverse of if_blank. |
if_equals |
{val.if_equals('target', 'yes', 'no')} |
Strict equality comparison. |
if_not_equals |
{val.if_not_equals('target', 'yes', 'no')} |
Inverted equality comparison. |
if_contains |
{val.if_contains('needle', 'yes', 'no')} |
Multibyte substring check. |
if_starts_with |
{val.if_starts_with('prefix', 'yes', 'no')} |
Prefix matching check. |
if_ends_with |
{val.if_ends_with('suffix', 'yes', 'no')} |
Suffix matching check. |
if_numeric |
{val.if_numeric('yes', 'no')} |
Numeric check; formats with %s. |
if_gt |
{val.if_gt('50', 'pass', 'fail')} |
Numeric greater-than comparison (val > 50). |
if_gte |
{val.if_gte('50', 'pass', 'fail')} |
Numeric greater-than-or-equal comparison (val >= 50). |
if_lt |
{val.if_lt('100', 'cheap', 'high')} |
Numeric less-than comparison (val < 100). |
if_lte |
{val.if_lte('100', 'cheap', 'high')} |
Numeric less-than-or-equal comparison (val <= 100). |
Multilingual Pluralization¶
| Helper | Signature | Description |
|---|---|---|
plural |
{count.plural('%s item', '%s items', 'no items')} |
Full multilingual pluralization engine. If count is 0 and 3rd argument is provided, returns the zero form; if count is 1, returns singular; otherwise returns plural. |
maybe_plural_s |
{count.maybe_plural_s} |
Returns 's' if integer > 1, else ''. |
Multibyte UTF-8 Text¶
| Helper | Signature | Description |
|---|---|---|
upper |
{val.upper} |
Multibyte uppercase (mb_strtoupper). |
lower |
{val.lower} |
Multibyte lowercase (mb_strtolower). |
title |
{val.title} |
Multibyte Title Case (mb_convert_case). |
length |
{val.length} |
Multibyte character length (mb_strlen). |
String Manipulation & CMS¶
| Helper | Signature | Description |
|---|---|---|
prepend |
{val.prepend('A', 'B')} |
Prepends prefixes in order. |
append |
{val.append('X', 'Y')} |
Appends suffixes in order. |
wrap |
{val.wrap('<', '>')} |
Wraps text with prefix and suffix. |
replace |
{val.replace('find', 'replace')} |
Direct replacement with value as subject. |
truncate |
{val.truncate(100, '...')} |
Multibyte string truncation. |
slug |
{val.slug('-')} |
Generates URL slug from text. |
mask |
{val.mask(0, 12, '*')} |
Masks sensitive characters (e.g. credit cards or phones). |
flatten_lines |
{val.flatten_lines} |
Replaces \r\n, \n, \r and multiple spaces with a single space. |
remove_lines |
{val.remove_lines} |
Completely strips newline characters. |
line |
{@line} |
Returns raw \n character. |
Arrays & Collections¶
| Helper | Signature | Description |
|---|---|---|
get |
{val.get('key', 'default')} |
Extracts key from array or property from object. |
count |
{val.count} |
Counts array elements or string characters. |
join |
{val.join(', ')} |
Implodes array elements with separator. |
Global PHP Functions¶
Any standard PHP function (trim, strtolower, ucfirst, number_format, md5, sha1, date, sprintf, etc.) is natively supported in the chain:
$template = 'Hash: {password.md5} | Amount: ${price.number_format(2)}';
echo Risma::quick($template, ['password' => 'secret', 'price' => 1250]);
// Output: Hash: 5ebe2294ecd0e0f08eab7690d2a6ee69 | Amount: $1,250.00
7. Extending Risma¶
Custom Functions (addFunc, addFuncs)¶
Register custom functions using closures or callables:
$risma = new Risma();
// Single function (fluent)
$risma->addFunc('currency', function ($amount, $symbol = '$') {
return $symbol . number_format((float) $amount, 2);
});
// Batch registration (fluent)
$risma->addFuncs([
'shout' => fn($val) => $val . '!!!',
'badge' => fn($val, $color = 'blue') => "<span class='badge-{$color}'>{$val}</span>",
]);
echo $risma->render('Total: {price.currency("€")}', ['price' => 49.9]);
// Output: Total: €49.90
Class Registration (addClass, addClasses)¶
Mount classes containing public static methods. All static methods become available inside the template:
class MarkdownHelper {
public static function bold($text) {
return "**{$text}**";
}
}
$risma->addClass(MarkdownHelper::class);
echo $risma->render('Hello {name.bold}!', ['name' => 'Sara']);
// Output: Hello **Sara**!
Object Registration (addObject, addObjects)¶
Mount instantiated objects. Their public instance methods become callable inside the template:
class Formatter {
protected $locale;
public function __construct($locale) { $this->locale = $locale; }
public function localize($text) { return "[{$this->locale}] " . $text; }
}
$risma->addObject(new Formatter('fa_IR'));
echo $risma->render('{message.localize}', ['message' => 'خوش آمدید']);
// Output: [fa_IR] خوش آمدید
Resolution Order¶
When a function name is called in a chain, Risma resolves it in the following order:
- Custom functions (
addFunc) - Registered objects (
addObject) - Registered classes (
addClass— includesNabeghe\Risma\Functions) - Global PHP functions
You can prioritize any registration by passing prepend: true:
8. Engine API Reference¶
| Method | Signature | Description |
|---|---|---|
render |
render(string $text, array $vars = [], bool $default = true): string |
Core template compiler. Renders all placeholders and pipelines. |
quick |
Risma::quick(string $text, array $vars = [], bool $default = true): string |
Static shortcut for instant template rendering. |
make |
Risma::make(): static |
Static factory method for fluent method chaining. |
share |
share(string $key, mixed $value): self |
Stores a shared global variable across renders. |
shareMany |
shareMany(array $vars): self |
Stores multiple shared global variables at once. |
getGlobals |
getGlobals(): array |
Returns all currently registered shared globals. |
clearGlobals |
clearGlobals(): self |
Clears all shared globals. |
setDelimiters |
setDelimiters(string $open, string $close): self |
Sets custom opening and closing delimiters (e.g. [:, :]). |
getDelimiters |
getDelimiters(): array |
Returns the active delimiters as ['open', 'close']. |
extractVariables |
extractVariables(string $text): array |
Extracts all unique variable names referenced in the template. |
setMaxDepth |
setMaxDepth(int $depth): self |
Sets the maximum recursion depth limit (default: 10). |
getMaxDepth |
getMaxDepth(): int |
Returns the current recursion depth limit. |
addFunc |
addFunc(string $name, callable $callback, bool $prepend = false): self |
Registers a custom pipeline function (fluent). |
addFuncs |
addFuncs(array $funcs, bool $prepend = false): self |
Batch-registers multiple custom functions. |
addClass |
addClass(string $className, bool $prepend = false): self |
Mounts a static class into the resolver (fluent). |
addClasses |
addClasses(array $classes, bool $prepend = false): self |
Batch-mounts multiple static classes. |
addObject |
addObject(object $object, bool $prepend = false): self |
Mounts an object instance into the resolver (fluent). |
addObjects |
addObjects(array $objects, bool $prepend = false): self |
Batch-mounts multiple object instances. |
9. Production Recipes¶
Multilingual Notification & Pluralization¶
$template = 'Salam {name}! You have {count} {count.plural("new message", "new messages", "no new messages")}.';
echo Risma::quick($template, ['name' => 'Arman', 'count' => 0]);
// Output: Salam Arman! You have no new messages.
echo Risma::quick($template, ['name' => 'Arman', 'count' => 1]);
// Output: Salam Arman! You have 1 new message.
echo Risma::quick($template, ['name' => 'Arman', 'count' => 7]);
// Output: Salam Arman! You have 7 new messages.
Transactional Email Pipeline¶
$template = 'Order #{id} update for {customer.trim.title}: '
. 'Total: ${total.number_format(2)} | '
. 'Notes: {notes.flatten_lines.if_empty("None", "%s")} | '
. 'VIP: {is_vip.ternary("★ VIP Customer", "Standard Customer")}';
$data = [
'id' => 1042,
'customer' => ' john doe ',
'total' => 149.5,
'notes' => "Please leave package\r\nat front door.",
'is_vip' => true,
];
echo Risma::quick($template, $data);
// Output: Order #1042 update for John Doe: Total: $149.50 | Notes: Please leave package at front door. | VIP: ★ VIP Customer
Dynamic CMS Metadata & Titles¶
$template = '{page_title.trim.title.or("Home")} — {site_name} (© {@date("Y")})';
echo Risma::quick($template, [
'page_title' => 'about our revolutionary platform',
'site_name' => 'TechFlow',
]);
// Output: About Our Revolutionary Platform — TechFlow (© 2026)
10. Testing¶
Risma maintains a 100% passing test suite powered by Pest PHP:
All 162 unit tests and 193 assertions run across PHP 7.4 through PHP 8.4 matrix.