diff --git a/src/Mustache/HelperCollection.php b/src/Mustache/HelperCollection.php new file mode 100644 index 0000000..047fe5f --- /dev/null +++ b/src/Mustache/HelperCollection.php @@ -0,0 +1,158 @@ + $helper` pairs. + * + * @throws \InvalidArgumentException if the $helpers argument isn't an array or \Traversable + * + * @param array|Traversable $helpers (default: null) + */ + public function __construct($helpers = null) { + if ($helpers !== null) { + if (!is_array($helpers) && !$helpers instanceof \Traversable) { + throw new \InvalidArgumentException('HelperCollection constructor expects an array of helpers'); + } + + foreach ($helpers as $name => $helper) { + $this->add($name, $helper); + } + } + } + + /** + * Magic mutator. + * + * @see \Mustache\HelperCollection::add + * + * @param string $name + * @param mixed $helper + */ + public function __set($name, $helper) { + $this->add($name, $helper); + } + + /** + * Add a helper to this collection. + * + * @param string $name + * @param mixed $helper + */ + public function add($name, $helper) { + $this->helpers[$name] = $helper; + } + + /** + * Magic accessor. + * + * @see \Mustache\HelperCollection::get + * + * @param string $name + * + * @return mixed Helper + */ + public function __get($name) { + return $this->get($name); + } + + /** + * Get a helper by name. + * + * @param string $name + * + * @return mixed Helper + */ + public function get($name) { + if (!$this->has($name)) { + throw new \InvalidArgumentException('Unknown helper: '.$name); + } + + return $this->helpers[$name]; + } + + /** + * Magic isset(). + * + * @see \Mustache\HelperCollection::has + * + * @param string $name + * + * @return boolean True if helper is present + */ + public function __isset($name) { + return $this->has($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @param string $name + * + * @return boolean True if helper is present + */ + public function has($name) { + return array_key_exists($name, $this->helpers); + } + + /** + * Magic unset(). + * + * @see \Mustache\HelperCollection::remove + * + * @param string $name + */ + public function __unset($name) { + $this->remove($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @throws \InvalidArgumentException if the requested helper is not present. + * + * @param string $name + */ + public function remove($name) { + if (!$this->has($name)) { + throw new \InvalidArgumentException('Unknown helper: '.$name); + } + + unset($this->helpers[$name]); + } + + /** + * Clear the helper collection. + * + * Removes all helpers from this collection + */ + public function clear() { + $this->helpers = array(); + } + + /** + * Check whether the helper collection is empty. + * + * @return boolean True if the collection is empty + */ + public function isEmpty() { + return empty($this->helpers); + } +} diff --git a/src/Mustache/Mustache.php b/src/Mustache/Mustache.php index 8975907..68ecd2a 100644 --- a/src/Mustache/Mustache.php +++ b/src/Mustache/Mustache.php @@ -39,6 +39,7 @@ class Mustache { private $cache = null; private $loader; private $partialsLoader; + private $helpers; private $charset = 'UTF-8'; /** @@ -63,6 +64,13 @@ class Mustache { * // efficient or lazy as a Filesystem (or database) loader. * 'partials' => array('foo' => file_get_contents(__DIR__.'/views/partials/foo.mustache')), * + * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order + * // sections), or any other valid Mustache context value. They will be prepended to the context stack, + * // so they will be available in any template loaded by this Mustache instance. + * 'helpers' => array('i18n' => function($text) { + * // do something translatey here... + * }), + * * // character set for `htmlspecialchars`. Defaults to 'UTF-8' * 'charset' => 'ISO-8859-1', * ); @@ -90,6 +98,10 @@ class Mustache { $this->setPartials($options['partials']); } + if (isset($options['helpers'])) { + $this->setHelpers($options['helpers']); + } + if (isset($options['charset'])) { $this->charset = $options['charset']; } @@ -187,6 +199,93 @@ class Mustache { $loader->setTemplates($partials); } + /** + * Set an array of Mustache helpers. + * + * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or + * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in + * any template loaded by this Mustache instance. + * + * @throws \InvalidArgumentException if $helpers is not an array or \Traversable + * + * @param array|Traversable $helpers + */ + public function setHelpers($helpers) { + if (!is_array($helpers) && !$helpers instanceof \Traversable) { + throw new \InvalidArgumentException('setHelpers expects an array of helpers'); + } + + $this->getHelpers()->clear(); + + foreach ($helpers as $name => $helper) { + $this->addHelper($name, $helper); + } + } + + /** + * Get the current set of Mustache helpers. + * + * @see \Mustache\Mustache::setHelpers + * + * @return \Mustache\HelperCollection + */ + public function getHelpers() { + if (!isset($this->helpers)) { + $this->helpers = new HelperCollection; + } + + return $this->helpers; + } + + /** + * Add a new Mustache helper. + * + * @see \Mustache\Mustache::setHelpers + * + * @param string $name + * @param mixed $helper + */ + public function addHelper($name, $helper) { + $this->getHelpers()->add($name, $helper); + } + + /** + * Get a Mustache helper by name. + * + * @see \Mustache\Mustache::setHelpers + * + * @param string $name + * + * @return mixed Helper + */ + public function getHelper($name) { + return $this->getHelpers()->get($name); + } + + /** + * Check whether this Mustache instance has a helper. + * + * @see \Mustache\Mustache::setHelpers + * + * @param string $name + * + * @return boolean True if the helper is present + */ + public function hasHelper($name) { + return $this->getHelpers()->has($name); + } + + /** + * Remove a helper by name. + * + * @see \Mustache\Mustache::setHelpers + * + * @param string $name + */ + public function removeHelper($name) { + $this->getHelpers()->remove($name); + } + /** * Set the Mustache Tokenizer instance. * diff --git a/src/Mustache/Template.php b/src/Mustache/Template.php index b3286f8..0fa2db6 100644 --- a/src/Mustache/Template.php +++ b/src/Mustache/Template.php @@ -57,7 +57,7 @@ abstract class Template { * @return string Rendered template */ public function render($context = array()) { - return $this->renderInternal(new Context($context)); + return $this->renderInternal($this->prepareContextStack($context)); } /** @@ -119,4 +119,27 @@ abstract class Template { } } + /** + * Helper method to prepare the Context stack. + * + * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present. + * + * @param mixed $context Optional first context frame (default: null) + * + * @return \Mustache\Context + */ + protected function prepareContextStack($context = null) { + $stack = new Context; + + $helpers = $this->mustache->getHelpers(); + if (!$helpers->isEmpty()) { + $stack->push($helpers); + } + + if (!empty($context)) { + $stack->push($context); + } + + return $stack; + } } diff --git a/test/Mustache/Test/HelperCollectionTest.php b/test/Mustache/Test/HelperCollectionTest.php new file mode 100644 index 0000000..12e48dd --- /dev/null +++ b/test/Mustache/Test/HelperCollectionTest.php @@ -0,0 +1,156 @@ + $foo, + 'bar' => $bar, + )); + + $this->assertSame($foo, $helpers->get('foo')); + $this->assertSame($bar, $helpers->get('bar')); + } + + public function testAccessorsAndMutators() { + $foo = function() { echo 'foo'; }; + $bar = 'BAR'; + + $helpers = new HelperCollection; + $this->assertTrue($helpers->isEmpty()); + $this->assertFalse($helpers->has('foo')); + $this->assertFalse($helpers->has('bar')); + + $helpers->add('foo', $foo); + $this->assertFalse($helpers->isEmpty()); + $this->assertTrue($helpers->has('foo')); + $this->assertFalse($helpers->has('bar')); + + $helpers->add('bar', $bar); + $this->assertFalse($helpers->isEmpty()); + $this->assertTrue($helpers->has('foo')); + $this->assertTrue($helpers->has('bar')); + + $helpers->remove('foo'); + $this->assertFalse($helpers->isEmpty()); + $this->assertFalse($helpers->has('foo')); + $this->assertTrue($helpers->has('bar')); + } + + public function testMagicMethods() { + $foo = function() { echo 'foo'; }; + $bar = 'BAR'; + + $helpers = new HelperCollection; + $this->assertTrue($helpers->isEmpty()); + $this->assertFalse($helpers->has('foo')); + $this->assertFalse($helpers->has('bar')); + $this->assertFalse(isset($helpers->foo)); + $this->assertFalse(isset($helpers->bar)); + + $helpers->foo = $foo; + $this->assertFalse($helpers->isEmpty()); + $this->assertTrue($helpers->has('foo')); + $this->assertFalse($helpers->has('bar')); + $this->assertTrue(isset($helpers->foo)); + $this->assertFalse(isset($helpers->bar)); + + $helpers->bar = $bar; + $this->assertFalse($helpers->isEmpty()); + $this->assertTrue($helpers->has('foo')); + $this->assertTrue($helpers->has('bar')); + $this->assertTrue(isset($helpers->foo)); + $this->assertTrue(isset($helpers->bar)); + + unset($helpers->foo); + $this->assertFalse($helpers->isEmpty()); + $this->assertFalse($helpers->has('foo')); + $this->assertTrue($helpers->has('bar')); + $this->assertFalse(isset($helpers->foo)); + $this->assertTrue(isset($helpers->bar)); + } + + /** + * @dataProvider getInvalidHelperArguments + */ + public function testHelperCollectionIsntAfraidToThrowExceptions($helpers = array(), $actions = array(), $exception = null) { + if ($exception) { + $this->setExpectedException($exception); + } + + $helpers = new HelperCollection($helpers); + + foreach ($actions as $method => $args) { + call_user_func_array(array($helpers, $method), $args); + } + } + + public function getInvalidHelperArguments() { + return array( + array( + 'not helpers', + array(), + '\InvalidArgumentException', + ), + array( + array(), + array('get' => array('foo')), + '\InvalidArgumentException', + ), + array( + array('foo' => 'FOO'), + array('get' => array('foo')), + null, + ), + array( + array('foo' => 'FOO'), + array('get' => array('bar')), + '\InvalidArgumentException', + ), + array( + array('foo' => 'FOO'), + array( + 'add' => array('bar', 'BAR'), + 'get' => array('bar'), + ), + null, + ), + array( + array('foo' => 'FOO'), + array( + 'get' => array('foo'), + 'remove' => array('foo'), + ), + null, + ), + array( + array('foo' => 'FOO'), + array( + 'remove' => array('foo'), + 'get' => array('foo'), + ), + '\InvalidArgumentException', + ), + array( + array(), + array('remove' => array('foo')), + '\InvalidArgumentException', + ), + ); + } +} diff --git a/test/Mustache/Test/MustacheTest.php b/test/Mustache/Test/MustacheTest.php index 88f9a03..4180bee 100644 --- a/test/Mustache/Test/MustacheTest.php +++ b/test/Mustache/Test/MustacheTest.php @@ -43,6 +43,10 @@ class MustacheTest extends \PHPUnit_Framework_TestCase { 'partials' => array( 'foo' => '{{ foo }}', ), + 'helpers' => array( + 'foo' => function() { return 'foo'; }, + 'bar' => 'BAR', + ), 'charset' => 'ISO-8859-1', )); @@ -51,6 +55,9 @@ class MustacheTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('{{ foo }}', $partialsLoader->load('foo')); $this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}')); $this->assertEquals('ISO-8859-1', $mustache->getCharset()); + $this->assertTrue($mustache->hasHelper('foo')); + $this->assertTrue($mustache->hasHelper('bar')); + $this->assertFalse($mustache->hasHelper('baz')); } public function testRender() { @@ -144,6 +151,49 @@ class MustacheTest extends \PHPUnit_Framework_TestCase { $mustache->setPartials(array('foo' => '{{ foo }}')); } + public function testHelpers() { + $foo = function() { return 'foo'; }; + $bar = 'BAR'; + $mustache = new Mustache(array('helpers' => array( + 'foo' => $foo, + 'bar' => $bar, + ))); + + $helpers = $mustache->getHelpers(); + $this->assertTrue($mustache->hasHelper('foo')); + $this->assertTrue($mustache->hasHelper('bar')); + $this->assertTrue($helpers->has('foo')); + $this->assertTrue($helpers->has('bar')); + $this->assertSame($foo, $mustache->getHelper('foo')); + $this->assertSame($bar, $mustache->getHelper('bar')); + + $mustache->removeHelper('bar'); + $this->assertFalse($mustache->hasHelper('bar')); + $mustache->addHelper('bar', $bar); + $this->assertSame($bar, $mustache->getHelper('bar')); + + $baz = function($text) { return '__'.$text.'__'; }; + $this->assertFalse($mustache->hasHelper('baz')); + $this->assertFalse($helpers->has('baz')); + + $mustache->addHelper('baz', $baz); + $this->assertTrue($mustache->hasHelper('baz')); + $this->assertTrue($helpers->has('baz')); + + // ... and a functional test + $tpl = $mustache->loadTemplate('{{foo}} - {{bar}} - {{#baz}}qux{{/baz}}'); + $this->assertEquals('foo - BAR - __qux__', $tpl->render()); + $this->assertEquals('foo - BAR - __qux__', $tpl->render(array('qux' => "won't mess things up"))); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSetHelpersThrowsExceptions() { + $mustache = new Mustache; + $mustache->setHelpers('monkeymonkeymonkey'); + } + private static function rmdir($path) { $path = rtrim($path, '/').'/'; $handle = opendir($path);