Parcourir la source

Add Helper implementation.

Justin Hileman il y a 13 ans
Parent
commit
ba75d746e1

+ 158 - 0
src/Mustache/HelperCollection.php

@@ -0,0 +1,158 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mustache;
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class HelperCollection {
+	private $helpers = array();
+
+	/**
+	 * Helper Collection constructor.
+	 *
+	 * Optionally accepts an array (or \Traversable) of `$name => $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);
+	}
+}

+ 99 - 0
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.
 	 *

+ 24 - 1
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;
+	}
 }

+ 156 - 0
test/Mustache/Test/HelperCollectionTest.php

@@ -0,0 +1,156 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mustache\Test;
+
+use Mustache\HelperCollection;
+
+class HelperCollectionTest extends \PHPUnit_Framework_TestCase {
+	public function testConstructor() {
+		$foo = function() { echo 'foo'; };
+		$bar = 'BAR';
+
+		$helpers = new HelperCollection(array(
+			'foo' => $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',
+            ),
+        );
+    }
+}

+ 50 - 0
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);