Skip to content

Commit

Permalink
Allow top level callbacks as well as direct access to PHP methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Enno Woortmann committed Sep 5, 2023
1 parent 93dc8de commit 8acee68
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 13 deletions.
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ A minimalistic, lightweight templating engine for PHP based on regular expressio
- Replace variables inside a template
- Iterate over an array or iterable object
- Conditional sections
- Pass objects and call functions on the objects
- Pass objects
- call functions

## Requirements ##

Expand Down Expand Up @@ -215,7 +216,7 @@ Multiple if statements can be nested. To invert an if condition the keyword *not

### function calls

The methods which are called on assigned objects can take parameters.
The methods which are called can take parameters.
Allowed parameters are variables taken out of the current scope or another function call on an object available in the current scope as well as the supported constant values integer numbers, strings in single quotes and booleans (true, false).
As an example a ViewHelper-Object can be assigned to the render process and methods of the ViewHelper can be used in the template for advanced logic inside the template.

Expand Down Expand Up @@ -283,6 +284,37 @@ $result = $render->renderTemplate(
</html>
```

Additionally, PHP global functions can be used directly in the template as well as assigned callback methods:

```php
<?php

use PHPMicroTemplate\Render;

/* ... */

$render = new Render(__DIR__ . '/Templates/');

$result = $render->renderTemplate(
'functionExample.template',
[
'customCallback' => function(string $in): string {
return trim(strtoupper($in));
},
]
);

/* ... */

```

```html
<html>
<p>{{ customCallback('products') }}</p>
<span>{{ strtolower('UNDER CONSTRUCTION') }}</span>
</html>
```

### Whitespace tolerance

The templating syntax is whitespace tolerant so a template like the one below would be perfectly fine:
Expand Down
62 changes: 51 additions & 11 deletions src/Render.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
class Render
{
private const REGEX_VARIABLE = '(?<expression>(?<variable>(\w+|\'[^\']+\'))(?<nestedVariable>(\.\w+)*?)(\.(?<method>\w+)\((?<parameter>[^{}%]*)\))?)';
private const REGEX_VARIABLE = '(?<expression>(?<variable>(\w+|\'[^\']+\'))(?<nestedVariable>(\.\w+)*)(?<methodCall>\((?<parameter>[^{}%]*)\))?)';

/** @var array */
private $templates = [];
Expand Down Expand Up @@ -252,28 +252,64 @@ protected function getValue(array $matches, array $variables)
}
}

if (!$this->resolveNestedVariable($resolved, $variablePath, $matches) || empty($matches['method'])) {
if (empty($matches['methodCall'])) {
$this->resolveNestedVariable($resolved, $variablePath, $matches);

return $resolved;
}

if (!is_object($resolved)) {
throw new UndefinedSymbolException(
sprintf('Trying to call %s on non-object %s', $matches['method'], implode('.', $variablePath))
return $this->methodCall($matches, $variablePath, $variables);
}

/**
* @param array $matches
* @param array $variablePath
* @param array $variables
*
* @return mixed
*
* @throws SyntaxErrorException
* @throws UndefinedSymbolException
*/
private function methodCall(array $matches, array $variablePath, array $variables)
{
$resolved = $variables;
$method = array_pop($variablePath);

if (empty($variablePath)) {
$resolvedMethod = array_key_exists($method, $variables) ? $variables[$method] : $method;

if (!is_callable($resolvedMethod)) {
throw new UndefinedSymbolException(sprintf('Function %s not callable', $method));
}

return call_user_func_array(
$resolvedMethod,
$this->extractParameter($matches['parameter'] ?? '', $variables)
);
}

if (!is_callable([$resolved, $matches['method']])) {
if (!$this->resolveNestedVariable($resolved, $variablePath, $matches)) {
// resolve error callback result
return $resolved;
}

if (!is_object($resolved)) {
throw new UndefinedSymbolException(
sprintf('Function %s on object %s not callable', $matches['method'], implode('.', $variablePath))
sprintf('Trying to call %s on non-object %s', $method, implode('.', $variablePath))
);
}

// check if the function to call has a given parameter. In this case resolve the parameter
if (!empty($matches['parameter'])) {
$parameter = $this->extractParameter($matches['parameter'], $variables);
if (!is_callable([$resolved, $method])) {
throw new UndefinedSymbolException(
sprintf('Function %s on object %s not callable', $method, implode('.', $variablePath))
);
}

return call_user_func_array([$resolved, $matches['method']], $parameter ?? []);
return call_user_func_array(
[$resolved, $method],
$this->extractParameter($matches['parameter'] ?? '', $variables)
);
}

/**
Expand Down Expand Up @@ -390,6 +426,10 @@ protected function getControlStructureRegEx(string $structure, array $additional
*/
protected function extractParameter(string $parameter, array $variables): array
{
if (empty($parameter)) {
return [];
}

$result = preg_match(
'/^\s*' . self::REGEX_VARIABLE . '(\s*,\s*(?<next>.+))?\s*$/is',
$parameter,
Expand Down
26 changes: 26 additions & 0 deletions tests/RenderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,32 @@ public function testConstantExpressionsAsFunctionParameter(): void
);
}

public function testBuiltinFunctionCall(): void
{
$this->assertSame('HELLO WORLD', $this->render->renderTemplateString("{{ strtoupper('hello world') }}"));
}

/**
* @dataProvider topLevelFunctionCallDataProvider
*/
public function testTopLevelFunctionCall(callable $callback): void
{
$this->assertSame(
'HELLO WORLD',
$this->render->renderTemplateString("{{ up('hello world') }}", ['up' => $callback])
);
}

public function topLevelFunctionCallDataProvider()
{
return [
'builtin' => ['strtoupper'],
'closure' => [function ($i) { return strtoupper($i); } ],
'object method' => [[new ViewHelper(), 'up']],
'static method' => [[ViewHelper::class, 'up']],
];
}

public function propertyDataProvider(): array
{
return [
Expand Down
4 changes: 4 additions & 0 deletions tests/ViewHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ public function double(int $number): int
{
return $number * 2;
}

public static function up(string $string): string {
return strtoupper($string);
}
}

0 comments on commit 8acee68

Please sign in to comment.