diff --git a/.gitmodules b/.gitmodules index 54f3a7b..042ea4d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "vendor/spec"] path = vendor/spec - url = git://github.com/mustache/spec.git + url = https://github.com/mustache/spec.git [submodule "vendor/yaml"] path = vendor/yaml - url = git://github.com/fabpot/yaml.git + url = https://github.com/fabpot/yaml.git diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.markdown new file mode 100644 index 0000000..fcbf6e7 --- /dev/null +++ b/CONTRIBUTING.markdown @@ -0,0 +1,33 @@ +# Contributions welcome! + + +### Here's a quick guide: + + 1. [Fork the repo on GitHub](https://github.com/bobthecow/mustache.php). + + 2. Run the test suite. We only take pull requests with passing tests, and it's great to know that you have a clean slate. Make sure you have PHPUnit 3.5+, then run `phpunit` from the project directory. + + 3. Add tests for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, add a test! + + 4. Make the tests pass. + + 5. Push your fork to GitHub and submit a pull request against the `dev` branch. + + +### You can do some things to increase the chance that your pull request is accepted the first time: + + * Submit one pull request per fix or feature. + * To help with that, do all your work in a feature branch (e.g. `feature/my-alsome-feature`). + * Follow the conventions you see used in the project. + * Use `phpcs --standard=PSR2` to check your changes against the coding standard. + * Write tests that fail without your code, and pass with it. + * Don't bump version numbers. Those will be updated — per [semver](http://semver.org) — once your change is merged into `master`. + * Update any documentation: docblocks, README, examples, etc. + * ... Don't update the wiki until your change is merged and released, but make a note in your pull request so we don't forget. + + +### Mustache.php follows the PSR-* coding standards: + + * [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) + * [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md) + * [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index e7f34e7..fe02419 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -22,6 +22,7 @@ class Mustache_Compiler private $indentNextLine; private $customEscape; private $charset; + private $pragmas; /** * Compile a Mustache token parse tree into PHP source code. @@ -36,6 +37,7 @@ class Mustache_Compiler */ public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8') { + $this->pragmas = array(); $this->sections = array(); $this->source = $source; $this->indentNextLine = true; @@ -61,6 +63,10 @@ class Mustache_Compiler $level++; foreach ($tree as $node) { switch ($node[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PRAGMA: + $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; + break; + case Mustache_Tokenizer::T_SECTION: $code .= $this->section( $node[Mustache_Tokenizer::NODES], @@ -118,8 +124,11 @@ class Mustache_Compiler class %s extends Mustache_Template { + private $lambdaHelper; + public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false) { + $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); $buffer = \'\'; %s @@ -159,7 +168,7 @@ class Mustache_Compiler if (!is_string($value) && is_callable($value)) { $source = %s; $buffer .= $this->mustache - ->loadLambda((string) call_user_func($value, $source)%s) + ->loadLambda((string) call_user_func($value, $source, $this->lambdaHelper)%s) ->renderInternal($context, $indent); } elseif (!empty($value)) { $values = $this->isIterable($value) ? $value : array($value); @@ -260,7 +269,7 @@ class Mustache_Compiler $value = $this->mustache ->loadLambda((string) call_user_func($value)) ->renderInternal($context, $indent); - } + }%s $buffer .= %s%s; '; @@ -275,11 +284,63 @@ class Mustache_Compiler */ private function variable($id, $escape, $level) { + $filters = ''; + + if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) { + list($id, $filters) = $this->getFilters($id, $level); + } + $method = $this->getFindMethod($id); $id = ($method !== 'last') ? var_export($id, true) : ''; $value = $escape ? $this->getEscape() : '$value'; - return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value); + return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); + } + + /** + * Generate Mustache Template variable filtering PHP source. + * + * @param string $id Variable name + * @param int $level + * + * @return string Generated variable filtering PHP source + */ + private function getFilters($id, $level) + { + $filters = array_map('trim', explode('|', $id)); + $id = array_shift($filters); + + return array($id, $this->getFilter($filters, $level)); + } + + const FILTER = ' + $filter = $context->%s(%s); + if (is_string($filter) || !is_callable($filter)) { + throw new UnexpectedValueException(%s); + } + $value = call_user_func($filter, $value);%s + '; + + /** + * Generate PHP source for a single filter. + * + * @param array $filters + * @param int $level + * + * @return string Generated filter PHP source + */ + private function getFilter(array $filters, $level) + { + if (empty($filters)) { + return ''; + } + + $name = array_shift($filters); + $method = $this->getFindMethod($name); + $filter = ($method !== 'last') ? var_export($name, true) : ''; + $msg = var_export(sprintf('Filter not found: %s', $name), true); + + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level)); } const LINE = '$buffer .= "\n";'; diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index 6a2d57c..e7783b4 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -133,7 +133,7 @@ class Mustache_Context private function findVariableInStack($id, array $stack) { for ($i = count($stack) - 1; $i >= 0; $i--) { - if (is_object($stack[$i])) { + if (is_object($stack[$i]) && !$stack[$i] instanceof Closure) { if (method_exists($stack[$i], $id)) { return $stack[$i]->$id(); } elseif (isset($stack[$i]->$id)) { diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 97cc57d..ddfffca 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -23,8 +23,10 @@ */ class Mustache_Engine { - const VERSION = '2.0.2'; - const SPEC_VERSION = '1.1.2'; + const VERSION = '2.1.0'; + const SPEC_VERSION = '1.1.2'; + + const PRAGMA_FILTERS = 'FILTERS'; // Template cache private $templates = array(); @@ -32,11 +34,13 @@ class Mustache_Engine // Environment private $templateClassPrefix = '__Mustache_'; private $cache = null; + private $cacheFileMode = null; private $loader; private $partialsLoader; private $helpers; private $escape; private $charset = 'UTF-8'; + private $logger; /** * Mustache class constructor. @@ -44,13 +48,17 @@ class Mustache_Engine * Passing an $options array allows overriding certain Mustache options during instantiation: * * $options = array( - * // The class prefix for compiled templates. Defaults to '__Mustache_' + * // The class prefix for compiled templates. Defaults to '__Mustache_'. * 'template_class_prefix' => '__MyTemplates_', * * // A cache directory for compiled templates. Mustache will not cache templates unless this is set * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', * - * // A Mustache template loader instance. Uses a StringLoader if not specified + * // Override default permissions for cache files. Defaults to using the system-defined umask. It is + * // *strongly* recommended that you configure your umask properly rather than overriding permissions here. + * 'cache_file_mode' => 0666, + * + * // A Mustache template loader instance. Uses a StringLoader if not specified. * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), * * // A Mustache loader instance for partials. @@ -64,16 +72,21 @@ class Mustache_Engine * // 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... - * }), + * // do something translatey here... + * }), * * // An 'escape' callback, responsible for escaping double-mustache variables. * 'escape' => function($value) { * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); * }, * - * // character set for `htmlspecialchars`. Defaults to 'UTF-8' + * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. * 'charset' => 'ISO-8859-1', + * + * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible + * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is + * // available as well: + * 'logger' => new Mustache_StreamLogger('php://stderr'), * ); * * @param array $options (default: array()) @@ -88,6 +101,10 @@ class Mustache_Engine $this->cache = $options['cache']; } + if (isset($options['cache_file_mode'])) { + $this->cacheFileMode = $options['cache_file_mode']; + } + if (isset($options['loader'])) { $this->setLoader($options['loader']); } @@ -115,6 +132,10 @@ class Mustache_Engine if (isset($options['charset'])) { $this->charset = $options['charset']; } + + if (isset($options['logger'])) { + $this->setLogger($options['logger']); + } } /** @@ -319,6 +340,30 @@ class Mustache_Engine $this->getHelpers()->remove($name); } + /** + * Set the Mustache Logger instance. + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + $this->logger = $logger; + } + + /** + * Get the current Mustache Logger instance. + * + * @return Mustache_Logger|Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + /** * Set the Mustache Tokenizer instance. * @@ -442,7 +487,12 @@ class Mustache_Engine try { return $this->loadSource($this->getPartialsLoader()->load($name)); } catch (InvalidArgumentException $e) { - // If the named partial cannot be found, return null. + // If the named partial cannot be found, log then return null. + $this->log( + Mustache_Logger::WARNING, + 'Partial not found: "{name}"', + array('name' => $name) + ); } } @@ -485,15 +535,33 @@ class Mustache_Engine if (!class_exists($className, false)) { if ($fileName = $this->getCacheFilename($source)) { if (!is_file($fileName)) { + $this->log( + Mustache_Logger::DEBUG, + 'Writing "{className}" class to template cache: "{fileName}"', + array('className' => $className, 'fileName' => $fileName) + ); + $this->writeCacheFile($fileName, $this->compile($source)); } require_once $fileName; } else { + $this->log( + Mustache_Logger::WARNING, + 'Template cache disabled, evaluating "{className}" class at runtime', + array('className' => $className) + ); + eval('?>'.$this->compile($source)); } } + $this->log( + Mustache_Logger::DEBUG, + 'Instantiating template: "{className}"', + array('className' => $className) + ); + $this->templates[$className] = new $className($this); } @@ -542,6 +610,12 @@ class Mustache_Engine $tree = $this->parse($source); $name = $this->getTemplateClassName($source); + $this->log( + Mustache_Logger::INFO, + 'Compiling template to "{className}" class', + array('className' => $name) + ); + return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset); } @@ -562,7 +636,7 @@ class Mustache_Engine /** * Helper method to dump a generated Mustache Template subclass to the file cache. * - * @throws RuntimeException if unable to write to $fileName. + * @throws RuntimeException if unable to create the cache directory or write to $fileName. * * @param string $fileName * @param string $source @@ -571,19 +645,57 @@ class Mustache_Engine */ private function writeCacheFile($fileName, $source) { - if (!is_dir(dirname($fileName))) { - mkdir(dirname($fileName), 0777, true); + $dirName = dirname($fileName); + if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + 'Creating Mustache template cache directory: "{dirName}"', + array('dirName' => $dirName) + ); + + @mkdir($dirName, 0777, true); + if (!is_dir($dirName)) { + throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); + } + } - $tempFile = tempnam(dirname($fileName), basename($fileName)); + $this->log( + Mustache_Logger::DEBUG, + 'Caching compiled template to "{fileName}"', + array('fileName' => $fileName) + ); + + $tempFile = tempnam($dirName, basename($fileName)); if (false !== @file_put_contents($tempFile, $source)) { if (@rename($tempFile, $fileName)) { - chmod($fileName, 0644); + $mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask()); + @chmod($fileName, $mode); return; } + + $this->log( + Mustache_Logger::ERROR, + 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', + array('tempName' => $tempFile, 'fileName' => $fileName) + ); } throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); } + + /** + * Add a log record if logging is enabled. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + private function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } } diff --git a/src/Mustache/LambdaHelper.php b/src/Mustache/LambdaHelper.php new file mode 100644 index 0000000..6f226c5 --- /dev/null +++ b/src/Mustache/LambdaHelper.php @@ -0,0 +1,48 @@ +mustache = $mustache; + $this->context = $context; + } + + /** + * Render a string as a Mustache template with the current rendering context. + * + * @param string $string + * + * @return Rendered template. + */ + public function render($string) + { + return $this->mustache + ->loadLambda((string) $string) + ->renderInternal($this->context); + } +} diff --git a/src/Mustache/Loader/FilesystemLoader.php b/src/Mustache/Loader/FilesystemLoader.php index 041622c..bbd2a43 100644 --- a/src/Mustache/Loader/FilesystemLoader.php +++ b/src/Mustache/Loader/FilesystemLoader.php @@ -12,19 +12,19 @@ /** * Mustache Template filesystem Loader implementation. * - * An ArrayLoader instance loads Mustache Template source from the filesystem by name: + * A FilesystemLoader instance loads Mustache Template source from the filesystem by name: * - * $loader = new FilesystemLoader(dirname(__FILE__).'/views'); + * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); * $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache'); * * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates: * * $m = new Mustache(array( - * 'loader' => new FilesystemLoader(dirname(__FILE__).'/views'), - * 'partials_loader' => new FilesystemLoader(dirname(__FILE__).'/views/partials'), + * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), + * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), * )); * - * @implements Loader + * @implements Mustache_Loader */ class Mustache_Loader_FilesystemLoader implements Mustache_Loader { @@ -55,15 +55,19 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir); } - if (isset($options['extension'])) { - $this->extension = '.' . ltrim($options['extension'], '.'); + if (array_key_exists('extension', $options)) { + if (empty($options['extension'])) { + $this->extension = ''; + } else { + $this->extension = '.' . ltrim($options['extension'], '.'); + } } } /** * Load a Template by name. * - * $loader = new FilesystemLoader(dirname(__FILE__).'/views'); + * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache"; * * @param string $name diff --git a/src/Mustache/Logger.php b/src/Mustache/Logger.php new file mode 100644 index 0000000..e08359a --- /dev/null +++ b/src/Mustache/Logger.php @@ -0,0 +1,135 @@ +log(Mustache_Logger::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = array()) + { + $this->log(Mustache_Logger::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = array()) + { + $this->log(Mustache_Logger::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = array()) + { + $this->log(Mustache_Logger::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = array()) + { + $this->log(Mustache_Logger::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = array()) + { + $this->log(Mustache_Logger::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = array()) + { + $this->log(Mustache_Logger::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = array()) + { + $this->log(Mustache_Logger::DEBUG, $message, $context); + } +} diff --git a/src/Mustache/Logger/StreamLogger.php b/src/Mustache/Logger/StreamLogger.php new file mode 100644 index 0000000..7f3fd50 --- /dev/null +++ b/src/Mustache/Logger/StreamLogger.php @@ -0,0 +1,188 @@ + 100, + self::INFO => 200, + self::NOTICE => 250, + self::WARNING => 300, + self::ERROR => 400, + self::CRITICAL => 500, + self::ALERT => 550, + self::EMERGENCY => 600, + ); + + protected $stream = null; + protected $url = null; + + /** + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param string $stream Resource instance or URL + * @param integer $level The minimum logging level at which this handler will be triggered + */ + public function __construct($stream, $level = Mustache_Logger::ERROR) + { + $this->setLevel($level); + + if (is_resource($stream)) { + $this->stream = $stream; + } else { + $this->url = $stream; + } + } + + /** + * Close stream resources. + */ + public function __destruct() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** + * Set the minimum logging level. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level The minimum logging level which will be written + */ + public function setLevel($level) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + $this->level = $level; + } + + /** + * Get the current minimum logging level. + * + * @return integer + */ + public function getLevel() + { + return $this->level; + } + + /** + * Logs with an arbitrary level. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + if (self::$levels[$level] >= self::$levels[$this->level]) { + $this->writeLog($level, $message, $context); + } + } + + /** + * Write a record to the log. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function writeLog($level, $message, array $context = array()) + { + if (!is_resource($this->stream)) { + if (!isset($this->url)) { + throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + + $this->stream = fopen($this->url, 'a'); + if (!is_resource($this->stream)) { + // @codeCoverageIgnoreStart + throw new UnexpectedValueException(sprintf('The stream or file "%s" could not be opened.', $this->url)); + // @codeCoverageIgnoreEnd + } + } + + fwrite($this->stream, self::formatLine($level, $message, $context)); + } + + /** + * Gets the name of the logging level. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level + * + * @return string + */ + protected static function getLevelName($level) + { + return strtoupper($level); + } + + /** + * Format a log line for output. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + * + * @return string + */ + protected static function formatLine($level, $message, array $context = array()) + { + return sprintf( + "%s: %s\n", + self::getLevelName($level), + self::interpolateMessage($message, $context) + ); + } + + /** + * Interpolate context values into the message placeholders. + * + * @param string $message + * @param array $context + * + * @return string + */ + protected static function interpolateMessage($message, array $context = array()) + { + $message = (string) $message; + + // build a replacement array with braces around the context keys + $replace = array(); + foreach ($context as $key => $val) { + $replace['{' . $key . '}'] = $val; + } + + // interpolate replacement values into the the message and return + return strtr($message, $replace); + } +} diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index 1dd3ef8..3b9e69a 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -34,6 +34,7 @@ class Mustache_Tokenizer const T_UNESCAPED = '{'; const T_UNESCAPED_2 = '&'; const T_TEXT = '_t'; + const T_PRAGMA = '%'; // Valid token types private static $tagTypes = array( @@ -47,6 +48,7 @@ class Mustache_Tokenizer self::T_ESCAPED => true, self::T_UNESCAPED => true, self::T_UNESCAPED_2 => true, + self::T_PRAGMA => true, ); // Interpolated tags @@ -67,6 +69,7 @@ class Mustache_Tokenizer const NODES = 'nodes'; const VALUE = 'value'; + private $pragmas; private $state; private $tagType; private $tag; @@ -126,6 +129,9 @@ class Mustache_Tokenizer if ($this->tagType === self::T_DELIM_CHANGE) { $i = $this->changeDelimiters($text, $i); $this->state = self::IN_TEXT; + } elseif ($this->tagType === self::T_PRAGMA) { + $i = $this->addPragma($text, $i); + $this->state = self::IN_TEXT; } else { if ($tag !== null) { $i++; @@ -168,6 +174,13 @@ class Mustache_Tokenizer $this->filterLine(true); + foreach ($this->pragmas as $pragma) { + array_unshift($this->tokens, array( + self::TYPE => self::T_PRAGMA, + self::NAME => $pragma, + )); + } + return $this->tokens; } @@ -185,6 +198,7 @@ class Mustache_Tokenizer $this->lineStart = 0; $this->otag = '{{'; $this->ctag = '}}'; + $this->pragmas = array(); } /** @@ -270,6 +284,14 @@ class Mustache_Tokenizer return $closeIndex + strlen($close) - 1; } + private function addPragma($text, $index) + { + $end = strpos($text, $this->ctag, $index); + $this->pragmas[] = trim(substr($text, $index + 2, $end - $index - 2)); + + return $end + strlen($this->ctag) - 1; + } + /** * Test whether it's time to change tags. * diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index 35390c1..bf59d18 100644 --- a/test/Mustache/Test/EngineTest.php +++ b/test/Mustache/Test/EngineTest.php @@ -27,11 +27,14 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase public function testConstructor() { + $logger = new Mustache_Logger_StreamLogger(tmpfile()); $loader = new Mustache_Loader_StringLoader; $partialsLoader = new Mustache_Loader_ArrayLoader; $mustache = new Mustache_Engine(array( 'template_class_prefix' => '__whot__', - 'cache' => self::$tempDir, + 'cache' => self::$tempDir, + 'cache_file_mode' => 777, + 'logger' => $logger, 'loader' => $loader, 'partials_loader' => $partialsLoader, 'partials' => array( @@ -45,6 +48,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase 'charset' => 'ISO-8859-1', )); + $this->assertSame($logger, $mustache->getLogger()); $this->assertSame($loader, $mustache->getLoader()); $this->assertSame($partialsLoader, $mustache->getPartialsLoader()); $this->assertEquals('{{ foo }}', $partialsLoader->load('foo')); @@ -85,12 +89,17 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase public function testSettingServices() { + $logger = new Mustache_Logger_StreamLogger(tmpfile()); $loader = new Mustache_Loader_StringLoader; $tokenizer = new Mustache_Tokenizer; $parser = new Mustache_Parser; $compiler = new Mustache_Compiler; $mustache = new Mustache_Engine; + $this->assertNotSame($logger, $mustache->getLogger()); + $mustache->setLogger($logger); + $this->assertSame($logger, $mustache->getLogger()); + $this->assertNotSame($loader, $mustache->getLoader()); $mustache->setLoader($loader); $this->assertSame($loader, $mustache->getLoader()); @@ -222,6 +231,74 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase $mustache->setHelpers('monkeymonkeymonkey'); } + /** + * @expectedException InvalidArgumentException + */ + public function testSetLoggerThrowsExceptions() + { + $mustache = new Mustache_Engine; + $mustache->setLogger(new StdClass); + } + + public function testPartialLoadFailLogging() + { + $name = tempnam(sys_get_temp_dir(), 'mustache-test'); + $mustache = new Mustache_Engine(array( + 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::WARNING), + 'partials' => array( + 'foo' => 'FOO', + 'bar' => 'BAR', + ), + )); + + $result = $mustache->render('{{> foo }}{{> bar }}{{> baz }}', array()); + $this->assertEquals('FOOBAR', $result); + + $this->assertContains('WARNING: Partial not found: "baz"', file_get_contents($name)); + } + + public function testCacheWarningLogging() + { + $name = tempnam(sys_get_temp_dir(), 'mustache-test'); + $mustache = new Mustache_Engine(array( + 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::WARNING) + )); + + $result = $mustache->render('{{ foo }}', array('foo' => 'FOO')); + $this->assertEquals('FOO', $result); + + $this->assertContains('WARNING: Template cache disabled, evaluating', file_get_contents($name)); + } + + public function testLoggingIsNotTooAnnoying() + { + $name = tempnam(sys_get_temp_dir(), 'mustache-test'); + $mustache = new Mustache_Engine(array( + 'logger' => new Mustache_Logger_StreamLogger($name) + )); + + $result = $mustache->render('{{ foo }}{{> bar }}', array('foo' => 'FOO')); + $this->assertEquals('FOO', $result); + + $this->assertEmpty(file_get_contents($name)); + } + + public function testVerboseLoggingIsVerbose() + { + $name = tempnam(sys_get_temp_dir(), 'mustache-test'); + $mustache = new Mustache_Engine(array( + 'logger' => new Mustache_Logger_StreamLogger($name, Mustache_Logger::DEBUG) + )); + + $result = $mustache->render('{{ foo }}{{> bar }}', array('foo' => 'FOO')); + $this->assertEquals('FOO', $result); + + $log = file_get_contents($name); + + $this->assertContains("DEBUG: Instantiating template: ", $log); + $this->assertContains("WARNING: Partial not found: \"bar\"", $log); + } + private static function rmdir($path) { $path = rtrim($path, '/').'/'; @@ -244,7 +321,8 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase } } -class MustacheStub extends Mustache_Engine { +class MustacheStub extends Mustache_Engine +{ public $source; public $template; public function loadTemplate($source) diff --git a/test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php b/test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php new file mode 100644 index 0000000..9f9f548 --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php @@ -0,0 +1,30 @@ +mustache = new Mustache_Engine; + } + + public function testClosuresDontLikeItWhenYouTouchTheirProperties() + { + $tpl = $this->mustache->loadTemplate('{{ foo.bar }}'); + $this->assertEquals('', $tpl->render(array('foo' => function() { return 'FOO'; }))); + } +} diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php new file mode 100644 index 0000000..8c51e37 --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -0,0 +1,94 @@ +mustache = new Mustache_Engine; + } + + public function testSingleFilter() + { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate }}'); + + $this->mustache->addHelper('longdate', function(\DateTime $value) { + return $value->format('Y-m-d h:m:s'); + }); + + $foo = new \StdClass; + $foo->date = new DateTime('1/1/2000'); + + $this->assertEquals('2000-01-01 12:01:00', $tpl->render($foo)); + } + + public function testChainedFilters() + { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate | withbrackets }}'); + + $this->mustache->addHelper('longdate', function(\DateTime $value) { + return $value->format('Y-m-d h:m:s'); + }); + + $this->mustache->addHelper('withbrackets', function($value) { + return sprintf('[[%s]]', $value); + }); + + $foo = new \StdClass; + $foo->date = new DateTime('1/1/2000'); + + $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); + } + + public function testInterpolateFirst() + { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar }}'); + $this->assertEquals('win!', $tpl->render(array( + 'foo' => 'FOO', + 'bar' => function($value) { + return ($value === 'FOO') ? 'win!' : 'fail :('; + }, + ))); + } + + /** + * @expectedException UnexpectedValueException + * @dataProvider getBrokenPipes + */ + public function testThrowsExceptionForBrokenPipes($tpl, $data) + { + $this->mustache + ->loadTemplate(sprintf('{{%% FILTERS }}{{ %s }}', $tpl)) + ->render($data); + } + + public function getBrokenPipes() + { + return array( + array('foo | bar', array()), + array('foo | bar', array('foo' => 'FOO')), + array('foo | bar', array('foo' => 'FOO', 'bar' => 'BAR')), + array('foo | bar', array('foo' => 'FOO', 'bar' => array(1, 2))), + array('foo | bar | baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; })), + array('foo | bar | baz', array('foo' => 'FOO', 'baz' => function() { return 'BAZ'; })), + array('foo | bar | baz', array('bar' => function() { return 'BAR'; })), + array('foo | bar | baz', array('baz' => function() { return 'BAZ'; })), + array('foo | bar.baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; }, 'baz' => function() { return 'BAZ'; })), + ); + } +} diff --git a/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php index 8453c93..f010f3c 100644 --- a/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php +++ b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php @@ -13,59 +13,65 @@ * @group lambdas * @group functional */ -class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase { +class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit_Framework_TestCase +{ + private $mustache; - private $mustache; + public function setUp() + { + $this->mustache = new Mustache_Engine; + } - public function setUp() { - $this->mustache = new Mustache_Engine; - } + public function testAnonymousFunctionSectionCallback() + { + $tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}'); - public function testAnonymousFunctionSectionCallback() { - $tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}'); + $foo = new Mustache_Test_FiveThree_Functional_Foo; + $foo->name = 'Mario'; + $foo->wrapper = function($text) { + return sprintf('