Skip to content

Risma PHP Documentation

A lightweight, zero-dependency string template engine and interpreter for PHP >= 7.4.
The logic lives inside the string itself.

Version PHP License Dependencies Tests Pest


📑 Table of Contents


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:

composer require nabeghe/risma

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:

$risma->setMaxDepth(5); // Default is 10
echo $risma->getMaxDepth(); // 5

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:

  1. Custom functions (addFunc)
  2. Registered objects (addObject)
  3. Registered classes (addClass — includes Nabeghe\Risma\Functions)
  4. Global PHP functions

You can prioritize any registration by passing prepend: true:

$risma->addFunc('trim', fn($val) => 'custom_trimmed', 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:

composer test
# or:
vendor/bin/pest

All 162 unit tests and 193 assertions run across PHP 7.4 through PHP 8.4 matrix.