0

I'm designing a simple templating system for a CMS in PHP which internally currently uses something like:

require_once 'templates/template1.php`;

to import the desired template.

I would like every content {{field123}} in this PHP file to be automatically converted into <?php echo $row['field123']; ?> before being passed into require_once and executed by PHP.

Is there a way to activate a preprocessor (I know that PHP is already named after preprocessor) that does this replacement {{anything}} -> <?php echo $row['anything']; ?> before executing the PHP code template1.php? If not, what's the usual way to do this?

Basj
  • 41,386
  • 99
  • 383
  • 673
  • You can use regular expressions to search for that format and replace them with the results of a custom function instead. Check out [preg_replace_callback()](https://www.php.net/manual/en/function.preg-replace-callback.php). However, is there a reason why you're reinventing the wheel? There already are plenty of tried and tested templating engines you can use which has this, and much more, functionality. Or is it simply for practice? – M. Eriksson Jan 25 '22 at 15:39
  • @M.Eriksson it's for a minimal templating for a domain-specific thing. – Basj Jan 25 '22 at 15:43
  • @M.Eriksson But how to replace things in a PHP file, and then `require_once` the file to execute it? I don't want to *actually* modify the original PHP file, but just load it in memory, do the replacement, and execute it. How would you do that? – Basj Jan 25 '22 at 15:47
  • You can return the contents of an include into a variable instead: `$template = include 'template.php';`. Then you can run your preg_replace_callback() on that and just echo the output. Otherwise, you can start [output buffering](https://www.php.net/manual/en/ref.outcontrol.php), include the file and then get the result in a variable, which you do the same with. Output buffering is a common solution in cases like this. So it would be executing the file, then replace the placeholders. You just don't output it when you execute it. – M. Eriksson Jan 25 '22 at 15:52
  • 1
    Templating into PHP source code and then running that code is very dangerous. The "usual way to do this" would be to template into a string and then echo the string. – Alex Howansky Jan 25 '22 at 16:02
  • @M.Eriksson Your last solution seems the best, execute the PHP first, get the result in a variable, and then do a preg_replace. Which functions would you use to get the result of a HTML / PHP output into a variable? Would you use `ob_start();` and then `ob_end(); $result = ob_get_flush();`? I think you can post your solution as an answer. – Basj Jan 25 '22 at 16:07
  • Does this answer your question? [How do I capture PHP output into a variable?](https://stackoverflow.com/questions/171318/how-do-i-capture-php-output-into-a-variable) – M. Eriksson Jan 25 '22 at 16:15
  • @M.Eriksson It's linked, but I think an answer would be interesting in the context of requiring another PHP file, as some corner cases could appear here. – Basj Jan 25 '22 at 16:21
  • @M.Eriksson I posted an answer (which I can convert into community wiki to avoid rep), do you know how to preg_replace to automatically replace `{{field123}}` into `row['field123']` ? If so, feel free to edit it. – Basj Jan 25 '22 at 16:26
  • maybe have a look at Twig and see how it's done there (or just use it)? – Dirk J. Faber Jan 25 '22 at 17:04
  • Do your template files have actual PHP code as well? There are a couple of ways to go about this, but it really depends on what all the templates have, including whether they have echos in them. – Markus AO Jan 25 '22 at 17:39
  • @MarkusAO In my examples, template files are mostly HTML + `{{fields}}`, but I want to be able to have some little PHP code in the future too. If it was 100% pure HTML, what solution would you use? `$s = file_get_contents('template.html'); $s = preg_replace_callback(..., ..., $s);`, or something else? – Basj Jan 26 '22 at 07:33
  • I personally keep PHP clean out of templates. You can substitute a straight-up variable or the result of a function all the same. For example, have a set of methods that can be called, and have your template parser handle `{{func:get_stuff}}`, in addition to one-to-one variable substitution. If you wanted "proper PHP" in there, you could e.g. do an `include` contained in a function scope, to which you feed your variables. ```function foo(array $vars, string $template) { ///and include and run your ob() etc. or whatever in here }```, you wouldn't even have to tokenize the vars you feed in. – Markus AO Jan 26 '22 at 08:22
  • @Basj so I indulged 10 minutes worth, please see the answer. That's _basically_ how I do it, though my homebrew framework rig is a bit (lot) more extensive as far as what you can toss in and get parsed. The basics below should be easy enough to extend for your needs. – Markus AO Jan 26 '22 at 12:08

2 Answers2

1

Having PHP code in templates - especially code with potential side-effects - can get dirty real quick. I would recommend using static templates, treating them as strings instead of executing them, then parsing them for tokens, with your main application compiling them and handling output.

Here is a rudimentary implementation that parses variables into tokens, and also handles mapped function calls in your templates. First, "fetching" our template (for a simple example):

$tpl = 'This is a sample template file. 
It can have values like {{foo}} and {{bar}}. 
It can also invoke mapped functions: 
{{func:hello}} or {{func:world}}. 
Hello user {{username}}. Have a good day!';

Then, the template parser:

function parse_template(string $tpl, array $vars): string {
    
    // Catch function tokens, handle if handler exists:
    $tpl = preg_replace_callback('~{{func:([a-z_]+)}}~', function($match) {
        $func = 'handler_' . $match[1];
        if(function_exists($func)) {
            return $func();
        }
        return "!!!What is: {$match[1]}!!!";
    }, $tpl);
    
    // Generate tokens for your variable keys;
    $keys = array_map(fn($key) => '{{' . $key . '}}', array_keys($vars));

    // Substitute tokens:
    $tpl = str_replace($keys, $vars, $tpl);
    
    return $tpl;
}

These are our handler functions, with handler_X matching {{func:X}}.

function handler_hello() {
    return 'HELLO THERE';
}
function handler_world() {
    return '@Current World Population: ' . mt_rand();
}

Then, here are the variables you'd like to parse in:

$vars = [
    'foo' => 'Food',
    'bar' => 'Barnacle',
    'username' => 'Herbert'
];

Now let's parse our template:

$parsed = parse_template($tpl, $vars);

echo $parsed;

This results in:

This is a sample template file. 
It can have values like Food and Barnacle. 
It can also invoke mapped functions: 
HELLO THERE or @Current World Population: 1477098027. 
Hello user Herbert. Have a good day!

Job done. You really don't need a complicated templating engine for something like this. You could easily extend this to allow the handlers to receive arguments defined in the template tokens -- however I'll leave that for your homework part. This should do to demonstrate the concept.

Markus AO
  • 4,771
  • 2
  • 18
  • 29
0

As mentioned in a comment and in How do I capture PHP output into a variable?, the use of output buffering can work:

<?php
ob_start();
?>

Hello
{{field123}} and {{field4}}    
World
<?php // or require_once 'template1.php'; ?>

<?php
$s = ob_get_clean();
$a = array('field123' => 'test', 'field4' => 'test2');
$s = preg_replace_callback('/{{(.*?)}}/', function ($m) use ($a) { return isset($a[$m[1]]) ? $a[$m[1]] : $m[0]; }, $s);
echo $s;
?>

// Output:
// Hello
// test and test2    
// World

Here we also used a method similar to Replace with dynamic variable in preg_replace to do the replacement.

Basj
  • 41,386
  • 99
  • 383
  • 673