Search by

arillo / silverstripe-merge-fields

Arillo

Allow-listed merge fields for CMS-editable text, without the template engine.

Package info

github.com/arillo-ch/silverstripe-merge-fields

Homepage

Issues

Type:silverstripe-vendormodule

pkg:composer/arillo/silverstripe-merge-fields

Statistics

Installs: 23

Dependents: 0

Suggesters: 0

Stars: 0

1.0.0 2026-11-04 08:11 UTC

This package is auto-updated.

Last update: 2026-11-04 12:45:01 UTC


README

Allow-listed merge fields for CMS-editable text on Silverstripe 4, 5 and 6, without the template engine.

Why

Text that editors type in the CMS must not be handed to the template engine. The engine treats that text as code, so anything an editor types that looks like template code would be interpreted as template code. This helper only looks at the text and looks up values. It never generates or runs code, and it never calls the template engine, eval or include.

You pass in the text, the data (a record or an array) and an explicit list of allowed variables. Only exact entries on that list are replaced. Everything else stays in the output as plain text.

Installation

composer require arillo/silverstripe-merge-fields

Requires PHP 7.4 or newer and silverstripe/framework ^4 || ^5 || ^6. There are no other dependencies.

Usage

use Arillo\MergeFields\MergeFields;

$variables = ['FirstName', 'JobPage.Title', 'Message.RAW', 'DeliveryDate'];

// Render the editor text
$html = MergeFields::render(
    $this->EmailContent,        // text from the CMS, may be null
    $submission,                // a record or an array
    $variables,                 // allowed names, exact entries only
    ['SiteName' => $siteTitle], // optional extra named values
    ['Strip' => function ($url) { return strtok($url, '?'); }] // optional functions
);

// Show the same list to editors next to the field
$hint = 'Verfügbare Variablen: ' . MergeFields::hint($variables, $submission);

render() returns an HTML-safe string that is already escaped, so do not escape it again: passing it through Convert::raw2xml or a Varchar cast double-escapes it. Where a template outputs the result without .RAW, wrap it as an HTMLFragment (DBField::create_field('HTMLFragment', $html)).

hint() returns only the tokens, for example $FirstName, $JobPage.Title, $Message.RAW, $DeliveryDate.Format('dd.MM.y'). Pass the data as the second argument so date and time entries show their .Format('dd.MM.y') form. The call site adds its own label.

Use one list for both render() and hint(), so they cannot drift apart.

Token rules

Form Example Replaced when
Variable $FirstName, {$FirstName} That exact name is on the list
Path $JobPage.Title That full path is on the list
Raw $Message.RAW The list has that entry including .RAW
Date format $DeliveryDate.Format('dd.MM.y') DeliveryDate is listed, its value is a date or time, and the pattern only has letters, digits, spaces and . : - /
Date format entry $DeliveryDate.Format('y') The list has DeliveryDate.Format('dd.MM.y') written out. Any valid pattern works on that path, a plain $DeliveryDate stays text, and hint() shows the entry as written
Function $Strip($Link) The call site registered the function, and the arguments are only listed variables or plain quoted text
Anything else $delete(), $FirstName('x'), <% if %>, $Unknown Never. It stays in the output as plain text

Behaviour:

  • Single pass. Substituted values are never scanned again.
  • The list is explicit, with no wildcards. Only the entries you list can be used.
  • A token with a parenthesis directly after the name is a call. It is never replaced as a variable, so $FirstName('x') stays as written even when FirstName is listed.
  • A backslash directly before the $ keeps the token as written.
  • Known differences from the template engine: all three engine parsers strip backslashes from plain text, so the engine renders C:\Temp as C:Temp and \$Foo as $Foo. The helper keeps backslashes as typed.
  • A listed variable with no value renders empty.
  • Values are escaped as the template engine would: a database field through forTemplate(), anything else through Convert::raw2xml(). .RAW output is not escaped, so make sure the getter escapes visitor-typed text.
  • Models and other objects that offer relations keep their token. Only scalars, database fields and string-convertible objects are substituted.
  • $extra scalars only go through Convert::raw2xml, while the engine casts record values (by default as Text, which turns newlines into <br />). Pass a DBField in $extra to get field-type behaviour.
  • .Format on a listed value that is not a date or time keeps the token as text.
  • Extra named values are allowed by their key, and are the way to supply values that are not on the record, such as site settings.
  • A null text renders empty.
  • Block tags such as <% if %> are left alone, not stripped.

Running the tests

The tests run inside a host Silverstripe project. Copy or install the package under vendor/arillo/silverstripe-merge-fields/, then from the project root:

vendor/bin/phpunit \
  --bootstrap vendor/silverstripe/framework/tests/bootstrap.php \
  vendor/arillo/silverstripe-merge-fields/tests/php

With ddev, run it through ddev exec. The test database credentials can come from SS_DATABASE_USERNAME and SS_DATABASE_PASSWORD.

License

MIT, see License.md.