diff options
-rw-r--r-- | php/composer.json | 2 | ||||
-rw-r--r-- | php/src/Template.php | 604 | ||||
-rw-r--r-- | php/tests/TemplateTest.php | 16 |
3 files changed, 601 insertions, 21 deletions
diff --git a/php/composer.json b/php/composer.json index 9cf4a1e..f0bdb51 100644 --- a/php/composer.json +++ b/php/composer.json @@ -22,7 +22,7 @@ "autoload": { "psr-4": { - "Luigi\\": "src/" + "Pablotron\\Luigi\\": "src/" } }, diff --git a/php/src/Template.php b/php/src/Template.php index da905d7..34dfe09 100644 --- a/php/src/Template.php +++ b/php/src/Template.php @@ -1,16 +1,51 @@ <?php -declare(strict_types = 1); +/** + * Fast string templating library for JavaScript, PHP, Ruby, and Java. + * + * @author Paul Duncan <pabs@pablotron.org> + * @copyright 2010-2018 Paul Duncan <pabs@pablotron.org> + * @license MIT + * @package Pablotron\Luigi + */ -namespace Luigi; +declare(strict_types = 1); +/** + * Luigi Template namespace. + * + * @api + */ +namespace Pablotron\Luigi; + +/** + * Current version of Luigi Template. + * + * @api + */ const VERSION = '0.4.2'; -class Error extends \Exception {}; - -class UnknownTypeError extends Error { +/** + * Base class for all exceptions raised by Luigi Template. + */ +class LuigiError extends \Exception {}; + +/** + * Base class for all all unknown type errors. + */ +class UnknownTypeError extends LuigiError { + /** + * @var string $type Unknown item type name ("method", "template", etc). + * @var string $name Unknown item name. + */ public $type, $name; + /** + * Create a new UnknownTypeError error. + * + * @param string $type Unknown item type name. + * @param string $name Unknown item name. + */ public function __construct(string $type, string $name) { $this->type = $type; $this->name = $name; @@ -18,61 +53,153 @@ class UnknownTypeError extends Error { } }; +/** + * Thrown when attempting to get an unknown template from a Cache. + * + * @see Cache + */ final class UnknownTemplateError extends UnknownTypeError { + /** + * Create a new UnknownTemplateError. + * + * @param string $name Unknown template name. + */ public function __construct(string $name) { parent::__construct('template', $name); } }; +/** + * Thrown when attempting to apply an unknown filter. + */ final class UnknownFilterError extends UnknownTypeError { + /** + * Create a new UnknownFilterError. + * + * @param string $name Unknown filter name. + */ public function __construct(string $name) { parent::__construct('filter', $name); } }; +/** + * Thrown when attempting to get an unknown key. + */ final class UnknownKeyError extends UnknownTypeError { + /** + * Create a new UnknownKeyError. + * + * @param string $name Unknown key name. + */ public function __construct(string $name) { parent::__construct('key', $name); } }; -final class MissingFilterParameterError extends Error { +/** + * Thrown when trying to use a filter with with a missing parameter. + */ +final class MissingFilterParameterError extends LuigiError { + /** @var string $filter_name Name of filter. */ public $filter_name; + /** + * Create a new MissingFilterParameterError. + * + * @param string $filter_name Name of filter. + */ public function __construct(string $filter_name) { $this->filter_name = $filter_name; parent::__construct("missing required filter parameter for filter $filter_name"); } }; -final class InvalidTemplateError extends Error { +/** + * Thrown when trying to parse an invalid template string. + */ +final class InvalidTemplateError extends LuigiError { + /** @var string $template Template string. */ public $template; + /** + * Create a new InvalidTemplateError. + * + * @param string $template Template string. + */ public function __construct(string $template) { $this->template = $template; parent::__construct("invalid template: $template"); } }; +/** + * Wrapper for context during Template#run(). + * + * @internal + */ final class RunContext { + /** + * @var array $args Hash of arguments. + * @var array $filters Hash of filters. + */ public $args, $filters; +/** + * Create a RunContext. + * + * @internal + * + * @param array $args Arguments hash. + * @param array $filters Filters hash. + */ public function __construct(array $args, array $filters) { $this->args = $args; $this->filters = $filters; } }; +/** + * Parsed filter name and arguments. + * + * @internal + */ final class TemplateFilter { + /** + * @var string $name Filter name. + * @var array $args Filter arguments. + */ private $name, $args; + /** + * Create a TemplateFilter. + * + * @internal + * + * @param string $name Filter name. + * @param array $args Filter arguments. + */ public function __construct(string $name, array $args) { $this->name = $name; $this->args = $args; } + /** + * Run filter on given value, arguments, and filter set. + * + * @internal + * + * @param string $v Input value. + * @param array $args Hash passed to Template#run(). + * @param array $filters Hash of filters. + * + * @return mixed Filter result. + * + * @throws UnknownFilterError If this filter does not exist in filter + * hash. + */ public function run($v, array &$args, array &$filters) { if (!isset($filters[$this->name])) { throw new UnknownFilterError($this->name); @@ -86,32 +213,97 @@ final class TemplateFilter { } }; - +/** + * Abstract base class for parser tokens. + * + * @internal + */ abstract class Token { + /** + * Apply this token. + * + * @internal + * + * @param RunContext $ctx Run context. + * + * @return string + */ public abstract function run(RunContext &$ctx) : string; }; +/** + * Literal parser token. + * + * @internal + */ final class LiteralToken extends Token { + /** @var string $val Literal value. */ private $val; + /** + * Create a new LiteralToken. + * + * @internal + * + * @param string $val Literal string. + */ public function __construct(string $val) { $this->val = $val; } + /** + * Returns the literal value. + * + * @internal + * + * @param RunContext $ctx Run context. + * + * @return string Literal value. + */ public function run(RunContext &$ctx) : string { return $this->val; } }; +/** + * Filter parser token. + * + * @internal + */ final class FilterToken extends Token { + /** + * @var string $key Argument name. + * @var array $filters Array of TemplateFilter instances. + */ private $key, $filters; + /** + * Create a new LiteralToken. + * + * @internal + * + * @param string $key Argument name. + * @param array $filters Array of TemplateFilter instances. + */ public function __construct(string $key, array $filters) { $this->key = $key; $this->filters = $filters; } + /** + * Get key from arguments hash and apply filters to it, then return + * the result. + * + * @internal + * + * @param RunContext $ctx Run context. + * + * @return string Filtered result. + * + * @throws UnknownKeyError If the given key does not exist in the + * arguments hash. + */ public function run(RunContext &$ctx) : string { # check key if (!isset($ctx->args[$this->key])) { @@ -133,6 +325,11 @@ final class FilterToken extends Token { } }; +/** + * Token parser regular expression. + * + * @internal + */ const TOKEN_RE = '/ # match opening brace %\{ @@ -156,6 +353,11 @@ const TOKEN_RE = '/ | (?<text>[^%]* | %) /mx'; +/** + * Filter parser regular expression. + * + * @internal + */ const FILTER_RE = '/ # match filter name (?<name>\S+) @@ -167,10 +369,32 @@ const FILTER_RE = '/ \s* /mx'; +/** + * Filter delimiter regular expression. + * + * @internal + */ const DELIM_FILTERS_RE = '/\s*\|\s*/m'; +/** + * Filter argument delimiter regular expression. + * + * @internal + */ const DELIM_ARGS_RE = '/\s+/m'; +/** + * Parse a string containing a filter clause into an array of + * TemplateFilter instances. + * + * @internal + * + * @param string $filters Input filter string. + * + * @return array Array of TemplateFilter instances. + * + * @throws UnknownFilterError if a filter clause could not be parsed. + */ function parse_filters(string $filters) : array { # split into individual filters $r = []; @@ -200,6 +424,17 @@ function parse_filters(string $filters) : array { return $r; } +/** + * Parse a template string into an array of Token instances. + * + * @internal + * + * @param string $template Input template string. + * + * @return array Array of Token instances. + * + * @throws InvalidTemplateError if the template could not be parsed. + */ function parse_template(string $template) : array { # build list of matches $matches = []; @@ -226,9 +461,64 @@ function parse_template(string $template) : array { }, []); } +/** + * Static class containing global filter map. + * + * The built-in default filters are: + * + * * `h`: HTML-escape input string (including quotes). + * * `u`: URL-escape input string. + * * `json`: JSON-encode input value. + * * `hash`: Hash input value. Requires hash algorithm as parameter. + * * `base64`: Base64-encode input string. + * * `nl2br`: Convert newlines to `\<br/\>` elements. + * * `uc`: Upper-case input string. + * * `lc`: Lower-case input string. + * * `trim`: Trim leading and trailing whitespace from input string. + * * `ltrim`: Trim leading whitespace from input string. + * * `rtrim`: Trim trailing whitespace from input string. + * * `s`: Return '' if input number is 1, and 's' otherwise (used for + * pluralization). + * * `strlen`: Return the length of the input string. + * * `count`: Return the number of elements in the input array. + * * `key`: Get the given key from the input array. Requires key as a + * parameter. + * + * You can add your own filters to the default set of filters by + * modifying `\Luigi\Filters::$FILTERS`, like so: + * + * # add a filter named "my-filter" + * \Luigi\Filters['my-filter'] = function($s) { + * # filter input string + * return "foo-{$s}-bar"; + * }; + * + * # use custom filter in template + * echo Template::once('%{some-key | my-filter}', [ + * 'some-key' => 'example', + * ]) . "\n"; + * + * # prints "foo-example-bar" + * + * @api + */ class Filters { + /** + * @var array $FILTERS Global filter map. + * + * @api + */ public static $FILTERS = null; + /** + * Initialize global filter map. + * + * Called internally by LuigiTemplate. + * + * @internal + * + * @return void + */ public static function init() : void { # prevent double initialization if (self::$FILTERS !== null) @@ -315,13 +605,62 @@ class Filters { } }; +# initialize filters Filters::init(); +/** + * Template object. + * + * Parse a template string into a Template instance, and then apply the + * Template via the Template#run() method. + * + * Example: + * + * # load template class + * use \Pablotron\Luigi\Template; + * + * # create template + * $tmpl = new Template('hello %{name}'); + * + * # run template and print result + * echo $tmpl->run([ + * 'name' => 'Paul', + * ]) . "\n"; + * + * # prints "hello Paul" + * + * @api + * + */ final class Template { - private $template, - $filters, + /** @var string $template Input template string. */ + public $template; + + /** + * @var array $filters Filter map. + * @var array $tokens Parsed template tokens. + */ + private $filters, $tokens; + /** + * Create a new Template object. + * + * Parse a template string into tokens. + * + * Example: + * + * # load template class + * use \Pablotron\Luigi\Template; + * + * # create template + * $tmpl = new Template('hello %{name}'); + * + * @api + * + * @param string $template Template string. + * @param array $custom_filters Custom filter map (optional). + */ public function __construct( string $template, array $custom_filters = [] @@ -333,6 +672,33 @@ final class Template { $this->tokens = parse_template($template); } + /** + * Apply given arguments to template and return the result as a + * string. + * + * Example: + * + * # load template class + * use \Pablotron\Luigi\Template; + * + * # create template + * $tmpl = new Template('hello %{name}'); + * + * # run template and print result + * echo $tmpl->run([ + * 'name' => 'Paul', + * ]) . "\n"; + * + * # prints "hello Paul" + * + * @api + * + * @param array $args Template arguments. + * + * @return string Expanded template. + * + * @throws UnknownKeyError If a referenced key is missing from $args. + */ public function run(array $args = []) : string { # create run context $ctx = new RunContext($args, $this->filters); @@ -342,6 +708,53 @@ final class Template { }, $this->tokens)); } + /** + * Return the input template string. + * + * Example: + * + * # load template class + * use \Pablotron\Luigi\Template; + * + * # create template + * $tmpl = new Template('hello %{name}'); + * + * # print template string + * echo $tmpl . "\n"; + * + * # prints "hello %{name}" + * + * @api + * + * @return string Input template string. + */ + public function __toString() : string { + return $this->template; + } + + /** + * Parse template string, expand it using given arguments, and return + * the result as a string. + * + * Example: + * + * # load template class + * use \Pablotron\Luigi\Template; + * + * # create template, run it, and print result + * echo Template::once('foo-%{some-key}-bar', [ + * 'some-key' => 'example', + * ]) . "\n"; + * + * # prints "foo-example-bar" + * @api + * + * @param string $template Template string. + * @param array $args Template arguments. + * @param array $filters Custom filter map (optional). + * + * @return string Expanded template. + */ public static function once( string $template, array $args = [], @@ -352,21 +765,107 @@ final class Template { } }; +/** + * Lazy-loading template cache. + * + * Group a set of templates together and only parse them on an as-needed + * basis. + * + * @api + */ final class Cache implements \ArrayAccess { + /** + * @var array $templates Map of keys to template strings. + * @var array $filters Filter map (optional). + * @var array $lut Parsed template cache. + */ private $templates, $filters, $lut = []; + /** + * Create a new template cache. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create template cache + * $cache = new Cache([ + * 'hi' => 'hi %{name}!', + * ]); + * + * @api + * + * @param array $templates Map of template keys to template strings. + * @param array $filters Custom filter map (optional). + */ public function __construct(array $templates, array $filters = []) { $this->templates = $templates; $this->filters = (count($filters) > 0) ? $filters : Filters::$FILTERS; } + /** + * Returns true if the given template key exists in this cache. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create cache + * $cache = new Cache([ + * 'hi' => 'hi %{name}!', + * ]); + * + * # get template 'hi' from cache + * foreach (array('hi', 'nope') as $tmpl_key) { + * echo "$key: " . (isset($cache[$key]) ? 'Yes' : 'No') . "\n" + * } + * + * # prints "hi: Yes" and "nope: No" + * @api + * + * @param string $key Template key. + * + * @return bool Returns true if the template key exists. + */ public function offsetExists($key) : bool { return isset($this->templates[$key]); } - public function offsetGet($key) { + /** + * Get the template associated with the given template key. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create cache + * $cache = new Cache([ + * 'hi' => 'hi %{name}!', + * ]); + * + * # get template 'hi' from cache + * $tmpl = $cache['hi']; + * + * echo $tmpl->run([ + * 'name' => 'Paul', + * ]); + * + * # prints "hi Paul!" + * + * @api + * + * @param string $key Template key. + * + * @return Template Returns template associated with this key. + * + * @throws UnknownTemplateError if the given template does not exist. + */ + public function offsetGet($key) : Template { if (isset($this->lut[$key])) { return $this->lut[$key]; } else if (isset($this->templates[$key])) { @@ -377,17 +876,98 @@ final class Cache implements \ArrayAccess { } } + /** + * Remove the template associated with the given template key. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create cache + * $cache = new Cache([ + * 'hi' => 'hi %{name}!', + * ]); + * + * # remove template 'hi' from cache + * unset($cache['hi']); + * + * echo $cache['hi']->run([ + * 'name' => 'Paul', + * ]); + * + * # throws UnknownTemplateError + * + * @api + * + * @param array $key Template key. + * + * @return void + */ public function offsetUnset($key) : void { unset($this->lut[$key]); unset($this->templates[$key]); } + /** + * Set the template associated with the given template key. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create cache + * $cache = new Cache(); + * + * # add template to cache as 'hash-name' + * $cache['hash-name'] = 'hashed name: %{name | hash sha1}'; + * + * echo $cache['hash-name']->run([ + * 'name' => 'Paul', + * ]); + * + * # prints "sha1 of name: c3687ab9880c26dfe7ab966a8a1701b5e017c2ff" + * + * @api + * + * @param string $key Template key. + * @param string $val Template string. + * + * @return void + */ public function offsetSet($key, $val) : void { unset($this->lut[$key]); $this->templates[$key] = $val; } - + /** + * Run template with the given arguments and return the result. + * + * Example: + * + * # load cache class + * use \Pablotron\Luigi\Cache; + * + * # create cache + * $cache = new Cache([ + * 'my-template' => 'hello %{name | uc}', + * ]); + * + * # run template + * echo $cache->run('my-template', [ + * 'name' => 'Paul', + * ]); + * + * # prints "hello PAUL" + * + * @api + * + * @param string $key Template key. + * @param array $args Template arguments. + * + * @return string Returns the expanded template result. + */ public function run(string $key, array $args = []) : string { return $this->offsetGet($key)->run($args); } diff --git a/php/tests/TemplateTest.php b/php/tests/TemplateTest.php index 18e5a6e..b6e72c9 100644 --- a/php/tests/TemplateTest.php +++ b/php/tests/TemplateTest.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace Pablotron\Luigi\Tests; use \PHPUnit\Framework\TestCase; -use \Luigi\Template; +use \Pablotron\Luigi\Template; final class TemplateTest extends TestCase { public function testNewTemplate() : void { @@ -89,12 +89,12 @@ final class TemplateTest extends TestCase { } public function testCustomGlobalFilter() : void { - \Luigi\Filters::$FILTERS['barify'] = function($s) { + \Pablotron\Luigi\Filters::$FILTERS['barify'] = function($s) { return 'BAR'; }; # create template cache - $cache = new \Luigi\Cache([ + $cache = new \Pablotron\Luigi\Cache([ 'foo' => 'foo%{bar|barify}', ]); @@ -124,7 +124,7 @@ final class TemplateTest extends TestCase { public function testCache() : void { # create template cache - $cache = new \Luigi\Cache([ + $cache = new \Pablotron\Luigi\Cache([ 'foo' => 'foo%{bar}', ]); @@ -148,7 +148,7 @@ final class TemplateTest extends TestCase { } public function testUnknownKeyError() : void { - $this->expectException(\Luigi\UnknownKeyError::class); + $this->expectException(\Pablotron\Luigi\UnknownKeyError::class); # run template $r = Template::once('foo%{unknown-key}', [ @@ -157,7 +157,7 @@ final class TemplateTest extends TestCase { } public function testUnknownFilterError() : void { - $this->expectException(\Luigi\UnknownFilterError::class); + $this->expectException(\Pablotron\Luigi\UnknownFilterError::class); # run template $r = Template::once('foo%{bar|unknown-filter}', [ @@ -166,10 +166,10 @@ final class TemplateTest extends TestCase { } public function testUnknownTemplateError() : void { - $this->expectException(\Luigi\UnknownTemplateError::class); + $this->expectException(\Pablotron\Luigi\UnknownTemplateError::class); # create cache - $cache = new \Luigi\Cache([ + $cache = new \Pablotron\Luigi\Cache([ 'foo' => 'foo%{bar}', ]); |