arillo / silverstripe-merge-fields
Allow-listed merge fields for CMS-editable text, without the template engine.
Package info
github.com/arillo-ch/silverstripe-merge-fields
Type:silverstripe-vendormodule
pkg:composer/arillo/silverstripe-merge-fields
Requires
- php: ^7.4 || ^8.0
- silverstripe/framework: ^4 || ^5 || ^6
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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 whenFirstNameis 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:\TempasC:Tempand\$Fooas$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 throughConvert::raw2xml()..RAWoutput 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.
$extrascalars only go throughConvert::raw2xml, while the engine casts record values (by default asText, which turns newlines into<br />). Pass aDBFieldin$extrato get field-type behaviour..Formaton 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.