Last active
April 6, 2016 17:35
-
-
Save gmazzap/510b731f4a1ee920b5df to your computer and use it in GitHub Desktop.
Very simple, quite powerful, extensible, plain-PHP one-class template engine.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php namespace GM; | |
/** | |
* Angie. Very simple, and quite powerful, plain PHP template engine. | |
* | |
* @author Giuseppe Mazzapica <[email protected]> | |
* @license http://opensource.org/licenses/MIT MIT | |
*/ | |
class Angie | |
{ | |
/** | |
* @var array | |
*/ | |
private $data; | |
/** | |
* @var bool | |
*/ | |
private $strictVars; | |
/** | |
* @var bool|string | |
*/ | |
private $rendering = false; | |
/** | |
* @var array | |
*/ | |
private $functions = []; | |
/** | |
* @param array $data Pre-defined data | |
* @param bool $strictVars | |
*/ | |
public function __construct(array $data = [], $strictVars = false) | |
{ | |
$this->data = $data; | |
$this->strictVars = $strictVars; | |
} | |
/** | |
* Allow to call registered functions from template files. | |
* | |
* Registered functions can be called in template files using `$this->functionName()` | |
* or the shorter alias `$A->functionName()` | |
* | |
* @param string $name | |
* @param array $args | |
* @return mixed | |
* @see Angie::registerFunction() | |
*/ | |
public function __call($name, array $args) | |
{ | |
if ( ! $this->rendering) { | |
throw new \LogicException( | |
sprintf('%s() function only available when rendering.', $name) | |
); | |
} | |
$exists = array_key_exists($name, $this->functions); | |
if (! $exists && ! function_exists($name)) { | |
throw new \RuntimeException( | |
sprintf('%s is not a registered function.', $name) | |
); | |
} | |
$callable = $exists ? $this->functions[$name] : $name; | |
ob_start(); | |
$return = call_user_func_array($callable, $args); | |
$output = ob_get_clean(); | |
return $output ? : $return; | |
} | |
/** | |
* Allow to call template variables in template files. | |
* | |
* Variables not set are rendered as empty string (no need to check for isset()) unless the | |
* `$strictVars` object var is set to `true` ins constructor. | |
* | |
* @param string $name | |
* @return string | |
*/ | |
public function __get($name) | |
{ | |
if (! $this->rendering) { | |
throw new \LogicException('Variable access is only available when rendering.'); | |
} | |
$exists = array_key_exists($name, $this->data); | |
if (! $exists && $this->strictVars) { | |
throw new \RuntimeException(sprintf('Unresolved var: %s.', $name)); | |
} | |
return $exists ? $this->data[$name] : ''; | |
} | |
/** | |
* Render a template with given data. | |
* | |
* Given data is merged with base data passed to constructor. | |
* | |
* Variable to be replaced with data have to be written using the syntax | |
* (similar to `sprintf` syntax): | |
* | |
* ``` | |
* Hello <b>%user_name$s<b>,<br> | |
* your last access was <span class="last-access">%last_access_days$s ago</span>. | |
* ``` | |
* | |
* Where the variable names (that must match `$data` keys) are: | |
* `user_name` and `last_access_days`. | |
* | |
* This syntax is, of course, available only for string variables. | |
* | |
* This syntax allows to write template without the need to use PHP code, | |
* e.g. just passing arbitrary string. | |
* | |
* However, when using template files, they're regular PHP files that may contain any PHP code. | |
* | |
* To access variables in template files is possible to use `$this->var_name` | |
* or the shorter alias `$A->var_name`. | |
* | |
* In the same way is possible to call Angie methods (e.g. `partial()`) | |
* and all the register functions. | |
* | |
* @param string $template Arbitrary or full path to a template file | |
* @param array $data | |
* @return string | |
*/ | |
public function render($template = '', $data = []) | |
{ | |
$baseData = $this->data; | |
$this->data = array_merge($this->data, $data); | |
$template = $this->maybeFile($template); | |
// replace sprintf-like vars | |
if (preg_match_all('~(?:^|[^\w])(%([a-zA-Z_]+)\$s)(?:\b)~m', $template, $matches)) { | |
$search = array_unique($matches[1]); | |
// muted to avoid double exception when `getReplace()` throws and exception | |
$replace = @array_map([$this, 'getReplace'], array_unique($matches[2])); | |
$template = str_replace($search, $replace, $template); | |
} | |
// reset data so additional calls to `render()` are not affected by template-specific data | |
$this->data = $baseData; | |
return $template; | |
} | |
/** | |
* Inside template files this method can be used to render a partial template. | |
* | |
* Given data is merged with the "parent" template data if `$isolate` argument | |
* is false-ish. | |
* | |
* The method can be called in template files using `$this->partial()` | |
* or the shorter alias `$A->partial()`. | |
* | |
* @param string $partial Full path to a template file | |
* @param array $data | |
* @param bool $isolate | |
* @return string | |
*/ | |
public function partial($partial, array $data = [], $isolate = false) | |
{ | |
if (! is_string($partial)) { | |
throw new \InvalidArgumentException( | |
sprintf('Partial path must be in a string.', $partial) | |
); | |
} | |
if (! $this->rendering) { | |
throw new \LogicException( | |
sprintf('%s only available when rendering.', __METHOD__) | |
); | |
} | |
$instance = clone $this; | |
$instance->data = $isolate ? $data : array_merge($this->data, $data); | |
// try to treat partial as relative path from the folder of the parent | |
if (! is_file($partial)) { | |
$relative = filter_var($partial, FILTER_SANITIZE_URL); | |
$dir = dirname($this->rendering); | |
$partial = $dir.DIRECTORY_SEPARATOR.ltrim($relative, '\\/.'); | |
} | |
if (! is_file($partial)) { | |
throw new \RuntimeException( | |
sprintf('%s is not a valid partial template.', $partial) | |
); | |
} | |
return $instance->render($partial); | |
} | |
/** | |
* Using this method (only available outside template files) is possible | |
* to register arbitrary callbacks that can then be used in template files, | |
* and accessed with `$this->funcName()` or the shorter alias `$A->funcName()`. | |
* | |
* In this way is possible to write expressive and readable templates, | |
* without having to write dozens of global PHP functions or dealing | |
* with PHP objects and namespaces in template files. | |
* | |
* @param string $name | |
* @param callable $function | |
*/ | |
public function registerFunction($name, callable $function) | |
{ | |
if ($this->rendering) { | |
throw new \LogicException( | |
sprintf('%s only available when not rendering.', __METHOD__) | |
); | |
} | |
$this->functions[$name] = $function; | |
} | |
/** | |
* If template is a file, loads it and return the output as template. | |
* If template is an arbitrary string, just returns it. | |
* | |
* @param string $template | |
* @return string | |
*/ | |
private function maybeFile($template) | |
{ | |
if (is_file($template) && is_readable($template)) { | |
$this->rendering = $template; | |
// inside template files the `$A` var is available to call Angie methods | |
// like $A->partial()` or any of the callback registered, | |
// using `registerFunction()` method | |
$A = $this; | |
ob_start(); | |
/** @noinspection PhpIncludeInspection */ | |
require $template; | |
$template = ob_get_clean(); | |
$this->rendering = false; | |
} | |
return $template; | |
} | |
/** | |
* Get the replacement value for variables wrote using sprintf-like syntax. | |
* | |
* @param string $var | |
* @return string | |
*/ | |
private function getReplace($var) | |
{ | |
if ($this->strictVars && ! isset($this->data[$var])) { | |
throw new \RuntimeException(sprintf('Var %s is not set.', $var)); | |
} | |
$replace = isset($this->data[$var]) ? $this->data[$var] : ''; | |
if (is_scalar($replace)) { | |
return (string) $replace; | |
} elseif (is_object($replace) && method_exists($replace, '__toString')) { | |
return $replace->__toString(); | |
} | |
if ($this->strictVars) { | |
throw new \InvalidArgumentException(sprintf('Var %s is not a string.', $var)); | |
} | |
return ''; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment