From 52c7ccf1b9fc8c13c331ae63484db71f25db50d5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 23 Jul 2012 11:56:07 -0700 Subject: [PATCH 01/31] Pass a helper to section lambdas --- src/Mustache/Compiler.php | 5 +- src/Mustache/LambdaHelper.php | 48 +++++++++++++++++++ .../FiveThree/Functional/LambdaHelperTest.php | 37 ++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/Mustache/LambdaHelper.php create mode 100644 test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index e7f34e7..aff8e88 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -118,8 +118,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 +162,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); 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/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php b/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php new file mode 100644 index 0000000..a414d65 --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php @@ -0,0 +1,37 @@ +mustache = new Mustache_Engine; + } + + public function testSectionLambdaHelper() { + $one = $this->mustache->loadTemplate('{{name}}'); + $two = $this->mustache->loadTemplate('{{#lambda}}{{name}}{{/lambda}}'); + + $foo = new StdClass; + $foo->name = 'Mario'; + $foo->lambda = function($text, $mustache) { + return strtoupper($mustache->render($text)); + }; + + $this->assertEquals('Mario', $one->render($foo)); + $this->assertEquals('MARIO', $two->render($foo)); + } +} From 169e6e0a1f7507fc5e7b7355be9dc094bdfd8915 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 17:58:55 -0700 Subject: [PATCH 02/31] Fix whitespace in a few test files (PSR-2) --- .../Functional/HigherOrderSectionsTest.php | 78 ++++----- .../FiveThree/Functional/MustacheSpecTest.php | 158 +++++++++--------- test/fixtures/examples/partials/Partials.php | 10 +- 3 files changed, 123 insertions(+), 123 deletions(-) diff --git a/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php index 8453c93..9862d06 100644 --- a/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php +++ b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php @@ -15,57 +15,57 @@ */ 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('
%s
', $text); - }; + $foo = new Mustache_Test_FiveThree_Functional_Foo; + $foo->name = 'Mario'; + $foo->wrapper = function($text) { + return sprintf('
%s
', $text); + }; - $this->assertEquals(sprintf('
%s
', $foo->name), $tpl->render($foo)); - } + $this->assertEquals(sprintf('
%s
', $foo->name), $tpl->render($foo)); + } - public function testSectionCallback() { - $one = $this->mustache->loadTemplate('{{name}}'); - $two = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); + public function testSectionCallback() { + $one = $this->mustache->loadTemplate('{{name}}'); + $two = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); - $foo = new Mustache_Test_FiveThree_Functional_Foo; - $foo->name = 'Luigi'; + $foo = new Mustache_Test_FiveThree_Functional_Foo; + $foo->name = 'Luigi'; - $this->assertEquals($foo->name, $one->render($foo)); - $this->assertEquals(sprintf('%s', $foo->name), $two->render($foo)); - } + $this->assertEquals($foo->name, $one->render($foo)); + $this->assertEquals(sprintf('%s', $foo->name), $two->render($foo)); + } - public function testViewArrayAnonymousSectionCallback() { - $tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); + public function testViewArrayAnonymousSectionCallback() { + $tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); - $data = array( - 'name' => 'Bob', - 'wrap' => function($text) { - return sprintf('[[%s]]', $text); - } - ); + $data = array( + 'name' => 'Bob', + 'wrap' => function($text) { + return sprintf('[[%s]]', $text); + } + ); - $this->assertEquals(sprintf('[[%s]]', $data['name']), $tpl->render($data)); - } + $this->assertEquals(sprintf('[[%s]]', $data['name']), $tpl->render($data)); + } } class Mustache_Test_FiveThree_Functional_Foo { - public $name = 'Justin'; - public $lorem = 'Lorem ipsum dolor sit amet,'; - public $wrap; + public $name = 'Justin'; + public $lorem = 'Lorem ipsum dolor sit amet,'; + public $wrap; - public function __construct() { - $this->wrap = function($text) { - return sprintf('%s', $text); - }; - } + public function __construct() { + $this->wrap = function($text) { + return sprintf('%s', $text); + }; + } } diff --git a/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php b/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php index cf26ae1..86b8795 100644 --- a/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php +++ b/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php @@ -17,98 +17,98 @@ */ class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCase { - private static $mustache; + private static $mustache; - public static function setUpBeforeClass() { - self::$mustache = new Mustache_Engine; - } + public static function setUpBeforeClass() { + self::$mustache = new Mustache_Engine; + } - /** - * For some reason data providers can't mark tests skipped, so this test exists - * simply to provide a 'skipped' test if the `spec` submodule isn't initialized. - */ - public function testSpecInitialized() { - if (!file_exists(dirname(__FILE__).'/../../../../../vendor/spec/specs/')) { - $this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"'); - } - } + /** + * For some reason data providers can't mark tests skipped, so this test exists + * simply to provide a 'skipped' test if the `spec` submodule isn't initialized. + */ + public function testSpecInitialized() { + if (!file_exists(dirname(__FILE__).'/../../../../../vendor/spec/specs/')) { + $this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"'); + } + } - /** - * @group lambdas - * @dataProvider loadLambdasSpec - */ - public function testLambdasSpec($desc, $source, $partials, $data, $expected) { - $template = self::loadTemplate($source, $partials); - $this->assertEquals($expected, $template($this->prepareLambdasSpec($data)), $desc); - } + /** + * @group lambdas + * @dataProvider loadLambdasSpec + */ + public function testLambdasSpec($desc, $source, $partials, $data, $expected) { + $template = self::loadTemplate($source, $partials); + $this->assertEquals($expected, $template($this->prepareLambdasSpec($data)), $desc); + } - public function loadLambdasSpec() { - return $this->loadSpec('~lambdas'); - } + public function loadLambdasSpec() { + return $this->loadSpec('~lambdas'); + } - /** - * Extract and lambdafy any 'lambda' values found in the $data array. - */ - private function prepareLambdasSpec($data) { - foreach ($data as $key => $val) { - if ($key === 'lambda') { - if (!isset($val['php'])) { - $this->markTestSkipped(sprintf('PHP lambda test not implemented for this test.')); - } + /** + * Extract and lambdafy any 'lambda' values found in the $data array. + */ + private function prepareLambdasSpec($data) { + foreach ($data as $key => $val) { + if ($key === 'lambda') { + if (!isset($val['php'])) { + $this->markTestSkipped(sprintf('PHP lambda test not implemented for this test.')); + } - $func = $val['php']; - $data[$key] = function($text = null) use ($func) { - return eval($func); - }; - } else if (is_array($val)) { - $data[$key] = $this->prepareLambdasSpec($val); - } - } + $func = $val['php']; + $data[$key] = function($text = null) use ($func) { + return eval($func); + }; + } elseif (is_array($val)) { + $data[$key] = $this->prepareLambdasSpec($val); + } + } - return $data; - } + return $data; + } - /** - * Data provider for the mustache spec test. - * - * Loads YAML files from the spec and converts them to PHPisms. - * - * @access public - * @return array - */ - private function loadSpec($name) { - $filename = dirname(__FILE__) . '/../../../../../vendor/spec/specs/' . $name . '.yml'; - if (!file_exists($filename)) { - return array(); - } + /** + * Data provider for the mustache spec test. + * + * Loads YAML files from the spec and converts them to PHPisms. + * + * @access public + * @return array + */ + private function loadSpec($name) { + $filename = dirname(__FILE__) . '/../../../../../vendor/spec/specs/' . $name . '.yml'; + if (!file_exists($filename)) { + return array(); + } - $data = array(); - $yaml = new sfYamlParser; - $file = file_get_contents($filename); + $data = array(); + $yaml = new sfYamlParser; + $file = file_get_contents($filename); - // @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain. - if ($name === '~lambdas') { - $file = str_replace(" !code\n", "\n", $file); - } + // @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain. + if ($name === '~lambdas') { + $file = str_replace(" !code\n", "\n", $file); + } - $spec = $yaml->parse($file); + $spec = $yaml->parse($file); - foreach ($spec['tests'] as $test) { - $data[] = array( - $test['name'] . ': ' . $test['desc'], - $test['template'], - isset($test['partials']) ? $test['partials'] : array(), - $test['data'], - $test['expected'], - ); - } + foreach ($spec['tests'] as $test) { + $data[] = array( + $test['name'] . ': ' . $test['desc'], + $test['template'], + isset($test['partials']) ? $test['partials'] : array(), + $test['data'], + $test['expected'], + ); + } - return $data; - } + return $data; + } - private static function loadTemplate($source, $partials) { - self::$mustache->setPartials($partials); + private static function loadTemplate($source, $partials) { + self::$mustache->setPartials($partials); - return self::$mustache->loadTemplate($source); - } + return self::$mustache->loadTemplate($source); + } } diff --git a/test/fixtures/examples/partials/Partials.php b/test/fixtures/examples/partials/Partials.php index f15d6b2..cfa110c 100644 --- a/test/fixtures/examples/partials/Partials.php +++ b/test/fixtures/examples/partials/Partials.php @@ -1,9 +1,9 @@ 'Page Title', - 'subtitle' => 'Page Subtitle', - 'content' => 'Lorem ipsum dolor sit amet.', - ); + public $page = array( + 'title' => 'Page Title', + 'subtitle' => 'Page Subtitle', + 'content' => 'Lorem ipsum dolor sit amet.', + ); } From 83cf3158ae68f87c7d22863c2c9f49c7b121c416 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:08:17 -0700 Subject: [PATCH 03/31] Context::find Failing test Closures don't like it when you touch their properties. --- .../Functional/ClosureQuirksTest.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/Mustache/Test/FiveThree/Functional/ClosureQuirksTest.php 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'; }))); + } +} From 5f29e8305b9fdd97ccb07c090d782946286f560d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:08:50 -0700 Subject: [PATCH 04/31] Fix Context::find Don't ask closures about their methods or properties --- src/Mustache/Context.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) { From cc7d164a425973ae67a9a918943bf098a223b30a Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 2 Aug 2012 10:32:35 -0700 Subject: [PATCH 05/31] Coding standards fix. --- .../Test/FiveThree/Functional/LambdaHelperTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php b/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php index a414d65..e095d35 100644 --- a/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php +++ b/test/Mustache/Test/FiveThree/Functional/LambdaHelperTest.php @@ -13,15 +13,17 @@ * @group lambdas * @group functional */ -class Mustache_Test_FiveThree_Functional_LambdaHelperTest extends PHPUnit_Framework_TestCase { - +class Mustache_Test_FiveThree_Functional_LambdaHelperTest extends PHPUnit_Framework_TestCase +{ private $mustache; - public function setUp() { + public function setUp() + { $this->mustache = new Mustache_Engine; } - public function testSectionLambdaHelper() { + public function testSectionLambdaHelper() + { $one = $this->mustache->loadTemplate('{{name}}'); $two = $this->mustache->loadTemplate('{{#lambda}}{{name}}{{/lambda}}'); From 277288137bc42ac27e302beb191e401634f9088f Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 2 Aug 2012 10:36:07 -0700 Subject: [PATCH 06/31] Coding standards fixes. --- test/Mustache/Test/EngineTest.php | 3 ++- .../Functional/HigherOrderSectionsTest.php | 22 ++++++++++------ .../FiveThree/Functional/MustacheSpecTest.php | 25 ++++++++++++------- .../Mustache/Test/Functional/ExamplesTest.php | 2 +- .../Test/Functional/MustacheInjectionTest.php | 3 --- test/fixtures/examples/partials/Partials.php | 3 ++- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index 35390c1..7178786 100644 --- a/test/Mustache/Test/EngineTest.php +++ b/test/Mustache/Test/EngineTest.php @@ -244,7 +244,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/HigherOrderSectionsTest.php b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php index 9862d06..f010f3c 100644 --- a/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php +++ b/test/Mustache/Test/FiveThree/Functional/HigherOrderSectionsTest.php @@ -13,15 +13,17 @@ * @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; - public function setUp() { + public function setUp() + { $this->mustache = new Mustache_Engine; } - public function testAnonymousFunctionSectionCallback() { + public function testAnonymousFunctionSectionCallback() + { $tpl = $this->mustache->loadTemplate('{{#wrapper}}{{name}}{{/wrapper}}'); $foo = new Mustache_Test_FiveThree_Functional_Foo; @@ -33,7 +35,8 @@ class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit $this->assertEquals(sprintf('
%s
', $foo->name), $tpl->render($foo)); } - public function testSectionCallback() { + public function testSectionCallback() + { $one = $this->mustache->loadTemplate('{{name}}'); $two = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); @@ -44,7 +47,8 @@ class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit $this->assertEquals(sprintf('%s', $foo->name), $two->render($foo)); } - public function testViewArrayAnonymousSectionCallback() { + public function testViewArrayAnonymousSectionCallback() + { $tpl = $this->mustache->loadTemplate('{{#wrap}}{{name}}{{/wrap}}'); $data = array( @@ -58,12 +62,14 @@ class Mustache_Test_FiveThree_Functional_HigherOrderSectionsTest extends PHPUnit } } -class Mustache_Test_FiveThree_Functional_Foo { +class Mustache_Test_FiveThree_Functional_Foo +{ public $name = 'Justin'; public $lorem = 'Lorem ipsum dolor sit amet,'; public $wrap; - public function __construct() { + public function __construct() + { $this->wrap = function($text) { return sprintf('%s', $text); }; diff --git a/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php b/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php index 86b8795..2414850 100644 --- a/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php +++ b/test/Mustache/Test/FiveThree/Functional/MustacheSpecTest.php @@ -15,11 +15,12 @@ * @group mustache-spec * @group functional */ -class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCase { - +class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCase +{ private static $mustache; - public static function setUpBeforeClass() { + public static function setUpBeforeClass() + { self::$mustache = new Mustache_Engine; } @@ -27,7 +28,8 @@ class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framew * For some reason data providers can't mark tests skipped, so this test exists * simply to provide a 'skipped' test if the `spec` submodule isn't initialized. */ - public function testSpecInitialized() { + public function testSpecInitialized() + { if (!file_exists(dirname(__FILE__).'/../../../../../vendor/spec/specs/')) { $this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"'); } @@ -37,19 +39,22 @@ class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framew * @group lambdas * @dataProvider loadLambdasSpec */ - public function testLambdasSpec($desc, $source, $partials, $data, $expected) { + public function testLambdasSpec($desc, $source, $partials, $data, $expected) + { $template = self::loadTemplate($source, $partials); $this->assertEquals($expected, $template($this->prepareLambdasSpec($data)), $desc); } - public function loadLambdasSpec() { + public function loadLambdasSpec() + { return $this->loadSpec('~lambdas'); } /** * Extract and lambdafy any 'lambda' values found in the $data array. */ - private function prepareLambdasSpec($data) { + private function prepareLambdasSpec($data) + { foreach ($data as $key => $val) { if ($key === 'lambda') { if (!isset($val['php'])) { @@ -76,7 +81,8 @@ class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framew * @access public * @return array */ - private function loadSpec($name) { + private function loadSpec($name) + { $filename = dirname(__FILE__) . '/../../../../../vendor/spec/specs/' . $name . '.yml'; if (!file_exists($filename)) { return array(); @@ -106,7 +112,8 @@ class Mustache_Test_FiveThree_Functional_MustacheSpecTest extends PHPUnit_Framew return $data; } - private static function loadTemplate($source, $partials) { + private static function loadTemplate($source, $partials) + { self::$mustache->setPartials($partials); return self::$mustache->loadTemplate($source); diff --git a/test/Mustache/Test/Functional/ExamplesTest.php b/test/Mustache/Test/Functional/ExamplesTest.php index 6c335e8..4dd2dae 100644 --- a/test/Mustache/Test/Functional/ExamplesTest.php +++ b/test/Mustache/Test/Functional/ExamplesTest.php @@ -116,7 +116,7 @@ class Mustache_Test_Functional_ExamplesTest extends PHPUnit_Framework_TestCase * * @param string $path * - * @return array $partials + * @return array $partials */ private function loadPartials($path) { diff --git a/test/Mustache/Test/Functional/MustacheInjectionTest.php b/test/Mustache/Test/Functional/MustacheInjectionTest.php index c6d6337..7621af8 100644 --- a/test/Mustache/Test/Functional/MustacheInjectionTest.php +++ b/test/Mustache/Test/Functional/MustacheInjectionTest.php @@ -49,7 +49,6 @@ class Mustache_Test_Functional_MustacheInjectionTest extends PHPUnit_Framework_T $this->assertEquals('{{ b }}', $tpl->render($data)); } - // sections public function testSectionInjection() @@ -78,7 +77,6 @@ class Mustache_Test_Functional_MustacheInjectionTest extends PHPUnit_Framework_T $this->assertEquals('{{ c }}', $tpl->render($data)); } - // partials public function testPartialInjection() @@ -111,7 +109,6 @@ class Mustache_Test_Functional_MustacheInjectionTest extends PHPUnit_Framework_T $this->assertEquals('{{ b }}', $tpl->render($data)); } - // lambdas public function testLambdaInterpolationInjection() diff --git a/test/fixtures/examples/partials/Partials.php b/test/fixtures/examples/partials/Partials.php index cfa110c..157bb3a 100644 --- a/test/fixtures/examples/partials/Partials.php +++ b/test/fixtures/examples/partials/Partials.php @@ -1,6 +1,7 @@ 'Page Title', 'subtitle' => 'Page Subtitle', From d1b5601bd2f67f5f65e0196df22d06233d7b806b Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 21 Aug 2012 12:59:15 -0700 Subject: [PATCH 07/31] Fix class names in documentation --- src/Mustache/Loader/FilesystemLoader.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mustache/Loader/FilesystemLoader.php b/src/Mustache/Loader/FilesystemLoader.php index dc488f2..f28ab3c 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 { @@ -63,7 +63,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader /** * 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 From 81a9fc37ae09f3f3c8d063f155d9b8504e6c409b Mon Sep 17 00:00:00 2001 From: Paul Dragoonis Date: Wed, 12 Sep 2012 17:55:14 +0200 Subject: [PATCH 08/31] Changing private to protected These fixes came out of this issue (https://github.com/ppi/framework/issues/51). Changing private to protected to that getFileName() can be overridden to use symfony-based templating locators. --- src/Mustache/Loader/FilesystemLoader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Loader/FilesystemLoader.php b/src/Mustache/Loader/FilesystemLoader.php index f28ab3c..2beadb0 100644 --- a/src/Mustache/Loader/FilesystemLoader.php +++ b/src/Mustache/Loader/FilesystemLoader.php @@ -88,7 +88,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader * * @return string Mustache Template source */ - private function loadFile($name) + protected function loadFile($name) { $fileName = $this->getFileName($name); @@ -106,7 +106,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader * * @return string Template file name */ - private function getFileName($name) + protected function getFileName($name) { $fileName = $this->baseDir . '/' . $name; if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) { From f73df2af68b7ffda63efa2fa653d307bbfd6f1e9 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:17:39 -0700 Subject: [PATCH 09/31] Add pragmas to compiler, tokenizer. --- src/Mustache/Compiler.php | 6 ++++++ src/Mustache/Tokenizer.php | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index aff8e88..d7ac74a 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], 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. * From d209d1aea91bc512237841a79464ad3f7030914c Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:18:16 -0700 Subject: [PATCH 10/31] Implement {{% FILTERS }} pragma. --- src/Mustache/Compiler.php | 38 ++++++++++++- src/Mustache/Engine.php | 6 ++- .../Test/FiveThree/Functional/FiltersTest.php | 53 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 test/Mustache/Test/FiveThree/Functional/FiltersTest.php diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index d7ac74a..7bc10b1 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -264,7 +264,7 @@ class Mustache_Compiler } const VARIABLE = ' - $value = $context->%s(%s); + $value = $context->%s(%s);%s if (!is_string($value) && is_callable($value)) { $value = $this->mustache ->loadLambda((string) call_user_func($value)) @@ -284,11 +284,45 @@ 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); + } + + const FILTER = ' + $filter = $context->%s(%s); + $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $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) + { + $chunks = array_map('trim', explode('|', $id)); + $id = array_shift($chunks); + $filters = ''; + + foreach ($chunks as $filter) { + $method = $this->getFindMethod($filter); + $filter = ($method !== 'last') ? var_export($filter, true) : ''; + $filters .= sprintf($this->prepare(self::FILTER, $level), $method, $filter); + } + + return array($id, $filters); } const LINE = '$buffer .= "\n";'; diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 97cc57d..6e99943 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.0.2'; + const SPEC_VERSION = '1.1.2'; + + const PRAGMA_FILTERS = 'FILTERS'; // Template cache private $templates = array(); diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php new file mode 100644 index 0000000..bc5b4de --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -0,0 +1,53 @@ +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)); + } +} From a64a6f82eeec8650f5a536c108b682b7e6a81be2 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:56:56 -0700 Subject: [PATCH 11/31] Handle broken pipes --- src/Mustache/Compiler.php | 6 +++-- .../Test/FiveThree/Functional/FiltersTest.php | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 7bc10b1..48d06ee 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -298,8 +298,10 @@ class Mustache_Compiler } const FILTER = ' - $filter = $context->%s(%s); - $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); + if (!empty($value)) { + $filter = $context->%s(%s); + $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); + } '; /** diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index bc5b4de..ff2c7dd 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -50,4 +50,26 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); } + + public function testBrokenPipe() { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar | baz }}'); + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + ))); + + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + 'bar' => function($value) { return 'BAR'; }, + ))); + + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + 'baz' => function($value) { return 'BAZ'; }, + ))); + + $this->assertEquals('', $tpl->render(array( + 'bar' => function($value) { return 'BAR'; }, + 'baz' => function($value) { return 'BAZ'; }, + ))); + } } From 1cf44d5de9dfca171013e847103468570d4059c0 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 11:05:29 -0700 Subject: [PATCH 12/31] First value in the pipe should be interpolated first. --- src/Mustache/Compiler.php | 4 ++-- .../Mustache/Test/FiveThree/Functional/FiltersTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 48d06ee..293f9e0 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -264,12 +264,12 @@ class Mustache_Compiler } const VARIABLE = ' - $value = $context->%s(%s);%s + $value = $context->%s(%s); if (!is_string($value) && is_callable($value)) { $value = $this->mustache ->loadLambda((string) call_user_func($value)) ->renderInternal($context, $indent); - } + }%s $buffer .= %s%s; '; diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index ff2c7dd..1d1af85 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -72,4 +72,14 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T 'baz' => function($value) { return 'BAZ'; }, ))); } + + 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 :('; + }, + ))); + } } From fc453b5d0100003a734d47fd3584c7fe6f43a40f Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:00:42 -0700 Subject: [PATCH 13/31] Better filters implementation: * Throw UnexpectedValueException when unknown filter is found. * More optimized compiler code. * Falsey initial values will still be fed through the pipe. --- src/Mustache/Compiler.php | 47 +++++++++----- .../Test/FiveThree/Functional/FiltersTest.php | 62 +++++++++++-------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 293f9e0..1c3bcc4 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -297,13 +297,6 @@ class Mustache_Compiler return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); } - const FILTER = ' - if (!empty($value)) { - $filter = $context->%s(%s); - $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); - } - '; - /** * Generate Mustache Template variable filtering PHP source. * @@ -314,17 +307,41 @@ class Mustache_Compiler */ private function getFilters($id, $level) { - $chunks = array_map('trim', explode('|', $id)); - $id = array_shift($chunks); - $filters = ''; + $filters = array_map('trim', explode('|', $id)); + $id = array_shift($filters); - foreach ($chunks as $filter) { - $method = $this->getFindMethod($filter); - $filter = ($method !== 'last') ? var_export($filter, true) : ''; - $filters .= sprintf($this->prepare(self::FILTER, $level), $method, $filter); + return array($id, $this->getFilter($filters, $level)); + } + + const FILTER = ' + $filter = $context->%s(%s); + if (!is_string($filter) && is_callable($filter)) { + $value = call_user_func($filter, $value);%s + } else { + throw new UnexpectedValueException(%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 ''; } - return array($id, $filters); + $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, $this->getFilter($filters, $level + 1), $msg); } const LINE = '$buffer .= "\n";'; diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index 1d1af85..b7d54d3 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -13,15 +13,18 @@ * @group filters * @group functional */ -class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase { +class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase +{ private $mustache; - public function setUp() { + public function setUp() + { $this->mustache = new Mustache_Engine; } - public function testSingleFilter() { + public function testSingleFilter() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate }}'); $this->mustache->addHelper('longdate', function(\DateTime $value) { @@ -34,7 +37,8 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('2000-01-01 12:01:00', $tpl->render($foo)); } - public function testChainedFilters() { + public function testChainedFilters() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate | withbrackets }}'); $this->mustache->addHelper('longdate', function(\DateTime $value) { @@ -51,29 +55,8 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); } - public function testBrokenPipe() { - $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar | baz }}'); - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - ))); - - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - 'bar' => function($value) { return 'BAR'; }, - ))); - - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - 'baz' => function($value) { return 'BAZ'; }, - ))); - - $this->assertEquals('', $tpl->render(array( - 'bar' => function($value) { return 'BAR'; }, - 'baz' => function($value) { return 'BAZ'; }, - ))); - } - - public function testInterpolateFirst() { + public function testInterpolateFirst() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar }}'); $this->assertEquals('win!', $tpl->render(array( 'foo' => 'FOO', @@ -82,4 +65,29 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T }, ))); } + + /** + * @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 | 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'; })), + ); + } } From 2872dd5048eee1fdcba62eee97a4c0cbf348cef2 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:28:56 -0700 Subject: [PATCH 14/31] Another non-callable test case for good measure --- test/Mustache/Test/FiveThree/Functional/FiltersTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index b7d54d3..8c51e37 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -83,6 +83,7 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T 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'; })), From 242e94c91b4853812422288e8622d1295788f837 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:29:20 -0700 Subject: [PATCH 15/31] Avoid indentomatic with long filter chains. --- src/Mustache/Compiler.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 1c3bcc4..fe02419 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -315,11 +315,10 @@ class Mustache_Compiler const FILTER = ' $filter = $context->%s(%s); - if (!is_string($filter) && is_callable($filter)) { - $value = call_user_func($filter, $value);%s - } else { + if (is_string($filter) || !is_callable($filter)) { throw new UnexpectedValueException(%s); } + $value = call_user_func($filter, $value);%s '; /** @@ -341,7 +340,7 @@ class Mustache_Compiler $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, $this->getFilter($filters, $level + 1), $msg); + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level)); } const LINE = '$buffer .= "\n";'; From 5b8e2215e2769cde85f750d7ddb2f5a02c40d215 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 1 Oct 2012 15:44:49 -0700 Subject: [PATCH 16/31] Adding CONTRIBUTING file --- CONTRIBUTING.markdown | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 CONTRIBUTING.markdown diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.markdown new file mode 100644 index 0000000..6dc5cef --- /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 pull request per fix or feature. + * To help with that, do 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) From a6e09fd45701f32fa58ff3c1438d97578803f6cf Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 1 Oct 2012 15:46:14 -0700 Subject: [PATCH 17/31] Learn to English --- CONTRIBUTING.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.markdown index 6dc5cef..fcbf6e7 100644 --- a/CONTRIBUTING.markdown +++ b/CONTRIBUTING.markdown @@ -16,8 +16,8 @@ ### You can do some things to increase the chance that your pull request is accepted the first time: - * Submit pull request per fix or feature. - * To help with that, do your work in a feature branch (e.g. `feature/my-alsome-feature`). + * 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. From 4da2fe43315c2855a74f2f93f7a5f82500df2f4e Mon Sep 17 00:00:00 2001 From: Rolando Henry Date: Thu, 25 Oct 2012 15:27:27 -0400 Subject: [PATCH 18/31] changed submodule url to bypass proxy problem --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 1560863c49cbd97861b81e99fbbe8b253ccd41f5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 28 Nov 2012 12:58:20 -0800 Subject: [PATCH 19/31] Failing test for empty-string filesystem loader extension. See #123 --- test/Mustache/Test/Loader/FilesystemLoaderTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Mustache/Test/Loader/FilesystemLoaderTest.php b/test/Mustache/Test/Loader/FilesystemLoaderTest.php index f0f0997..d12a6ff 100644 --- a/test/Mustache/Test/Loader/FilesystemLoaderTest.php +++ b/test/Mustache/Test/Loader/FilesystemLoaderTest.php @@ -30,6 +30,19 @@ class Mustache_Test_Loader_FilesystemLoaderTest extends PHPUnit_Framework_TestCa $this->assertEquals('two contents', $loader->load('two.mustache')); } + public function testEmptyExtensionString() + { + $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates'); + + $loader = new Mustache_Loader_FilesystemLoader($baseDir, array('extension' => '')); + $this->assertEquals('one contents', $loader->load('one.mustache')); + $this->assertEquals('alpha contents', $loader->load('alpha.ms')); + + $loader = new Mustache_Loader_FilesystemLoader($baseDir, array('extension' => null)); + $this->assertEquals('two contents', $loader->load('two.mustache')); + $this->assertEquals('beta contents', $loader->load('beta.ms')); + } + /** * @expectedException RuntimeException */ From 042d537992063b86e5d4d064e01357f6869e42e8 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 28 Nov 2012 12:59:06 -0800 Subject: [PATCH 20/31] Allow "empty" filesystem loader extension. Fixes #123 --- src/Mustache/Loader/FilesystemLoader.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Loader/FilesystemLoader.php b/src/Mustache/Loader/FilesystemLoader.php index 2beadb0..bbd2a43 100644 --- a/src/Mustache/Loader/FilesystemLoader.php +++ b/src/Mustache/Loader/FilesystemLoader.php @@ -55,8 +55,12 @@ 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'], '.'); + } } } From 197a2b68077c8045acdaaba51be0735b18bb26f9 Mon Sep 17 00:00:00 2001 From: jandreasn Date: Tue, 23 Oct 2012 09:28:40 +0200 Subject: [PATCH 21/31] Added option to set cache file permissions. --- src/Mustache/Engine.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 6e99943..a45f320 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -39,6 +39,7 @@ class Mustache_Engine private $helpers; private $escape; private $charset = 'UTF-8'; + private $cacheFilePerm = 0644; /** * Mustache class constructor. @@ -76,6 +77,9 @@ class Mustache_Engine * * // character set for `htmlspecialchars`. Defaults to 'UTF-8' * 'charset' => 'ISO-8859-1', + * + * // permissions for cache files. Defaults to 0644 + * 'cache_file_perm' => 0666, * ); * * @param array $options (default: array()) @@ -117,6 +121,10 @@ class Mustache_Engine if (isset($options['charset'])) { $this->charset = $options['charset']; } + + if (isset($options['cache_file_perm'])) { + $this->cacheFilePerm = $options['cache_file_perm']; + } } /** @@ -580,7 +588,7 @@ class Mustache_Engine $tempFile = tempnam(dirname($fileName), basename($fileName)); if (false !== @file_put_contents($tempFile, $source)) { if (@rename($tempFile, $fileName)) { - chmod($fileName, 0644); + chmod($fileName, $this->cacheFilePerm); return; } From 1792140afa18f0a84c9380c1fb5f536e04488a7d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 28 Nov 2012 20:24:14 -0800 Subject: [PATCH 22/31] Default cache permissions to the current umask. Use system-defined umask by default, and strongly recommend going that route, but allow overrides. --- src/Mustache/Engine.php | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index a45f320..ab92a7a 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -39,7 +39,7 @@ class Mustache_Engine private $helpers; private $escape; private $charset = 'UTF-8'; - private $cacheFilePerm = 0644; + private $cacheFileMode = null; /** * Mustache class constructor. @@ -75,11 +75,12 @@ class Mustache_Engine * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); * }, * - * // character set for `htmlspecialchars`. Defaults to 'UTF-8' + * // Character set for `htmlspecialchars`. Defaults to 'UTF-8' * 'charset' => 'ISO-8859-1', * - * // permissions for cache files. Defaults to 0644 - * 'cache_file_perm' => 0666, + * // 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, * ); * * @param array $options (default: array()) @@ -122,8 +123,8 @@ class Mustache_Engine $this->charset = $options['charset']; } - if (isset($options['cache_file_perm'])) { - $this->cacheFilePerm = $options['cache_file_perm']; + if (isset($options['cache_file_mode'])) { + $this->cacheFileMode = $options['cache_file_mode']; } } @@ -572,7 +573,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 $fileName * * @param string $fileName * @param string $source @@ -581,14 +582,19 @@ 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)) { + @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)); + $tempFile = tempnam($dirName, basename($fileName)); if (false !== @file_put_contents($tempFile, $source)) { if (@rename($tempFile, $fileName)) { - chmod($fileName, $this->cacheFilePerm); + $mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask()); + @chmod($fileName, $mode); return; } From 2e97db3fc0bdbdf884dac5e3d52256c831203884 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 28 Nov 2012 20:28:03 -0800 Subject: [PATCH 23/31] Clean up options documentation, order. --- src/Mustache/Engine.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index ab92a7a..34d3ce2 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -47,13 +47,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. @@ -67,20 +71,16 @@ 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', - * - * // 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, * ); * * @param array $options (default: array()) @@ -95,6 +95,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']); } @@ -122,10 +126,6 @@ class Mustache_Engine if (isset($options['charset'])) { $this->charset = $options['charset']; } - - if (isset($options['cache_file_mode'])) { - $this->cacheFileMode = $options['cache_file_mode']; - } } /** From cd4e6b3b2697d09f210e9f003fce2da0c9b1b6e5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 27 Sep 2012 17:59:32 -0700 Subject: [PATCH 24/31] Initial logging proposal. C.f. #112 Add logging to the Mustache Engine. * Add a Logger interface. * If a Logger instance is passed to the Engine constructor (or added later via setLogger) template compiling, caching, errors and missing partials will be logged. * Add a Stream Logger and Monolog Logger implementation. You should use the Monolog Logger. --- src/Mustache/Engine.php | 96 ++++++++++++++++++++++++- src/Mustache/Logger.php | 75 ++++++++++++++++++++ src/Mustache/Logger/AbstractLogger.php | 98 ++++++++++++++++++++++++++ src/Mustache/Logger/MonologLogger.php | 59 ++++++++++++++++ src/Mustache/Logger/StreamLogger.php | 63 +++++++++++++++++ 5 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 src/Mustache/Logger.php create mode 100644 src/Mustache/Logger/AbstractLogger.php create mode 100644 src/Mustache/Logger/MonologLogger.php create mode 100644 src/Mustache/Logger/StreamLogger.php diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 34d3ce2..b5b7b22 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -34,12 +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 $cacheFileMode = null; + private $logger; /** * Mustache class constructor. @@ -81,6 +82,9 @@ class Mustache_Engine * * // 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. + * 'logger' => new Mustache_StreamLogger('php://stderr'), * ); * * @param array $options (default: array()) @@ -126,6 +130,10 @@ class Mustache_Engine if (isset($options['charset'])) { $this->charset = $options['charset']; } + + if (isset($options['logger'])) { + $this->setLogger($options['logger']); + } } /** @@ -330,6 +338,26 @@ class Mustache_Engine $this->getHelpers()->remove($name); } + /** + * Set the Mustache Logger instance. + * + * @param Mustache_Logger $logger + */ + public function setLogger(Mustache_Logger $logger) + { + $this->logger = $logger; + } + + /** + * Get the current Mustache Logger instance. + * + * @return Mustache_Logger + */ + public function getLogger() + { + return $this->logger; + } + /** * Set the Mustache Tokenizer instance. * @@ -453,7 +481,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, + sprintf('Partial not found: "%s"', $name), + array('name' => $name) + ); } } @@ -496,15 +529,33 @@ class Mustache_Engine if (!class_exists($className, false)) { if ($fileName = $this->getCacheFilename($source)) { if (!is_file($fileName)) { + $this->log( + Mustache_Logger::DEBUG, + sprintf('Writing "%s" class to template cache: "%s"', $className, $fileName), + array('className' => $className, 'fileName' => $fileName) + ); + $this->writeCacheFile($fileName, $this->compile($source)); } require_once $fileName; } else { + $this->log( + Mustache_Logger::WARNING, + sprintf('Template cache disabled, evaluating "%s" class at runtime', $className), + array('className' => $className) + ); + eval('?>'.$this->compile($source)); } } + $this->log( + Mustache_Logger::DEBUG, + sprintf('Instantiating template: "%s"', $className), + array('className' => $className) + ); + $this->templates[$className] = new $className($this); } @@ -553,6 +604,12 @@ class Mustache_Engine $tree = $this->parse($source); $name = $this->getTemplateClassName($source); + $this->log( + Mustache_Logger::INFO, + sprintf('Compiling template to "%s" class', $name), + array('name' => $name) + ); + return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset); } @@ -573,7 +630,7 @@ class Mustache_Engine /** * Helper method to dump a generated Mustache Template subclass to the file cache. * - * @throws RuntimeException if unable to create the cache directory or write $fileName + * @throws RuntimeException if unable to create the cache directory or write to $fileName. * * @param string $fileName * @param string $source @@ -584,12 +641,25 @@ class Mustache_Engine { $dirName = dirname($fileName); if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + sprintf('Creating Mustache template cache directory: "%s"', $dirName), + array('dirName' => $dirName) + ); + @mkdir($dirName, 0777, true); if (!is_dir($dirName)) { throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); } + } + $this->log( + Mustache_Logger::DEBUG, + sprintf('Caching compiled template to "%s"', dirname($fileName)), + array('filename' => $fileName) + ); + $tempFile = tempnam($dirName, basename($fileName)); if (false !== @file_put_contents($tempFile, $source)) { if (@rename($tempFile, $fileName)) { @@ -598,8 +668,28 @@ class Mustache_Engine return; } + + $this->log( + Mustache_Logger::ERROR, + sprintf('Unable to rename Mustache temp cache file: "%s" -> "%s"', $tempFile, $fileName), + array('tempFile' => $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/Logger.php b/src/Mustache/Logger.php new file mode 100644 index 0000000..0200970 --- /dev/null +++ b/src/Mustache/Logger.php @@ -0,0 +1,75 @@ + 'DEBUG', + 200 => 'INFO', + 250 => 'NOTICE', + 300 => 'WARNING', + 400 => 'ERROR', + 500 => 'CRITICAL', + 550 => 'ALERT', + 600 => 'EMERGENCY', + ); + + /** + * Abstract Logger constructor. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level The minimum logging level which will be written + */ + public function __construct($level = self::ERROR) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + $this->level = $level; + } + + /** + * Adds a log record. + * + * @see Mustache_Logger_AbstractLogger::write + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + public function log($level, $message, array $context = array()) + { + if ($level >= $this->level) { + $this->writeLog($level, $message, $context); + } + } + + /** + * Gets the name of the logging level. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level + * + * @return string + */ + public static function getLevelName($level) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + return self::$levels[$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 + */ + public static function formatLine($level, $message, array $context = array()) + { + return sprintf('%s: %s %s', self::getLevelName($level), (string) $message, json_encode($context)); + } + + /** + * Write a record to the log. Implemented by subclasses. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + abstract protected function write($level, $message, array $context = array()); +} diff --git a/src/Mustache/Logger/MonologLogger.php b/src/Mustache/Logger/MonologLogger.php new file mode 100644 index 0000000..2123343 --- /dev/null +++ b/src/Mustache/Logger/MonologLogger.php @@ -0,0 +1,59 @@ +logger = $logger; + } + + /** + * Adds a log record. + * + * Overload the AbstractLogger::log method, because all log messages should + * be passed through to Monolog regardless of the log level. Monolog will + * handle ignoring the messages it doesn't care about. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + public function log($level, $message, array $context = array()) + { + $this->write($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 write($level, $message, array $context = array()) + { + $this->logger->addRecord($level, $message, $context); + } +} diff --git a/src/Mustache/Logger/StreamLogger.php b/src/Mustache/Logger/StreamLogger.php new file mode 100644 index 0000000..05939bf --- /dev/null +++ b/src/Mustache/Logger/StreamLogger.php @@ -0,0 +1,63 @@ +stream = $stream; + } else { + $this->url = $stream; + } + } + + /** + * 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 write($level, $message, array $context = array()) + { + if ($this->stream === null) { + 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)) { + throw new UnexpectedValueException(sprintf('The stream or file "%s" could not be opened.', $this->url)); + } + } + + fwrite($this->stream, self::formatLine($level, $message, $context)); + } +} From 782017e4619f12516bf230a45b6f54686ea285ea Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 28 Sep 2012 11:03:52 -0400 Subject: [PATCH 25/31] Add get/setLevel to AbstractLogger. --- src/Mustache/Logger/AbstractLogger.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Mustache/Logger/AbstractLogger.php b/src/Mustache/Logger/AbstractLogger.php index d22f166..498cdce 100644 --- a/src/Mustache/Logger/AbstractLogger.php +++ b/src/Mustache/Logger/AbstractLogger.php @@ -33,6 +33,18 @@ abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger * @param integer $level The minimum logging level which will be written */ public function __construct($level = self::ERROR) + { + $this->setLevel($level); + } + + /** + * 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); @@ -41,6 +53,16 @@ abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger $this->level = $level; } + /** + * Get the current minimum logging level. + * + * @return integer + */ + public function getLevel() + { + return $this->level; + } + /** * Adds a log record. * From 8c059f566b3fad10a98767821df5d97ea71eef64 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 28 Sep 2012 11:04:12 -0400 Subject: [PATCH 26/31] MonologLogger should not extend AbstractLogger. --- src/Mustache/Logger/MonologLogger.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/Mustache/Logger/MonologLogger.php b/src/Mustache/Logger/MonologLogger.php index 2123343..a6a9ea0 100644 --- a/src/Mustache/Logger/MonologLogger.php +++ b/src/Mustache/Logger/MonologLogger.php @@ -12,7 +12,7 @@ /** * A Mustache Monolog Logger adapter. */ -class MonologLogger extends Mustache_Logger_AbstractLogger +class MonologLogger implements Mustache_Logger { protected $logger; @@ -41,18 +41,6 @@ class MonologLogger extends Mustache_Logger_AbstractLogger * @param array $context The log context */ public function log($level, $message, array $context = array()) - { - $this->write($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 write($level, $message, array $context = array()) { $this->logger->addRecord($level, $message, $context); } From d1fb1d86c546a83f02ae3b6023d28512b14a7832 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 28 Sep 2012 11:04:29 -0400 Subject: [PATCH 27/31] Handle prematurely closed streams in StreamLogger. --- src/Mustache/Logger/StreamLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mustache/Logger/StreamLogger.php b/src/Mustache/Logger/StreamLogger.php index 05939bf..727a626 100644 --- a/src/Mustache/Logger/StreamLogger.php +++ b/src/Mustache/Logger/StreamLogger.php @@ -47,7 +47,7 @@ class StreamLogger extends Mustache_Logger_AbstractLogger */ protected function write($level, $message, array $context = array()) { - if ($this->stream === null) { + 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().'); } From 7522e2206dc3593f3314d6c132c4c8a8cb9a164b Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 30 Nov 2012 19:01:47 -0800 Subject: [PATCH 28/31] Update loging to match (proposed) logging PSR. --- src/Mustache/Engine.php | 10 +- src/Mustache/Logger.php | 138 ++++++++++++----- src/Mustache/Logger/AbstractLogger.php | 120 --------------- src/Mustache/Logger/MonologLogger.php | 47 ------ src/Mustache/Logger/StreamLogger.php | 204 ++++++++++++++++++++++++- 5 files changed, 307 insertions(+), 212 deletions(-) delete mode 100644 src/Mustache/Logger/AbstractLogger.php delete mode 100644 src/Mustache/Logger/MonologLogger.php diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index b5b7b22..43ac4f6 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -341,17 +341,21 @@ class Mustache_Engine /** * Set the Mustache Logger instance. * - * @param Mustache_Logger $logger + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger */ - public function setLogger(Mustache_Logger $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 + * @return Mustache_Logger|Psr\Log\LoggerInterface */ public function getLogger() { diff --git a/src/Mustache/Logger.php b/src/Mustache/Logger.php index 0200970..0a6b027 100644 --- a/src/Mustache/Logger.php +++ b/src/Mustache/Logger.php @@ -10,66 +10,126 @@ */ /** - * The Mustache Logger interface. + * Describes a Mustache logger instance + * + * This is identical to the Psr\Log\LoggerInterface. + * + * The message MUST be a string or object implementing __toString(). + * + * The message MAY contain placeholders in the form: %foo% where foo + * will be replaced by the context data in key "foo". + * + * The context array can contain arbitrary data, the only assumption that + * can be made by implementors is that if an Exception instance is given + * to produce a stack trace, it MUST be in a key named "exception". + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md + * for the full interface specification. */ interface Mustache_Logger { /** - * Detailed debug information + * Psr\Log compatible log levels */ - const DEBUG = 100; + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; /** - * Interesting events + * System is unusable. * - * Examples: User logs in, SQL logs. + * @param string $message + * @param array $context + * @return null */ - const INFO = 200; + public function emergency($message, array $context = array()); /** - * Uncommon events - */ - const NOTICE = 250; - - /** - * Exceptional occurrences that are not errors + * Action must be taken immediately. * - * Examples: Use of deprecated APIs, poor use of an API, - * undesirable things that are not necessarily wrong. + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * @return null */ - const WARNING = 300; + public function alert($message, array $context = array()); /** - * Runtime errors - */ - const ERROR = 400; - - /** - * Critical conditions + * Critical conditions. * * Example: Application component unavailable, unexpected exception. - */ - const CRITICAL = 500; - - /** - * 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 + * @return null */ - const ALERT = 550; + public function critical($message, array $context = array()); /** - * Urgent alert. - */ - const EMERGENCY = 600; - - /** - * Adds a log record. + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. * - * @param integer $level The logging level - * @param string $message The log message - * @param array $context The log context + * @param string $message + * @param array $context + * @return null + */ + public function error($message, array $context = array()); + + /** + * 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 + * @return null + */ + public function warning($message, array $context = array()); + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * @return null + */ + public function notice($message, array $context = array()); + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * @return null + */ + public function info($message, array $context = array()); + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * @return null + */ + public function debug($message, array $context = array()); + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null */ public function log($level, $message, array $context = array()); -} +} \ No newline at end of file diff --git a/src/Mustache/Logger/AbstractLogger.php b/src/Mustache/Logger/AbstractLogger.php deleted file mode 100644 index 498cdce..0000000 --- a/src/Mustache/Logger/AbstractLogger.php +++ /dev/null @@ -1,120 +0,0 @@ - 'DEBUG', - 200 => 'INFO', - 250 => 'NOTICE', - 300 => 'WARNING', - 400 => 'ERROR', - 500 => 'CRITICAL', - 550 => 'ALERT', - 600 => 'EMERGENCY', - ); - - /** - * Abstract Logger constructor. - * - * @throws InvalidArgumentException if the logging level is unknown. - * - * @param integer $level The minimum logging level which will be written - */ - public function __construct($level = self::ERROR) - { - $this->setLevel($level); - } - - /** - * 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; - } - - /** - * Adds a log record. - * - * @see Mustache_Logger_AbstractLogger::write - * - * @param integer $level The logging level - * @param string $message The log message - * @param array $context The log context - */ - public function log($level, $message, array $context = array()) - { - if ($level >= $this->level) { - $this->writeLog($level, $message, $context); - } - } - - /** - * Gets the name of the logging level. - * - * @throws InvalidArgumentException if the logging level is unknown. - * - * @param integer $level - * - * @return string - */ - public static function getLevelName($level) - { - if (!array_key_exists($level, self::$levels)) { - throw new InvalidArgumentException('Unexpected logging level: ' . $level); - } - - return self::$levels[$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 - */ - public static function formatLine($level, $message, array $context = array()) - { - return sprintf('%s: %s %s', self::getLevelName($level), (string) $message, json_encode($context)); - } - - /** - * Write a record to the log. Implemented by subclasses. - * - * @param integer $level The logging level - * @param string $message The log message - * @param array $context The log context - */ - abstract protected function write($level, $message, array $context = array()); -} diff --git a/src/Mustache/Logger/MonologLogger.php b/src/Mustache/Logger/MonologLogger.php deleted file mode 100644 index a6a9ea0..0000000 --- a/src/Mustache/Logger/MonologLogger.php +++ /dev/null @@ -1,47 +0,0 @@ -logger = $logger; - } - - /** - * Adds a log record. - * - * Overload the AbstractLogger::log method, because all log messages should - * be passed through to Monolog regardless of the log level. Monolog will - * handle ignoring the messages it doesn't care about. - * - * @param integer $level The logging level - * @param string $message The log message - * @param array $context The log context - */ - public function log($level, $message, array $context = array()) - { - $this->logger->addRecord($level, $message, $context); - } -} diff --git a/src/Mustache/Logger/StreamLogger.php b/src/Mustache/Logger/StreamLogger.php index 727a626..57e90f3 100644 --- a/src/Mustache/Logger/StreamLogger.php +++ b/src/Mustache/Logger/StreamLogger.php @@ -18,8 +18,19 @@ * * Hint: Try `php://stderr` for your stream URL. */ -class StreamLogger extends Mustache_Logger_AbstractLogger +class StreamLogger implements Mustache_Logger { + protected static $levels = array( + self::DEBUG => 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; @@ -29,7 +40,7 @@ class StreamLogger extends Mustache_Logger_AbstractLogger */ public function __construct($stream, $level = Mustache_Logger::ERROR) { - parent::__construct($level); + $this->setLevel($level); if (is_resource($stream)) { $this->stream = $stream; @@ -38,6 +49,158 @@ class StreamLogger extends Mustache_Logger_AbstractLogger } } + /** + * 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; + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * @return null + */ + public function emergency($message, array $context = array()) + { + $this->log(self::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 + * @return null + */ + public function alert($message, array $context = array()) + { + $this->log(self::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * @return null + */ + public function critical($message, array $context = array()) + { + $this->log(self::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * @return null + */ + public function error($message, array $context = array()) + { + $this->log(self::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 + * @return null + */ + public function warning($message, array $context = array()) + { + $this->log(self::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * @return null + */ + public function notice($message, array $context = array()) + { + $this->log(self::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * @return null + */ + public function info($message, array $context = array()) + { + $this->log(self::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * @return null + */ + public function debug($message, array $context = array()) + { + $this->log(self::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + 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] >= $this->level) { + $this->writeLog($level, $message, $context); + } + } + /** * Write a record to the log. * @@ -45,7 +208,7 @@ class StreamLogger extends Mustache_Logger_AbstractLogger * @param string $message The log message * @param array $context The log context */ - protected function write($level, $message, array $context = array()) + protected function writeLog($level, $message, array $context = array()) { if (!is_resource($this->stream)) { if (!isset($this->url)) { @@ -60,4 +223,39 @@ class StreamLogger extends Mustache_Logger_AbstractLogger 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) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $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 + */ + protected static function formatLine($level, $message, array $context = array()) + { + $message = (string) $message; + foreach ($context as $key => $val) { + $message = str_replace('%'.$key.'%', $val, $message); + } + + return sprintf('%s: %s %s', self::getLevelName($level), (string) $message, json_encode($context)); + } } From 1515f5126b35436228f40ee3338c8ede5398d7fb Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 11 Jan 2013 22:41:28 -0800 Subject: [PATCH 29/31] Update logger to implement accepted PSR log spec Add tests! --- src/Mustache/Engine.php | 22 +- src/Mustache/Logger.php | 4 +- src/Mustache/Logger/AbstractLogger.php | 121 ++++++++++ src/Mustache/Logger/StreamLogger.php | 159 ++++---------- test/Mustache/Test/EngineTest.php | 79 ++++++- .../Test/Logger/AbstractLoggerTest.php | 60 +++++ .../Mustache/Test/Logger/StreamLoggerTest.php | 206 ++++++++++++++++++ 7 files changed, 521 insertions(+), 130 deletions(-) create mode 100644 src/Mustache/Logger/AbstractLogger.php create mode 100644 test/Mustache/Test/Logger/AbstractLoggerTest.php create mode 100644 test/Mustache/Test/Logger/StreamLoggerTest.php diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 43ac4f6..16e2e86 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -488,7 +488,7 @@ class Mustache_Engine // If the named partial cannot be found, log then return null. $this->log( Mustache_Logger::WARNING, - sprintf('Partial not found: "%s"', $name), + 'Partial not found: "{name}"', array('name' => $name) ); } @@ -535,7 +535,7 @@ class Mustache_Engine if (!is_file($fileName)) { $this->log( Mustache_Logger::DEBUG, - sprintf('Writing "%s" class to template cache: "%s"', $className, $fileName), + 'Writing "{className}" class to template cache: "{fileName}"', array('className' => $className, 'fileName' => $fileName) ); @@ -546,7 +546,7 @@ class Mustache_Engine } else { $this->log( Mustache_Logger::WARNING, - sprintf('Template cache disabled, evaluating "%s" class at runtime', $className), + 'Template cache disabled, evaluating "{className}" class at runtime', array('className' => $className) ); @@ -556,7 +556,7 @@ class Mustache_Engine $this->log( Mustache_Logger::DEBUG, - sprintf('Instantiating template: "%s"', $className), + 'Instantiating template: "{className}"', array('className' => $className) ); @@ -610,8 +610,8 @@ class Mustache_Engine $this->log( Mustache_Logger::INFO, - sprintf('Compiling template to "%s" class', $name), - array('name' => $name) + 'Compiling template to "{className}" class', + array('className' => $name) ); return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset); @@ -647,7 +647,7 @@ class Mustache_Engine if (!is_dir($dirName)) { $this->log( Mustache_Logger::INFO, - sprintf('Creating Mustache template cache directory: "%s"', $dirName), + 'Creating Mustache template cache directory: "{dirName}"', array('dirName' => $dirName) ); @@ -660,8 +660,8 @@ class Mustache_Engine $this->log( Mustache_Logger::DEBUG, - sprintf('Caching compiled template to "%s"', dirname($fileName)), - array('filename' => $fileName) + 'Caching compiled template to "{fileName}"', + array('fileName' => $fileName) ); $tempFile = tempnam($dirName, basename($fileName)); @@ -675,8 +675,8 @@ class Mustache_Engine $this->log( Mustache_Logger::ERROR, - sprintf('Unable to rename Mustache temp cache file: "%s" -> "%s"', $tempFile, $fileName), - array('tempFile' => $tempFile, 'fileName' => $fileName) + 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', + array('tempName' => $tempFile, 'fileName' => $fileName) ); } diff --git a/src/Mustache/Logger.php b/src/Mustache/Logger.php index 0a6b027..e08359a 100644 --- a/src/Mustache/Logger.php +++ b/src/Mustache/Logger.php @@ -16,7 +16,7 @@ * * The message MUST be a string or object implementing __toString(). * - * The message MAY contain placeholders in the form: %foo% where foo + * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data, the only assumption that @@ -132,4 +132,4 @@ interface Mustache_Logger * @return null */ public function log($level, $message, array $context = array()); -} \ No newline at end of file +} diff --git a/src/Mustache/Logger/AbstractLogger.php b/src/Mustache/Logger/AbstractLogger.php new file mode 100644 index 0000000..bb057d6 --- /dev/null +++ b/src/Mustache/Logger/AbstractLogger.php @@ -0,0 +1,121 @@ +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 index 57e90f3..7f3fd50 100644 --- a/src/Mustache/Logger/StreamLogger.php +++ b/src/Mustache/Logger/StreamLogger.php @@ -18,7 +18,7 @@ * * Hint: Try `php://stderr` for your stream URL. */ -class StreamLogger implements Mustache_Logger +class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger { protected static $levels = array( self::DEBUG => 100, @@ -35,6 +35,8 @@ class StreamLogger implements Mustache_Logger 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 */ @@ -49,6 +51,16 @@ class StreamLogger implements Mustache_Logger } } + /** + * Close stream resources. + */ + public function __destruct() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + /** * Set the minimum logging level. * @@ -75,120 +87,14 @@ class StreamLogger implements Mustache_Logger return $this->level; } - /** - * System is unusable. - * - * @param string $message - * @param array $context - * @return null - */ - public function emergency($message, array $context = array()) - { - $this->log(self::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 - * @return null - */ - public function alert($message, array $context = array()) - { - $this->log(self::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * @return null - */ - public function critical($message, array $context = array()) - { - $this->log(self::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * @return null - */ - public function error($message, array $context = array()) - { - $this->log(self::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 - * @return null - */ - public function warning($message, array $context = array()) - { - $this->log(self::WARNING, $message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * @return null - */ - public function notice($message, array $context = array()) - { - $this->log(self::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * @return null - */ - public function info($message, array $context = array()) - { - $this->log(self::INFO, $message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * @return null - */ - public function debug($message, array $context = array()) - { - $this->log(self::DEBUG, $message, $context); - } - /** * Logs with an arbitrary level. * + * @throws InvalidArgumentException if the logging level is unknown. + * * @param mixed $level * @param string $message * @param array $context - * @return null */ public function log($level, $message, array $context = array()) { @@ -196,7 +102,7 @@ class StreamLogger implements Mustache_Logger throw new InvalidArgumentException('Unexpected logging level: ' . $level); } - if (self::$levels[$level] >= $this->level) { + if (self::$levels[$level] >= self::$levels[$this->level]) { $this->writeLog($level, $message, $context); } } @@ -217,7 +123,9 @@ class StreamLogger implements Mustache_Logger $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 } } @@ -235,10 +143,6 @@ class StreamLogger implements Mustache_Logger */ protected static function getLevelName($level) { - if (!array_key_exists($level, self::$levels)) { - throw new InvalidArgumentException('Unexpected logging level: ' . $level); - } - return strtoupper($level); } @@ -248,14 +152,37 @@ class StreamLogger implements Mustache_Logger * @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) { - $message = str_replace('%'.$key.'%', $val, $message); + $replace['{' . $key . '}'] = $val; } - return sprintf('%s: %s %s', self::getLevelName($level), (string) $message, json_encode($context)); + // interpolate replacement values into the the message and return + return strtr($message, $replace); } } diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index 7178786..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, '/').'/'; diff --git a/test/Mustache/Test/Logger/AbstractLoggerTest.php b/test/Mustache/Test/Logger/AbstractLoggerTest.php new file mode 100644 index 0000000..733b2eb --- /dev/null +++ b/test/Mustache/Test/Logger/AbstractLoggerTest.php @@ -0,0 +1,60 @@ +emergency('emergency message'); + $logger->alert('alert message'); + $logger->critical('critical message'); + $logger->error('error message'); + $logger->warning('warning message'); + $logger->notice('notice message'); + $logger->info('info message'); + $logger->debug('debug message'); + + $expected = array( + array(Mustache_Logger::EMERGENCY, 'emergency message', array()), + array(Mustache_Logger::ALERT, 'alert message', array()), + array(Mustache_Logger::CRITICAL, 'critical message', array()), + array(Mustache_Logger::ERROR, 'error message', array()), + array(Mustache_Logger::WARNING, 'warning message', array()), + array(Mustache_Logger::NOTICE, 'notice message', array()), + array(Mustache_Logger::INFO, 'info message', array()), + array(Mustache_Logger::DEBUG, 'debug message', array()), + ); + + $this->assertEquals($expected, $logger->log); + } +} + +class Mustache_Test_Logger_TestLogger extends Mustache_Logger_AbstractLogger +{ + public $log = array(); + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + $this->log[] = array($level, $message, $context); + } +} diff --git a/test/Mustache/Test/Logger/StreamLoggerTest.php b/test/Mustache/Test/Logger/StreamLoggerTest.php new file mode 100644 index 0000000..9dfd4c2 --- /dev/null +++ b/test/Mustache/Test/Logger/StreamLoggerTest.php @@ -0,0 +1,206 @@ +log(Mustache_Logger::CRITICAL, 'message'); + + $this->assertEquals("CRITICAL: message\n", file_get_contents($name)); + } + + public function testAcceptsResource() + { + $name = tempnam(sys_get_temp_dir(), 'mustache-test'); + $file = fopen($name, 'a'); + $logger = new Mustache_Logger_StreamLogger($file); + $logger->log(Mustache_Logger::CRITICAL, 'message'); + + $this->assertEquals("CRITICAL: message\n", file_get_contents($name)); + } + + /** + * @expectedException LogicException + */ + public function testPrematurelyClosedStreamThrowsException() + { + $stream = tmpfile(); + $logger = new Mustache_Logger_StreamLogger($stream); + fclose($stream); + + $logger->log(Mustache_Logger::CRITICAL, 'message'); + } + + /** + * @dataProvider getLevels + */ + public function testLoggingThresholds($logLevel, $level, $shouldLog) + { + $stream = tmpfile(); + $logger = new Mustache_Logger_StreamLogger($stream, $logLevel); + $logger->log($level, "logged"); + + rewind($stream); + $result = fread($stream, 1024); + + if ($shouldLog) { + $this->assertContains("logged", $result); + } else { + $this->assertEmpty($result); + } + } + + public function getLevels() + { + // $logLevel, $level, $shouldLog + return array( + // identities + array(Mustache_Logger::EMERGENCY, Mustache_Logger::EMERGENCY, true), + array(Mustache_Logger::ALERT, Mustache_Logger::ALERT, true), + array(Mustache_Logger::CRITICAL, Mustache_Logger::CRITICAL, true), + array(Mustache_Logger::ERROR, Mustache_Logger::ERROR, true), + array(Mustache_Logger::WARNING, Mustache_Logger::WARNING, true), + array(Mustache_Logger::NOTICE, Mustache_Logger::NOTICE, true), + array(Mustache_Logger::INFO, Mustache_Logger::INFO, true), + array(Mustache_Logger::DEBUG, Mustache_Logger::DEBUG, true), + + // one above + array(Mustache_Logger::ALERT, Mustache_Logger::EMERGENCY, true), + array(Mustache_Logger::CRITICAL, Mustache_Logger::ALERT, true), + array(Mustache_Logger::ERROR, Mustache_Logger::CRITICAL, true), + array(Mustache_Logger::WARNING, Mustache_Logger::ERROR, true), + array(Mustache_Logger::NOTICE, Mustache_Logger::WARNING, true), + array(Mustache_Logger::INFO, Mustache_Logger::NOTICE, true), + array(Mustache_Logger::DEBUG, Mustache_Logger::INFO, true), + + // one below + array(Mustache_Logger::EMERGENCY, Mustache_Logger::ALERT, false), + array(Mustache_Logger::ALERT, Mustache_Logger::CRITICAL, false), + array(Mustache_Logger::CRITICAL, Mustache_Logger::ERROR, false), + array(Mustache_Logger::ERROR, Mustache_Logger::WARNING, false), + array(Mustache_Logger::WARNING, Mustache_Logger::NOTICE, false), + array(Mustache_Logger::NOTICE, Mustache_Logger::INFO, false), + array(Mustache_Logger::INFO, Mustache_Logger::DEBUG, false), + ); + } + + /** + * @dataProvider getLogMessages + */ + public function testLogging($level, $message, $context, $expected) + { + $stream = tmpfile(); + $logger = new Mustache_Logger_StreamLogger($stream, Mustache_Logger::DEBUG); + $logger->log($level, $message, $context); + + rewind($stream); + $result = fread($stream, 1024); + + $this->assertEquals($expected, $result); + } + + public function getLogMessages() + { + // $level, $message, $context, $expected + return array( + array(Mustache_Logger::DEBUG, 'debug message', array(), "DEBUG: debug message\n"), + array(Mustache_Logger::INFO, 'info message', array(), "INFO: info message\n"), + array(Mustache_Logger::NOTICE, 'notice message', array(), "NOTICE: notice message\n"), + array(Mustache_Logger::WARNING, 'warning message', array(), "WARNING: warning message\n"), + array(Mustache_Logger::ERROR, 'error message', array(), "ERROR: error message\n"), + array(Mustache_Logger::CRITICAL, 'critical message', array(), "CRITICAL: critical message\n"), + array(Mustache_Logger::ALERT, 'alert message', array(), "ALERT: alert message\n"), + array(Mustache_Logger::EMERGENCY, 'emergency message', array(), "EMERGENCY: emergency message\n"), + + // with context + array( + Mustache_Logger::ERROR, + 'error message', + array('name' => 'foo', 'number' => 42), + "ERROR: error message\n" + ), + + // with interpolation + array( + Mustache_Logger::ERROR, + 'error {name}-{number}', + array('name' => 'foo', 'number' => 42), + "ERROR: error foo-42\n" + ), + + // with iterpolation false positive + array( + Mustache_Logger::ERROR, + 'error {nothing}', + array('name' => 'foo', 'number' => 42), + "ERROR: error {nothing}\n" + ), + + // with interpolation injection + array( + Mustache_Logger::ERROR, + '{foo}', + array('foo' => '{bar}', 'bar' => 'FAIL'), + "ERROR: {bar}\n" + ), + ); + } + + public function testChangeLoggingLevels() + { + $stream = tmpfile(); + $logger = new Mustache_Logger_StreamLogger($stream); + + $logger->setLevel(Mustache_Logger::ERROR); + $this->assertEquals(Mustache_Logger::ERROR, $logger->getLevel()); + + $logger->log(Mustache_Logger::WARNING, 'ignore this'); + + $logger->setLevel(Mustache_Logger::INFO); + $this->assertEquals(Mustache_Logger::INFO, $logger->getLevel()); + + $logger->log(Mustache_Logger::WARNING, 'log this'); + + $logger->setLevel(Mustache_Logger::CRITICAL); + $this->assertEquals(Mustache_Logger::CRITICAL, $logger->getLevel()); + + $logger->log(Mustache_Logger::ERROR, 'ignore this'); + + rewind($stream); + $result = fread($stream, 1024); + + $this->assertEquals("WARNING: log this\n", $result); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsInvalidArgumentExceptionWhenSettingUnknownLevels() + { + $logger = new Mustache_Logger_StreamLogger(tmpfile()); + $logger->setLevel('bacon'); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsInvalidArgumentExceptionWhenLoggingUnknownLevels() + { + $logger = new Mustache_Logger_StreamLogger(tmpfile()); + $logger->log('bacon', 'CODE BACON ERROR!'); + } +} From d252fddf4ab731ade8aca80214dd5c27407b4ac3 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 11 Jan 2013 22:53:27 -0800 Subject: [PATCH 30/31] Add plug for PSR-3 --- src/Mustache/Engine.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 16e2e86..5eafe81 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -83,7 +83,9 @@ class Mustache_Engine * // 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. + * // 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'), * ); * From 638ed9abc1b0ac2a498751e38f3cd62e41f72344 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 11 Jan 2013 23:17:53 -0800 Subject: [PATCH 31/31] Bump version to 2.1.0 --- src/Mustache/Engine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 5eafe81..ddfffca 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -23,7 +23,7 @@ */ class Mustache_Engine { - const VERSION = '2.0.2'; + const VERSION = '2.1.0'; const SPEC_VERSION = '1.1.2'; const PRAGMA_FILTERS = 'FILTERS';