From 6f899c8d5a31854f4b41efe73e3e06305f2f2f5f Mon Sep 17 00:00:00 2001 From: Evan Coury Date: Wed, 14 Dec 2011 13:27:22 -0700 Subject: [PATCH 01/69] Add unit tests for FormCsrf view helper --- src/Helper/FormCsrf.php | 2 +- test/Helper/FormCsrfTest.php | 103 +++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/Helper/FormCsrfTest.php diff --git a/src/Helper/FormCsrf.php b/src/Helper/FormCsrf.php index 3c5e9a4d..ec5584f3 100644 --- a/src/Helper/FormCsrf.php +++ b/src/Helper/FormCsrf.php @@ -51,6 +51,6 @@ public function __invoke($name = 'csrf') $this->hashElements[$name] = new HashElement($name); $this->hashElements[$name]->setDecorators(array('ViewHelper')); } - return $this->hashElements[$name]->render($this->getView()); + return trim($this->hashElements[$name]->render($this->getView())); } } diff --git a/test/Helper/FormCsrfTest.php b/test/Helper/FormCsrfTest.php new file mode 100644 index 00000000..9c87b763 --- /dev/null +++ b/test/Helper/FormCsrfTest.php @@ -0,0 +1,103 @@ +helper = new FormCsrf(); + $this->view = new View(); + $this->view->doctype()->setDoctype(strtoupper("XHTML1_STRICT")); + $this->helper->setView($this->view); + + if (isset($_SERVER['HTTPS'])) { + unset ($_SERVER['HTTPS']); + } + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + unset($this->helper, $this->view); + } + + public function testCsrfXhtmlDoctype() + { + $this->assertRegExp( + '/\/>$/', + $this->helper->__invoke() + ); + } + + public function testCsrfHtmlDoctype() + { + $object = new FormCsrf(); + $view = new View(); + $view->doctype()->setDoctype(strtoupper("HTML5")); + $object->setView($view); + + $this->assertRegExp( + '/[^\/]>$/', + $object->__invoke() + ); + } + + public function testReturnInputTag() + { + $this->assertRegExp( + "/^helper->__invoke() + ); + } +} From b6f73ed7ed3152e69aa8f8c1d8db8f0ed536789a Mon Sep 17 00:00:00 2001 From: Rob Allen Date: Mon, 2 Jan 2012 20:29:11 +0000 Subject: [PATCH 02/69] revert PR689 as it breaks the unit tests --- src/Helper/BasePath.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Helper/BasePath.php b/src/Helper/BasePath.php index f09ac75f..3e788575 100644 --- a/src/Helper/BasePath.php +++ b/src/Helper/BasePath.php @@ -58,7 +58,7 @@ public function __invoke($file = null) } if (null !== $file) { - $file = ltrim($file, '/'); + $file = '/' . ltrim($file, '/'); } return $this->basePath . $file; @@ -72,7 +72,7 @@ public function __invoke($file = null) */ public function setBasePath($basePath) { - $this->basePath = rtrim($basePath, '/') . '/'; + $this->basePath = $basePath; return $this; } } From 1ac3bcb2d8b079c38505fe3ec27b87d432bf4eb1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 16 Jan 2012 14:30:33 -0600 Subject: [PATCH 03/69] Created concrete view model objects - ViewModel -- base view model; contains accessors for options and variables manipulation. - JsonModel -- extends ViewModel, offers a serialize() method for getting JSON serialization of variables. - FeedModel -- marker class for ViewModels intended for generating feeds. Main features are unimplemented for now. --- src/Model/FeedModel.php | 39 ++++++++++ src/Model/JsonModel.php | 49 ++++++++++++ src/Model/ViewModel.php | 164 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 src/Model/FeedModel.php create mode 100644 src/Model/JsonModel.php create mode 100644 src/Model/ViewModel.php diff --git a/src/Model/FeedModel.php b/src/Model/FeedModel.php new file mode 100644 index 00000000..8c36f0e2 --- /dev/null +++ b/src/Model/FeedModel.php @@ -0,0 +1,39 @@ +getVariables(); + if ($variables instanceof Traversable) { + $variables = IteratorToArray::convert($variables); + } + return json_encode($variables); + } +} diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php new file mode 100644 index 00000000..108447e0 --- /dev/null +++ b/src/Model/ViewModel.php @@ -0,0 +1,164 @@ +setVariables($variables); + $this->setOptions($options); + } + + /** + * Set renderer option/hint + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setOption($name, $value) + { + $this->options[(string) $name] = $value; + return $this; + } + + /** + * Set renderer options/hints en masse + * + * @param array|Traversable $name + * @return ViewModel + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = IteratorToArray::convert($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable argument; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + $this->options = $options; + return $this; + } + + /** + * Get renderer options/hints + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Set view variable + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setVariable($name, $value) + { + $this->variables[(string) $name] = $value; + return $this; + } + + /** + * Set view variables en masse + * + * Can be an array or a Traversable + ArrayAccess object. + * + * @param array|ArrayAccess&Traversable $variables + * @return ViewModel + */ + public function setVariables($variables) + { + if ($variables instanceof ArrayAccess && $variables instanceof Traversable) { + $this->variables = $variables; + return $this; + } + + if ($variables instanceof Traversable) { + $variables = IteratorToArray::convert($variables); + } + + if (!is_array($variables)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable ArrayAccess argument; received "%s"', + __METHOD__, + (is_object($variables) ? get_class($variables) : gettype($variables)) + )); + } + + $this->variables = $variables; + return $this; + } + + /** + * Get view variables + * + * @return array|ArrayAccess|Traversable + */ + public function getVariables() + { + return $this->variables; + } +} From b7400964392cd8060b818829763250dcd9fb72ec Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 16 Jan 2012 16:11:35 -0600 Subject: [PATCH 04/69] Resolver implementations - Renamed TemplatePathStack to Resolver\TemplatePathStack - Added Resolver\TemplateMapResolver - Removed TemplateResolver interface (now simply Resolver) - Updated PhpRenderer to use new Resolver API --- src/PhpRenderer.php | 27 +--- src/Resolver.php | 3 +- src/Resolver/TemplateMapResolver.php | 191 +++++++++++++++++++++++ src/{ => Resolver}/TemplatePathStack.php | 16 +- src/TemplateResolver.php | 43 ----- 5 files changed, 209 insertions(+), 71 deletions(-) create mode 100644 src/Resolver/TemplateMapResolver.php rename src/{ => Resolver}/TemplatePathStack.php (96%) delete mode 100644 src/TemplateResolver.php diff --git a/src/PhpRenderer.php b/src/PhpRenderer.php index 368ce421..e9945473 100644 --- a/src/PhpRenderer.php +++ b/src/PhpRenderer.php @@ -41,7 +41,7 @@ class PhpRenderer implements Renderer, Pluggable /** * Template resolver * - * @var TemplateResolver + * @var Resolver */ private $templateResolver; @@ -116,25 +116,12 @@ public function init() /** * Set script resolver * - * @param string|TemplateResolver $resolver - * @param mixed $options + * @param Resolver $resolver * @return PhpRenderer * @throws Exception\InvalidArgumentException */ - public function setResolver($resolver, $options = null) + public function setResolver(Resolver $resolver) { - if (is_string($resolver)) { - if (!class_exists($resolver)) { - throw new Exception\InvalidArgumentException('Class passed as resolver could not be found'); - } - $resolver = new $resolver($options); - } - if (!$resolver instanceof TemplateResolver) { - throw new Exception\InvalidArgumentException(sprintf( - 'Expected resolver to implement TemplateResolver; received "%s"', - (is_object($resolver) ? get_class($resolver) : gettype($resolver)) - )); - } $this->templateResolver = $resolver; return $this; } @@ -143,16 +130,16 @@ public function setResolver($resolver, $options = null) * Retrieve template name or template resolver * * @param null|string $name - * @return string|TemplateResolver + * @return string|Resolver */ public function resolver($name = null) { if (null === $this->templateResolver) { - $this->setResolver(new TemplatePathStack()); + $this->setResolver(new Resolver\TemplatePathStack()); } if (null !== $name) { - return $this->templateResolver->getScriptPath($name); + return $this->templateResolver->resolve($name, $this); } return $this->templateResolver; @@ -408,7 +395,7 @@ public function getFilterChain() /** * Processes a view script and returns the output. * - * @param string $name The script name to process. + * @param string $name The script name to process. * @return string The script output. */ public function render($name, $vars = null) diff --git a/src/Resolver.php b/src/Resolver.php index 704a66ec..f3f9bb69 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -34,7 +34,8 @@ interface Resolver * Resolve a template/pattern name to a resource the renderer can consume * * @param string $name + * @param null|Renderer $renderer * @return mixed */ - public function resolve($name); + public function resolve($name, Renderer $renderer = null); } diff --git a/src/Resolver/TemplateMapResolver.php b/src/Resolver/TemplateMapResolver.php new file mode 100644 index 00000000..f2605da5 --- /dev/null +++ b/src/Resolver/TemplateMapResolver.php @@ -0,0 +1,191 @@ +setMap($map); + } + + /** + * Set (overwrite) template map + * + * Maps should be arrays or Traversable objects with name => path pairs + * + * @param array|Traversable $map + * @return TemplateMapResolver + */ + public function setMap($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = IteratorToArray::convert($map); + } + + $this->map = $map; + return $this; + } + + /** + * Add an entry to the map + * + * @param string|array|Traversable $nameOrMap + * @param null|string $path + * @return TemplateResolver + */ + public function add($nameOrMap, $path = null) + { + if (is_array($nameOrMap) || $nameOrMap instanceof Traversable) { + $this->merge($nameOrMap); + return $this; + } + + if (!is_string($nameOrMap)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects a string, array, or Traversable for the first argument; received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if (empty($path)) { + if (isset($this->map[$nameOrMap])) { + unset($this->map[$nameOrMap]); + } + return $this; + } + + $this->map[$nameOrMap] = $path; + return $this; + } + + /** + * Merge internal map with provided map + * + * @param array|Traversable $map + * @return TemplateMapResolver + */ + public function merge($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = IteratorToArray::convert($map); + } + + $this->map = array_replace_recursive($this->map, $map); + return $this; + } + + /** + * Does the resolver contain an entry for the given name? + * + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->map); + } + + /** + * Retrieve a template path by name + * + * @param string $name + * @return string + * @throws Exception\DomainException if no entry exists + */ + public function get($name) + { + if (!$this->has($name)) { + throw new Exception\DomainException(sprintf( + '%s: no template by name of "%s" found', + __METHOD__, + (string) $name + )); + } + return $this->map[$name]; + } + + /** + * Retrieve the template map + * + * @return array + */ + public function getMap() + { + return $this->map; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return string + */ + public function resolve($name, Renderer $renderer = null) + { + return $this->get($name); + } +} diff --git a/src/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php similarity index 96% rename from src/TemplatePathStack.php rename to src/Resolver/TemplatePathStack.php index 75ae58cf..678b4663 100644 --- a/src/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -14,27 +14,28 @@ * * @category Zend * @package Zend_View + * @subpackage Resolver * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -/** - * @namespace - */ -namespace Zend\View; +namespace Zend\View\Resolver; use SplFileInfo, - Zend\Stdlib\SplStack; + Zend\Stdlib\SplStack, + Zend\View\Renderer, + Zend\View\Resolver; /** * Resolves view scripts based on a stack of paths * * @category Zend * @package Zend_View + * @subpackage Resolver * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class TemplatePathStack implements TemplateResolver +class TemplatePathStack implements Resolver { /** * @var SplStack @@ -250,10 +251,11 @@ public function useStreamWrapper() * Retrieve the filesystem path to a view script * * @param string $name + * @param null|Renderer $renderer * @return string * @throws Exception\RuntimeException */ - public function getScriptPath($name) + public function resolve($name, Renderer $renderer = null) { if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { throw new Exception\DomainException( diff --git a/src/TemplateResolver.php b/src/TemplateResolver.php deleted file mode 100644 index 69c439ec..00000000 --- a/src/TemplateResolver.php +++ /dev/null @@ -1,43 +0,0 @@ - Date: Mon, 16 Jan 2012 16:48:59 -0600 Subject: [PATCH 05/69] Added stack capabilities - new method, enqueue($template) - render() now uses a while loop to render content --- src/PhpRenderer.php | 36 +++++++++++++++++++++++++++++++----- test/PhpRendererTest.php | 17 +++++++++++++++-- test/_templates/block.phtml | 6 ++++++ test/_templates/layout.phtml | 5 +++++ 4 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 test/_templates/block.phtml create mode 100644 test/_templates/layout.phtml diff --git a/src/PhpRenderer.php b/src/PhpRenderer.php index e9945473..6a4dbffb 100644 --- a/src/PhpRenderer.php +++ b/src/PhpRenderer.php @@ -38,6 +38,17 @@ */ class PhpRenderer implements Renderer, Pluggable { + /** + * @var string Rendered content + */ + private $content = ''; + + /** + * Queue of templates to render + * @var array + */ + private $extensions = array(); + /** * Template resolver * @@ -401,7 +412,7 @@ public function getFilterChain() public function render($name, $vars = null) { // find the script file name using the parent private method - $this->file = $this->resolver($name); + $this->enqueue($name); unset($name); // remove $name from local scope $this->varsCache[] = $this->vars(); @@ -420,13 +431,28 @@ public function render($name, $vars = null) extract($__vars); unset($__vars); // remove $__vars from local scope - ob_start(); - include $this->file; - $content = ob_get_clean(); + while ($this->file = array_shift($this->extensions)) { + $this->file = $this->resolver($this->file); + ob_start(); + include $this->file; + $this->content = ob_get_clean(); + } $this->setVars(array_pop($this->varsCache)); - return $this->getFilterChain()->filter($content); // filter output + return $this->getFilterChain()->filter($this->content); // filter output + } + + /** + * Add a template to the stack + * + * @param string $template + * @return PhpRenderer + */ + public function enqueue($template) + { + $this->extensions[] = $template; + return $this; } /** diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index b16fe1f0..d098def7 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -22,7 +22,8 @@ namespace ZendTest\View; use Zend\View\PhpRenderer, - Zend\View\TemplatePathStack, + Zend\View\Resolver\TemplateMapResolver, + Zend\View\Resolver\TemplatePathStack, Zend\View\Variables, Zend\Filter\FilterChain; @@ -48,7 +49,7 @@ public function testEngineIsIdenticalToRenderer() public function testUsesTemplatePathStackAsDefaultResolver() { - $this->assertInstanceOf('Zend\View\TemplatePathStack', $this->renderer->resolver()); + $this->assertInstanceOf('Zend\View\Resolver\TemplatePathStack', $this->renderer->resolver()); } public function testCanSetResolverInstance() @@ -284,4 +285,16 @@ public function testInjectsVariablesContainerWithEscapeHelperAsEscapeCallbackWhe $escapeHelper = $this->renderer->plugin('escape'); $this->assertSame($escapeHelper, $this->renderer->vars()->getEscapeCallback()); } + + public function testRendersTemplatesInAStack() + { + $resolver = new TemplateMapResolver(array( + 'layout' => __DIR__ . '/_templates/layout.phtml', + 'block' => __DIR__ . '/_templates/block.phtml', + )); + $this->renderer->setResolver($resolver); + + $content = $this->renderer->render('block'); + $this->assertRegexp('#\s*Block content\s*#', $content); + } } diff --git a/test/_templates/block.phtml b/test/_templates/block.phtml new file mode 100644 index 00000000..983af791 --- /dev/null +++ b/test/_templates/block.phtml @@ -0,0 +1,6 @@ +enqueue('layout'); +$this->placeholder('block')->captureStart(); +?> +Block content +placeholder('block')->captureEnd(); ?> diff --git a/test/_templates/layout.phtml b/test/_templates/layout.phtml new file mode 100644 index 00000000..3a137190 --- /dev/null +++ b/test/_templates/layout.phtml @@ -0,0 +1,5 @@ + + +placeholder('block') ?> + + From 9d643f885901603144599b34c747845be1e35b0b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 16 Jan 2012 17:07:03 -0600 Subject: [PATCH 06/69] Integrated ViewModel into PhpRenderer::render() - Allows rendering a ViewModel now --- src/PhpRenderer.php | 48 +++++++++++++++++++++++++++++++------ test/PhpRendererTest.php | 46 +++++++++++++++++++++++++++++++++++ test/_templates/empty.phtml | 1 + 3 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 test/_templates/empty.phtml diff --git a/src/PhpRenderer.php b/src/PhpRenderer.php index 6a4dbffb..fe186b27 100644 --- a/src/PhpRenderer.php +++ b/src/PhpRenderer.php @@ -406,21 +406,55 @@ public function getFilterChain() /** * Processes a view script and returns the output. * - * @param string $name The script name to process. + * @param string|Model $nameOrModel Either the template to use, or a + * ViewModel. The ViewModel must have the + * template as an option in order to be + * valid. + * @param null|array|Traversable Values to use when rendering. If none + * provided, uses those in the composed + * variables container. * @return string The script output. + * @throws Exception\DomainException if a ViewModel is passed, but does not + * contain a template option. + * @throws Exception\InvalidArgumentException if the values passed are not + * an array or ArrayAccess object */ - public function render($name, $vars = null) + public function render($nameOrModel, $values = null) { + if ($nameOrModel instanceof Model) { + $model = $nameOrModel; + $options = $model->getOptions(); + if (!isset($options['template']) || empty($options['template'])) { + throw new Exception\DomainException(sprintf( + '%s: received View Model argument, but missing "template" option', + __METHOD__ + )); + } + $nameOrModel = $options['template']; + unset($options['template']); + foreach ($options as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + unset($method, $setting, $value); + } + unset($options); + + $values = $model->getVariables(); + unset($model); + } + // find the script file name using the parent private method - $this->enqueue($name); - unset($name); // remove $name from local scope + $this->enqueue($nameOrModel); + unset($nameOrModel); // remove $name from local scope $this->varsCache[] = $this->vars(); - if (null !== $vars) { - $this->setVars($vars); + if (null !== $values) { + $this->setVars($values); } - unset($vars); + unset($values); // extract all assigned vars (pre-escaped), but not 'this'. // assigns to a double-underscored variable, to prevent naming collisions diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index d098def7..f665a081 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -22,6 +22,7 @@ namespace ZendTest\View; use Zend\View\PhpRenderer, + Zend\View\Model\ViewModel, Zend\View\Resolver\TemplateMapResolver, Zend\View\Resolver\TemplatePathStack, Zend\View\Variables, @@ -297,4 +298,49 @@ public function testRendersTemplatesInAStack() $content = $this->renderer->render('block'); $this->assertRegexp('#\s*Block content\s*#', $content); } + + /** + * @group view-model + */ + public function testCanRenderViewModel() + { + $resolver = new TemplateMapResolver(array( + 'empty' => __DIR__ . '/_templates/empty.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setOption('template', 'empty'); + + $content = $this->renderer->render($model); + $this->assertRegexp('/\s*Empty view\s*/s', $content); + } + + /** + * @group view-model + */ + public function testViewModelWithoutTemplateRaisesException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\DomainException'); + $content = $this->renderer->render($model); + } + + /** + * @group view-model + */ + public function testRendersViewModelWithVariablesSpecified() + { + $resolver = new TemplateMapResolver(array( + 'test' => __DIR__ . '/_templates/test.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setOption('template', 'test'); + $model->setVariable('bar', 'bar'); + + $content = $this->renderer->render($model); + $this->assertRegexp('/\s*foo bar baz\s*/s', $content); + } } diff --git a/test/_templates/empty.phtml b/test/_templates/empty.phtml new file mode 100644 index 00000000..6e1b56a5 --- /dev/null +++ b/test/_templates/empty.phtml @@ -0,0 +1 @@ +Empty view From c91c87b919d394f5b45cf270c3f7e7520c499df1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 17 Jan 2012 11:02:14 -0600 Subject: [PATCH 07/69] Added JsonRenderer - Simple, but incomplete, JSON renderer --- src/Renderer/JsonRenderer.php | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/Renderer/JsonRenderer.php diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php new file mode 100644 index 00000000..d745b99b --- /dev/null +++ b/src/Renderer/JsonRenderer.php @@ -0,0 +1,119 @@ +resolver = $resolver; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting both $nameOrModel and $values + * @param string|Model $name The script/resource process, or a view model + * @param null|array|\ArrayAccess Values to use during rendering + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + // use case 1: View Models + // Serialize variables in view model + if ($nameOrModel instanceof Model) { + if ($nameOrModel instanceof Model\JsonViewModel) { + $values = $nameOrModel->serialize(); + } else { + $values = $nameOrModel->getVariables(); + $values = json_encode($values); + } + + return $values; + } + + // use case 2: $nameOrModel is populated, $values is not + // Serialize $nameOrModel + if (null === $values) { + if (!is_object($nameOrModel)) { + return json_encode($nameOrModel); + } + + if ($nameOrModel instanceof JsonSerializable) { + return $nameOrModel->jsonSerialize(); + } + + if ($nameOrModel instanceof Traversable) { + $nameOrModel = IteratorToArray::convert($nameOrModel); + return json_encode($nameOrModel); + } + + return json_encode(get_object_vars($nameOrModel)); + } + + // use case 3: Both $nameOrModel and $values are populated + throw new Exception\DomainException(sprintf( + '%s: Do not know how to handle operation when both $nameOrModel and $values are populated', + __METHOD__ + )); + } +} + From 78cafc764b74efcca109165182d93dd63a7145d5 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 17 Jan 2012 11:55:44 -0600 Subject: [PATCH 08/69] Added FeedRenderer - Created Zend\Feed\Writer\FeedFactory - Fleshed out FeedModel - Created FeedRenderer, which renders FeedModel objects --- src/Model/FeedModel.php | 66 ++++++++++++++- src/Renderer/FeedRender.php | 149 ++++++++++++++++++++++++++++++++++ src/Renderer/JsonRenderer.php | 2 +- 3 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 src/Renderer/FeedRender.php diff --git a/src/Model/FeedModel.php b/src/Model/FeedModel.php index 8c36f0e2..3fe5d8f4 100644 --- a/src/Model/FeedModel.php +++ b/src/Model/FeedModel.php @@ -21,13 +21,12 @@ namespace Zend\View\Model; +use Zend\Feed\Writer\Feed, + Zend\Feed\Writer\FeedFactory; + /** * Marker view model for indicating feed data. * - * @todo This should probably take the $variables passed as a data structure - * and create a Zend\Feed\Writer\Feed object from it. As such, we should - * likely also create some sort of Factory object for feeds that would - * take a structured assoc array and use it to populate a Feed object. * @category Zend * @package Zend_View * @subpackage Model @@ -36,4 +35,63 @@ */ class FeedModel extends ViewModel { + /** + * @var Feed + */ + protected $feed; + + /** + * @var false|string + */ + protected $type = false; + + public function getFeed() + { + if ($this->feed instanceof Feed) { + return $this->feed; + } + + if (!$this->type) { + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + } + + $variables = $this->getVariables(); + $feed = FeedFactory::factory($variables); + $this->setFeed($feed); + + return $this->feed; + } + + /** + * Set the feed object + * + * @param Feed $feed + * @return FeedModel + */ + public function setFeed(Feed $feed) + { + $this->feed = $feed; + return $this; + } + + /** + * Get the feed type + * + * @return false|string + */ + public function getFeedType() + { + if ($this->type) { + return $this->type; + } + + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + return $this->type; + } } diff --git a/src/Renderer/FeedRender.php b/src/Renderer/FeedRender.php new file mode 100644 index 00000000..14f78678 --- /dev/null +++ b/src/Renderer/FeedRender.php @@ -0,0 +1,149 @@ +resolver = $resolver; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting only $nameOrModel + * @param string|Model $name The script/resource process, or a view model + * @param null|array|\ArrayAccess Values to use during rendering + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + // Use case 1: View Model provided + // Non-FeedModel: cast to FeedModel + if (!$nameOrModel instanceof Model\FeedModel) { + $vars = $nameOrModel->getVariables(); + $options = $nameOrModel->getOptions(); + $type = $this->getFeedType(); + if (isset($options['feed_type'])) { + $type = $options['feed_type']; + } + $nameOrModel = new Model\FeedModel($vars, $type); + } + } elseif (is_string($nameOrModel)) { + // Use case 2: string $nameOrModel + array|Traversable|Feed $values + $nameOrModel = new Model\FeedModel($values, $nameOrModel); + } else { + // Use case 3: failure + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a ViewModel or a string feed type as the first argument; received "%s"', + (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) + )); + } + + // Get feed and type + $feed = $nameOrModel->getFeed(); + $type = $nameOrModel->getFeedType(); + if (!$type) { + $type = $this->getFeedType(); + } + + // Render feed + return $feed->export($type); + } + + /** + * Set feed type ('rss' or 'atom') + * + * @param string $feedType + * @return FeedRenderer + */ + public function setFeedType($feedType) + { + $feedType = strtolower($feedType); + if (!in_array($feedType, array('rss', 'atom'))) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string of either "rss" or "atom"', + __METHOD__ + )); + } + + $this->feedType = $feedType; + return $this; + } + + /** + * Get feed type + * + * @return string + */ + public function getFeedType() + { + return $this->feedType; + } +} diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index d745b99b..06a915bd 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -80,7 +80,7 @@ public function render($nameOrModel, $values = null) // use case 1: View Models // Serialize variables in view model if ($nameOrModel instanceof Model) { - if ($nameOrModel instanceof Model\JsonViewModel) { + if ($nameOrModel instanceof Model\JsonModel) { $values = $nameOrModel->serialize(); } else { $values = $nameOrModel->getVariables(); From 2d4954fbec6fb78a87d88c8704fe7b9bcd7bc517 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 17 Jan 2012 17:14:16 -0600 Subject: [PATCH 09/69] Created default rendering strategy - Renders at priority -10000 on "dispatch" event - Populates View with appropriate renderers if not already present - Uses either ViewModel type or Accept header to determine renderer - Added q-priority parsing/handling to Accept header implementation - Renders JSON, RSS and Atom Feeds, and defaults to PhpRenderer - TODO: auto-determination of view script name if not present --- src/Renderer/FeedRender.php | 4 ++++ src/View.php | 1 + src/ViewEvent.php | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Renderer/FeedRender.php b/src/Renderer/FeedRender.php index 14f78678..e5c9d5d8 100644 --- a/src/Renderer/FeedRender.php +++ b/src/Renderer/FeedRender.php @@ -92,6 +92,8 @@ public function render($nameOrModel, $values = null) $type = $this->getFeedType(); if (isset($options['feed_type'])) { $type = $options['feed_type']; + } else { + $this->setFeedType($type); } $nameOrModel = new Model\FeedModel($vars, $type); } @@ -111,6 +113,8 @@ public function render($nameOrModel, $values = null) $type = $nameOrModel->getFeedType(); if (!$type) { $type = $this->getFeedType(); + } else { + $this->setFeedType($type); } // Render feed diff --git a/src/View.php b/src/View.php index 23ab8ce2..ec5a4949 100644 --- a/src/View.php +++ b/src/View.php @@ -239,6 +239,7 @@ public function render(Model $model) __METHOD__ )); } + $event->setRenderer($renderer); $rendered = $renderer->render($model); diff --git a/src/ViewEvent.php b/src/ViewEvent.php index c5f3b348..b6714a88 100644 --- a/src/ViewEvent.php +++ b/src/ViewEvent.php @@ -37,6 +37,11 @@ class ViewEvent extends Event */ protected $model; + /** + * @var Renderer + */ + protected $renderer; + /** * @var null|Request */ @@ -108,6 +113,28 @@ public function getModel() return $this->model; } + /** + * Set value for renderer + * + * @param Renderer $renderer + * @return ViewEvent + */ + public function setRenderer(Renderer $renderer) + { + $this->renderer = $renderer; + return $this; + } + + /** + * Get value for renderer + * + * @return null|Renderer + */ + public function getRenderer() + { + return $this->renderer; + } + /** * Retrieve the MVC request object * @@ -150,6 +177,8 @@ public function getParam($name, $default = null) switch($name) { case 'model': return $this->getModel(); + case 'renderer': + return $this->getRenderer(); case 'request': return $this->getRequest(); case 'response': @@ -170,6 +199,7 @@ public function getParams() { $params = parent::getParams(); $params['model'] = $this->getModel(); + $params['renderer'] = $this->getRenderer(); $params['request'] = $this->getRequest(); $params['response'] = $this->getResponse(); $params['result'] = $this->getResult(); @@ -189,7 +219,7 @@ public function setParams($params) return $this; } - foreach (array('model', 'request', 'response', 'result') as $param) { + foreach (array('model', 'renderer', 'request', 'response', 'result') as $param) { if (isset($params[$param])) { $method = 'set' . $param; $this->$method($params[$param]); @@ -211,6 +241,9 @@ public function setParam($name, $value) case 'model': $this->setModel($value); break; + case 'renderer': + $this->setRenderer($value); + break; case 'request': $this->setRequest($value); break; From 84cac2ef15e60c618fcba7e050580c1d89b15e90 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 24 Jan 2012 14:18:27 -0600 Subject: [PATCH 10/69] Prototype for default rendering strategy - Performs layouts - Performs 404s/error handling - Allows partial HTML responses - Working feed renderer --- src/Renderer/FeedRenderer.php | 153 +++++++++++++++++++++++++++++ src/Resolver/TemplatePathStack.php | 1 + src/View.php | 9 +- src/ViewEvent.php | 2 + 4 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/Renderer/FeedRenderer.php diff --git a/src/Renderer/FeedRenderer.php b/src/Renderer/FeedRenderer.php new file mode 100644 index 00000000..e5c9d5d8 --- /dev/null +++ b/src/Renderer/FeedRenderer.php @@ -0,0 +1,153 @@ +resolver = $resolver; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting only $nameOrModel + * @param string|Model $name The script/resource process, or a view model + * @param null|array|\ArrayAccess Values to use during rendering + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + // Use case 1: View Model provided + // Non-FeedModel: cast to FeedModel + if (!$nameOrModel instanceof Model\FeedModel) { + $vars = $nameOrModel->getVariables(); + $options = $nameOrModel->getOptions(); + $type = $this->getFeedType(); + if (isset($options['feed_type'])) { + $type = $options['feed_type']; + } else { + $this->setFeedType($type); + } + $nameOrModel = new Model\FeedModel($vars, $type); + } + } elseif (is_string($nameOrModel)) { + // Use case 2: string $nameOrModel + array|Traversable|Feed $values + $nameOrModel = new Model\FeedModel($values, $nameOrModel); + } else { + // Use case 3: failure + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a ViewModel or a string feed type as the first argument; received "%s"', + (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) + )); + } + + // Get feed and type + $feed = $nameOrModel->getFeed(); + $type = $nameOrModel->getFeedType(); + if (!$type) { + $type = $this->getFeedType(); + } else { + $this->setFeedType($type); + } + + // Render feed + return $feed->export($type); + } + + /** + * Set feed type ('rss' or 'atom') + * + * @param string $feedType + * @return FeedRenderer + */ + public function setFeedType($feedType) + { + $feedType = strtolower($feedType); + if (!in_array($feedType, array('rss', 'atom'))) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string of either "rss" or "atom"', + __METHOD__ + )); + } + + $this->feedType = $feedType; + return $this; + } + + /** + * Get feed type + * + * @return string + */ + public function getFeedType() + { + return $this->feedType; + } +} diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php index 678b4663..61bc18bd 100644 --- a/src/Resolver/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -23,6 +23,7 @@ use SplFileInfo, Zend\Stdlib\SplStack, + Zend\View\Exception, Zend\View\Renderer, Zend\View\Resolver; diff --git a/src/View.php b/src/View.php index ec5a4949..9439091d 100644 --- a/src/View.php +++ b/src/View.php @@ -21,6 +21,7 @@ namespace Zend\View; use Zend\EventManager\EventCollection, + Zend\EventManager\EventManager, Zend\Stdlib\RequestDescription as Request, Zend\Stdlib\ResponseDescription as Response; @@ -93,7 +94,7 @@ public function getRequest() */ public function getResponse() { - return $this->request; + return $this->response; } @@ -106,7 +107,7 @@ public function getResponse() public function addRenderer(Renderer $renderer) { $class = get_class($renderer); - if (array_key_exists($this->renderers[$class])) { + if (array_key_exists($class, $this->renderers)) { throw new Exception\RuntimeException(sprintf( 'Unable to add renderer of type "%s"; another renderer of that type is already attached', $class @@ -124,7 +125,7 @@ public function addRenderer(Renderer $renderer) */ public function hasRenderer($class) { - return array_key_exists($this->renderers[$class]); + return array_key_exists($class, $this->renderers); } /** @@ -228,6 +229,7 @@ public function addResponseStrategy($callable, $priority = 1) public function render(Model $model) { $event = $this->getEvent(); + $event->setModel($model); $events = $this->events(); $results = $events->trigger('renderer', $event, function($result) { return ($result instanceof Renderer); @@ -256,7 +258,6 @@ public function render(Model $model) protected function getEvent() { $event = new ViewEvent(); - $event->setModel($model); $event->setTarget($this); if (null !== ($request = $this->getRequest())) { $event->setRequest($request); diff --git a/src/ViewEvent.php b/src/ViewEvent.php index b6714a88..92e2e6bd 100644 --- a/src/ViewEvent.php +++ b/src/ViewEvent.php @@ -65,6 +65,8 @@ class ViewEvent extends Event */ public function setModel(Model $model) { + $this->model = $model; + return $this; } /** From 299266c6bfab1b6d50a631468a50b7d007a050ea Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 2 Feb 2012 16:35:55 -0600 Subject: [PATCH 11/69] Added tests for ViewModel --- src/Model/ViewModel.php | 3 + test/Model/ViewModelTest.php | 131 +++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 test/Model/ViewModelTest.php diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index 108447e0..c9ebb52c 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -82,6 +82,8 @@ public function setOption($name, $value) */ public function setOptions($options) { + // Assumption is that lowest common denominator for renderer configuration + // is an array if ($options instanceof Traversable) { $options = IteratorToArray::convert($options); } @@ -131,6 +133,7 @@ public function setVariable($name, $value) */ public function setVariables($variables) { + // Assumption is that renderers can handle arrays or ArrayAccess objects if ($variables instanceof ArrayAccess && $variables instanceof Traversable) { $this->variables = $variables; return $this; diff --git a/test/Model/ViewModelTest.php b/test/Model/ViewModelTest.php new file mode 100644 index 00000000..193d0c2c --- /dev/null +++ b/test/Model/ViewModelTest.php @@ -0,0 +1,131 @@ +assertEquals(array(), $model->getVariables()); + $this->assertEquals(array(), $model->getOptions()); + } + + public function testAllowsEmptyOptionsArgumentToConstructor() + { + $model = new ViewModel(array('foo' => 'bar')); + $this->assertEquals(array('foo' => 'bar'), $model->getVariables()); + $this->assertEquals(array(), $model->getOptions()); + } + + public function testAllowsPassingBothVariablesAndOptionsArgumentsToConstructor() + { + $model = new ViewModel(array('foo' => 'bar'), array('template' => 'foo/bar')); + $this->assertEquals(array('foo' => 'bar'), $model->getVariables()); + $this->assertEquals(array('template' => 'foo/bar'), $model->getOptions()); + } + + public function testAllowsPassingTraversableArgumentsToVariablesAndOptionsInConstructor() + { + $vars = new ArrayObject; + $options = new ArrayObject; + $model = new ViewModel($vars, $options); + $this->assertSame($vars, $model->getVariables()); + $this->assertSame(iterator_to_array($options), $model->getOptions()); + } + + public function testCanSetVariablesSingly() + { + $model = new ViewModel(array('foo' => 'bar')); + $model->setVariable('bar', 'baz'); + $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $model->getVariables()); + } + + public function testCanOverwriteVariablesSingly() + { + $model = new ViewModel(array('foo' => 'bar')); + $model->setVariable('foo', 'baz'); + $this->assertEquals(array('foo' => 'baz'), $model->getVariables()); + } + + public function testSetVariablesOverwritesAllPreviouslyStored() + { + $model = new ViewModel(array('foo' => 'bar', 'bar' => 'baz')); + $model->setVariables(array('bar' => 'BAZBAT')); + $this->assertEquals(array('bar' => 'BAZBAT'), $model->getVariables()); + } + + public function testCanSetOptionsSingly() + { + $model = new ViewModel(array(), array('foo' => 'bar')); + $model->setOption('bar', 'baz'); + $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $model->getOptions()); + } + + public function testCanOverwriteOptionsSingly() + { + $model = new ViewModel(array(), array('foo' => 'bar')); + $model->setOption('foo', 'baz'); + $this->assertEquals(array('foo' => 'baz'), $model->getOptions()); + } + + public function testSetOptionsOverwritesAllPreviouslyStored() + { + $model = new ViewModel(array(), array('foo' => 'bar', 'bar' => 'baz')); + $model->setOptions(array('bar' => 'BAZBAT')); + $this->assertEquals(array('bar' => 'BAZBAT'), $model->getOptions()); + } + + public function testOptionsAreInternallyConvertedToAnArrayFromTraversables() + { + $options = new ArrayObject(array('foo' => 'bar')); + $model = new ViewModel(); + $model->setOptions($options); + $this->assertEquals($options->getArrayCopy(), $model->getOptions()); + } + + public function testPassingAnInvalidArgumentToSetVariablesRaisesAnException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects an array'); + $model->setVariables(new stdClass); + } + + public function testPassingAnInvalidArgumentToSetOptionsRaisesAnException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects an array'); + $model->setOptions(new stdClass); + } +} From 3806eabb9d75d584e0f4416952882aabb649b153 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 3 Feb 2012 11:42:46 -0600 Subject: [PATCH 12/69] Added tests for TemplateMapResolver - Also added new feature: implements IteratorAggregate, allowing merging of multiple TemplateMapResolver instances. --- src/Resolver/TemplateMapResolver.php | 16 +- test/Resolver/TemplateMapResolverTest.php | 218 ++++++++++++++++++++++ 2 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 test/Resolver/TemplateMapResolverTest.php diff --git a/src/Resolver/TemplateMapResolver.php b/src/Resolver/TemplateMapResolver.php index f2605da5..a80defcb 100644 --- a/src/Resolver/TemplateMapResolver.php +++ b/src/Resolver/TemplateMapResolver.php @@ -21,7 +21,9 @@ namespace Zend\View\Resolver; -use Traversable, +use ArrayIterator, + IteratorAggregate, + Traversable, Zend\Stdlib\IteratorToArray, Zend\View\Exception, Zend\View\Renderer, @@ -34,7 +36,7 @@ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class TemplateMapResolver implements Resolver +class TemplateMapResolver implements IteratorAggregate, Resolver { /** * @var array @@ -54,6 +56,16 @@ public function __construct($map = array()) $this->setMap($map); } + /** + * IteratorAggregate: return internal iterator + * + * @return Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->map); + } + /** * Set (overwrite) template map * diff --git a/test/Resolver/TemplateMapResolverTest.php b/test/Resolver/TemplateMapResolverTest.php new file mode 100644 index 00000000..1c72254e --- /dev/null +++ b/test/Resolver/TemplateMapResolverTest.php @@ -0,0 +1,218 @@ +assertEquals(array(), $resolver->getMap()); + } + + public function testCanSeedMapWithArrayViaConstructor() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map, $resolver->getMap()); + } + + public function testCanSeedMapWithTraversableViaConstructor() + { + $map = new ArrayObject(array('foo/bar' => __DIR__ . '/foo/bar.phtml')); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map->getArrayCopy(), $resolver->getMap()); + } + + public function testCanSeedMapWithArrayViaSetter() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver(); + $resolver->setMap($map); + $this->assertEquals($map, $resolver->getMap()); + } + + public function testCanSeedMapWithTraversableViaSetter() + { + $map = new ArrayObject(array('foo/bar' => __DIR__ . '/foo/bar.phtml')); + $resolver = new TemplateMapResolver(); + $resolver->setMap($map); + $this->assertEquals($map->getArrayCopy(), $resolver->getMap()); + } + + public function testCanAppendSingleEntriesViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $resolver->add('foo/baz', __DIR__ . '/../foo/baz.phtml'); + $expected = array_merge($map, array('foo/baz' => __DIR__ . '/../foo/baz.phtml')); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsArrayViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->add($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsTraversableViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new ArrayObject(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->add($more); + $expected = array_merge($map, $more->getArrayCopy()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsArrayViaMerge() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->merge($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsTraversableViaMerge() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new ArrayObject(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getArrayCopy()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanMergeTwoMaps() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new TemplateMapResolver(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getMap()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testAddOverwritesMatchingEntries() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/bar' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->merge($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + $this->assertEquals(__DIR__ . '/../foo/baz.phtml', $resolver->get('foo/bar')); + } + + public function testMergeOverwritesMatchingEntries() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new TemplateMapResolver(array( + 'foo/bar' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getMap()); + $this->assertEquals($expected, $resolver->getMap()); + $this->assertEquals(__DIR__ . '/../foo/baz.phtml', $resolver->get('foo/bar')); + } + + public function testHasReturnsTrueWhenMatchingNameFound() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertTrue($resolver->has('foo/bar')); + } + + public function testHasReturnsFalseWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertFalse($resolver->has('bar/baz')); + } + + public function testGetReturnsPathWhenNameHasMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map['foo/bar'], $resolver->get('foo/bar')); + } + + public function testGetRaisesExceptionWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->setExpectedException('Zend\View\Exception\DomainException', 'no template'); + $resolver->get('bar/baz'); + } + + public function testResolveReturnsPathWhenNameHasMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map['foo/bar'], $resolver->resolve('foo/bar')); + } + + public function testResolveRaisesExceptionWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->setExpectedException('Zend\View\Exception\DomainException', 'no template'); + $resolver->resolve('bar/baz'); + } +} From 1c7541e43dafb42f0a7bc04b38811e3f9486f047 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 3 Feb 2012 13:03:58 -0600 Subject: [PATCH 13/69] Created tests for ViewEvent --- test/ViewEventTest.php | 158 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/ViewEventTest.php diff --git a/test/ViewEventTest.php b/test/ViewEventTest.php new file mode 100644 index 00000000..e07f9531 --- /dev/null +++ b/test/ViewEventTest.php @@ -0,0 +1,158 @@ +event = new ViewEvent; + } + + public function testModelIsNullByDefault() + { + $this->assertNull($this->event->getModel()); + } + + public function testRendererIsNullByDefault() + { + $this->assertNull($this->event->getRenderer()); + } + + public function testRequestIsNullByDefault() + { + $this->assertNull($this->event->getRequest()); + } + + public function testResponseIsNullByDefault() + { + $this->assertNull($this->event->getResponse()); + } + + public function testResultIsNullByDefault() + { + $this->assertNull($this->event->getResult()); + } + + public function testModelIsMutable() + { + $model = new ViewModel(); + $this->event->setModel($model); + $this->assertSame($model, $this->event->getModel()); + } + + public function testRendererIsMutable() + { + $renderer = new PhpRenderer(); + $this->event->setRenderer($renderer); + $this->assertSame($renderer, $this->event->getRenderer()); + } + + public function testRequestIsMutable() + { + $request = new Request(); + $this->event->setRequest($request); + $this->assertSame($request, $this->event->getRequest()); + } + + public function testResponseIsMutable() + { + $response = new Response(); + $this->event->setResponse($response); + $this->assertSame($response, $this->event->getResponse()); + } + + public function testResultIsMutable() + { + $result = 'some result'; + $this->event->setResult($result); + $this->assertSame($result, $this->event->getResult()); + } + + public function testModelIsMutableViaSetParam() + { + $model = new ViewModel(); + $this->event->setParam('model', $model); + $this->assertSame($model, $this->event->getModel()); + $this->assertSame($model, $this->event->getParam('model')); + } + + public function testRendererIsMutableViaSetParam() + { + $renderer = new PhpRenderer(); + $this->event->setParam('renderer', $renderer); + $this->assertSame($renderer, $this->event->getRenderer()); + $this->assertSame($renderer, $this->event->getParam('renderer')); + } + + public function testRequestIsMutableViaSetParam() + { + $request = new Request(); + $this->event->setParam('request', $request); + $this->assertSame($request, $this->event->getRequest()); + $this->assertSame($request, $this->event->getParam('request')); + } + + public function testResponseIsMutableViaSetParam() + { + $response = new Response(); + $this->event->setParam('response', $response); + $this->assertSame($response, $this->event->getResponse()); + $this->assertSame($response, $this->event->getParam('response')); + } + + public function testResultIsMutableViaSetParam() + { + $result = 'some result'; + $this->event->setParam('result', $result); + $this->assertSame($result, $this->event->getResult()); + $this->assertSame($result, $this->event->getParam('result')); + } + + public function testSpecializedParametersMayBeSetViaSetParams() + { + $model = new ViewModel(); + $renderer = new PhpRenderer(); + $request = new Request(); + $response = new Response(); + $result = 'some result'; + + $params = array( + 'model' => $model, + 'renderer' => $renderer, + 'request' => $request, + 'response' => $response, + 'result' => $result, + 'otherkey' => 'other value', + ); + + $this->event->setParams($params); + $this->assertEquals($params, $this->event->getParams()); + + $this->assertSame($params['model'], $this->event->getModel()); + $this->assertSame($params['model'], $this->event->getParam('model')); + + $this->assertSame($params['renderer'], $this->event->getRenderer()); + $this->assertSame($params['renderer'], $this->event->getParam('renderer')); + + $this->assertSame($params['request'], $this->event->getRequest()); + $this->assertSame($params['request'], $this->event->getParam('request')); + + $this->assertSame($params['response'], $this->event->getResponse()); + $this->assertSame($params['response'], $this->event->getParam('response')); + + $this->assertSame($params['result'], $this->event->getResult()); + $this->assertSame($params['result'], $this->event->getParam('result')); + + $this->assertEquals($params['otherkey'], $this->event->getParam('otherkey')); + } +} From f7481c228b803c3a30393e7935dcbe0879896adb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 6 Feb 2012 11:27:52 -0600 Subject: [PATCH 14/69] Completed basic renderering tests - Added "DumbStrategy" for mocking non-default strategies - Treat extensions as a LIFO stack, not a queue (should rename enqueue to push) --- src/PhpRenderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpRenderer.php b/src/PhpRenderer.php index fe186b27..1e1c0d3c 100644 --- a/src/PhpRenderer.php +++ b/src/PhpRenderer.php @@ -465,7 +465,7 @@ public function render($nameOrModel, $values = null) extract($__vars); unset($__vars); // remove $__vars from local scope - while ($this->file = array_shift($this->extensions)) { + while ($this->file = array_pop($this->extensions)) { $this->file = $this->resolver($this->file); ob_start(); include $this->file; From 0d5bb0c3b69dfe037428592b1861b1fab3533243 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 9 Feb 2012 16:10:21 -0600 Subject: [PATCH 15/69] Updates to view model - Added explicit template option - Added "children" concept, including counting and iteration - Added "capture to" concept as explicit option - Added ability to mark a model as "terminal" -- typically to indicate that no layout (parent model) is needed --- src/Model.php | 81 ++++++++++++++++++- src/Model/ViewModel.php | 148 +++++++++++++++++++++++++++++++++++ test/Model/ViewModelTest.php | 86 ++++++++++++++++++++ 3 files changed, 314 insertions(+), 1 deletion(-) diff --git a/src/Model.php b/src/Model.php index 7d304e2b..9cc7a918 100644 --- a/src/Model.php +++ b/src/Model.php @@ -21,14 +21,24 @@ namespace Zend\View; +use Countable, + IteratorAggregate; + /** + * Interface describing a view model. + * + * Extends "Countable"; count() should return the number of children attached + * to the model. + * + * Extends "IteratorAggregate"; should allow iterating over children. + * * @category Zend * @package Zend_View * @subpackage Model * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -interface Model +interface Model extends Countable, IteratorAggregate { /** * Set renderer option/hint @@ -77,4 +87,73 @@ public function setVariables($variables); * @return array|\ArrayAccess */ public function getVariables(); + + /** + * Set the template to be used by this model + * + * @param string $template + * @return Model + */ + public function setTemplate($template); + + /** + * Get the template to be used by this model + * + * @return string + */ + public function getTemplate(); + + /** + * Add a child model + * + * @param Model $child + * @return Model + */ + public function addChild(Model $child); + + /** + * Return all children. + * + * Return specifies an array, but may be any iterable object. + * + * @return array + */ + public function getChildren(); + + /** + * Does the model have any children? + * + * @return bool + */ + public function hasChildren(); + + /** + * Set the name of the variable to capture this model to, if it is a child model + * + * @param string $capture + * @return Model + */ + public function setCaptureTo($capture); + + /** + * Get the name of the variable to which to capture this model + * + * @return string + */ + public function captureTo(); + + /** + * Set flag indicating whether or not this is considered a terminal or standalone model + * + * @param bool $terminate + * @return Model + */ + public function setTerminal($terminate); + + /** + * Is this considered a terminal or standalone model? + * + * @return bool + */ + public function terminate(); } diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index c9ebb52c..9400407a 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -22,6 +22,7 @@ namespace Zend\View\Model; use ArrayAccess, + ArrayIterator, Traversable, Zend\Stdlib\IteratorToArray, Zend\View\Exception, @@ -36,12 +37,39 @@ */ class ViewModel implements Model { + /** + * What variable a parent model should capture this model to + * + * @var string + */ + protected $captureTo = 'content'; + + /** + * Child models + * @var array + */ + protected $children = array(); + /** * Renderer options * @var array */ protected $options = array(); + /** + * Template to use when rendering this model + * + * @var string + */ + protected $template = ''; + + /** + * Is this a standalone, or terminal, model? + * + * @var bool + */ + protected $terminate = false; + /** * View variables * @var array|ArrayAccess&Traversable @@ -164,4 +192,124 @@ public function getVariables() { return $this->variables; } + + /** + * Set the template to be used by this model + * + * @param string $template + * @return ViewModel + */ + public function setTemplate($template) + { + $this->template = (string) $template; + return $this; + } + + /** + * Get the template to be used by this model + * + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Add a child model + * + * @param Model $child + * @return ViewModel + */ + public function addChild(Model $child) + { + $this->children[] = $child; + return $this; + } + + /** + * Return all children. + * + * Return specifies an array, but may be any iterable object. + * + * @return array + */ + public function getChildren() + { + return $this->children; + } + + /** + * Does the model have any children? + * + * @return bool + */ + public function hasChildren() + { + return (0 < count($this->children)); + } + + /** + * Set the name of the variable to capture this model to, if it is a child model + * + * @param string $capture + * @return ViewModel + */ + public function setCaptureTo($capture) + { + $this->captureTo = (string) $capture; + return $this; + } + + /** + * Get the name of the variable to which to capture this model + * + * @return string + */ + public function captureTo() + { + return $this->captureTo; + } + + /** + * Set flag indicating whether or not this is considered a terminal or standalone model + * + * @param bool $terminate + * @return ViewModel + */ + public function setTerminal($terminate) + { + $this->terminate = (bool) $terminate; + return $this; + } + + /** + * Is this considered a terminal or standalone model? + * + * @return bool + */ + public function terminate() + { + return $this->terminate; + } + + /** + * Return count of children + * + * @return int + */ + public function count() + { + return count($this->children); + } + + /** + * Get iterator of children + * + * @return Iterator + */ + public function getIterator() + { + return new ArrayIterator($this->children); + } } diff --git a/test/Model/ViewModelTest.php b/test/Model/ViewModelTest.php index 193d0c2c..772f2853 100644 --- a/test/Model/ViewModelTest.php +++ b/test/Model/ViewModelTest.php @@ -128,4 +128,90 @@ public function testPassingAnInvalidArgumentToSetOptionsRaisesAnException() $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects an array'); $model->setOptions(new stdClass); } + + public function testCaptureToDefaultsToContent() + { + $model = new ViewModel(); + $this->assertEquals('content', $model->captureTo()); + } + + public function testCaptureToValueIsMutable() + { + $model = new ViewModel(); + $model->setCaptureTo('foo'); + $this->assertEquals('foo', $model->captureTo()); + } + + public function testHasNoChildrenByDefault() + { + $model = new ViewModel(); + $this->assertFalse($model->hasChildren()); + } + + public function testWhenNoChildrenCountIsZero() + { + $model = new ViewModel(); + $this->assertEquals(0, count($model)); + } + + public function testCanAddChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $this->assertTrue($model->hasChildren()); + } + + public function testCanCountChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $this->assertEquals(1, count($model)); + $model->addChild($child); + $this->assertEquals(2, count($model)); + } + + public function testCanIterateChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $model->addChild($child); + $model->addChild($child); + + $count = 0; + foreach ($model as $childModel) { + $this->assertSame($child, $childModel); + $count++; + } + $this->assertEquals(3, $count); + } + + public function testTemplateIsEmptyByDefault() + { + $model = new ViewModel(); + $template = $model->getTemplate(); + $this->assertTrue(empty($template)); + } + + public function testTemplateIsMutable() + { + $model = new ViewModel(); + $model->setTemplate('foo'); + $this->assertEquals('foo', $model->getTemplate()); + } + + public function testIsNotTerminatedByDefault() + { + $model = new ViewModel(); + $this->assertFalse($model->terminate()); + } + + public function testTerminationFlagIsMutable() + { + $model = new ViewModel(); + $model->setTerminal(true); + $this->assertTrue($model->terminate()); + } } From 7a07ac377b1f62d301dc61aacb69184a9b739fda Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 9 Feb 2012 22:18:07 -0600 Subject: [PATCH 16/69] Created scaffolding for View tests - Created test class and imports - Created setUp() routine - Stubbed required tests for behavior --- test/ViewTest.php | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/ViewTest.php diff --git a/test/ViewTest.php b/test/ViewTest.php new file mode 100644 index 00000000..1bc07ca2 --- /dev/null +++ b/test/ViewTest.php @@ -0,0 +1,90 @@ +request = new Request; + $this->response = new Response; + $this->model = new ViewModel; + $this->view = new View; + + $this->view->setRequest($this->request); + $this->view->setResponse($this->response); + } + + public function testRendersViewModelWithNoChildren() + { + $this->markTestIncomplete(); + } + + public function testRendersViewModelWithChildren() + { + $this->markTestIncomplete(); + } + + public function testRendersTreeOfModels() + { + $this->markTestIncomplete(); + } + + public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() + { + $this->markTestIncomplete(); + } + + public function testTerminalChildRaisesException() + { + $this->markTestIncomplete(); + } + + public function testChildrenAreCapturedToParentVariables() + { + $this->markTestIncomplete(); + } + + public function testOmittingCaptureToValueInChildLeadsToOmissionInParent() + { + $this->markTestIncomplete(); + } + + public function testResponseStrategyIsTriggeredForParentModel() + { + $this->markTestIncomplete(); + } + + public function testResponseStrategyIsNotTriggeredForChildModel() + { + $this->markTestIncomplete(); + } +} From 56bbdac4ed37ce52ee14473e89fb459126f49551 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 9 Feb 2012 23:15:42 -0600 Subject: [PATCH 17/69] Initial tests for View - Created mock strategy, VarExportRenderer - Created scaffolding method, attachTestStrategies() - Tested: - renders view model with no children - renders view model with children --- src/View.php | 31 ++++++++++ test/TestAsset/Renderer/VarExportRenderer.php | 56 +++++++++++++++++++ test/ViewTest.php | 43 +++++++++++++- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/TestAsset/Renderer/VarExportRenderer.php diff --git a/src/View.php b/src/View.php index 9439091d..ddc2b97d 100644 --- a/src/View.php +++ b/src/View.php @@ -228,6 +228,11 @@ public function addResponseStrategy($callable, $priority = 1) */ public function render(Model $model) { + // If we have children, render them first + if ($model->hasChildren()) { + $this->renderChildren($model); + } + $event = $this->getEvent(); $event->setModel($model); $events = $this->events(); @@ -245,11 +250,37 @@ public function render(Model $model) $rendered = $renderer->render($model); + // If this is a child model, return the rendered content; do not + // invoke the response strategy. + $options = $model->getOptions(); + if (array_key_exists('has_parent', $options) && $options['has_parent']) { + return $rendered; + } + $event->setResult($rendered); $events->trigger('response', $event); } + /** + * Loop through children, rendering each + * + * @param Model $model + * @return void + */ + protected function renderChildren(Model $model) + { + foreach ($model as $child) { + $child->setOption('has_parent', true); + $result = $this->render($child); + $child->setOption('has_parent', null); + $capture = $child->captureTo(); + if (!empty($capture)) { + $model->setVariable($capture, $result); + } + } + } + /** * Create and return ViewEvent used by render() * diff --git a/test/TestAsset/Renderer/VarExportRenderer.php b/test/TestAsset/Renderer/VarExportRenderer.php new file mode 100644 index 00000000..6bf24173 --- /dev/null +++ b/test/TestAsset/Renderer/VarExportRenderer.php @@ -0,0 +1,56 @@ +getVariables(); + return var_export($values, true); + } +} diff --git a/test/ViewTest.php b/test/ViewTest.php index 1bc07ca2..4377d128 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -22,6 +22,7 @@ namespace ZendTest\View; use PHPUnit_Framework_TestCase as TestCase, + stdClass, Zend\Http\Request, Zend\Http\Response, Zend\View\Model\ViewModel, @@ -43,14 +44,52 @@ public function setUp() $this->view->setResponse($this->response); } + public function attachTestStrategies() + { + $this->view->addRenderingStrategy(function ($e) { + return new TestAsset\Renderer\VarExportRenderer(); + }); + $this->result = $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + } + public function testRendersViewModelWithNoChildren() { - $this->markTestIncomplete(); + $this->attachTestStrategies(); + $variables = array( + 'foo' => 'bar', + 'bar' => 'baz', + ); + $this->model->setVariables($variables); + $this->view->render($this->model); + + $this->assertEquals(var_export($variables, true), $this->result->content); } public function testRendersViewModelWithChildren() { - $this->markTestIncomplete(); + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $expected = var_export(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + 'child2' => var_export(array('bar' => 'baz'), true), + ), true); + $this->assertEquals($expected, $this->result->content); } public function testRendersTreeOfModels() From bf1f4cdd8cbafa90f6710693bbfc3c99f79246a1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 08:55:55 -0600 Subject: [PATCH 18/69] Added test for rendering tree of models --- test/ViewTest.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/ViewTest.php b/test/ViewTest.php index 4377d128..6da6c87b 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -94,7 +94,28 @@ public function testRendersViewModelWithChildren() public function testRendersTreeOfModels() { - $this->markTestIncomplete(); + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + $child1->addChild($child2); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + + $this->view->render($this->model); + + $expected = var_export(array( + 'parent' => 'node', + 'child1' => var_export(array( + 'foo' => 'bar', + 'child2' => var_export(array('bar' => 'baz'), true), + ), true), + ), true); + $this->assertEquals($expected, $this->result->content); } public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() From ca223544e7b10fd68001deaa77aa45a745fc2c32 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 09:05:00 -0600 Subject: [PATCH 19/69] Added test for child rendering strategies - Children select their own rendering strategies --- test/ViewTest.php | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/ViewTest.php b/test/ViewTest.php index 6da6c87b..ecd6e7aa 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -25,6 +25,7 @@ stdClass, Zend\Http\Request, Zend\Http\Response, + Zend\View\Model, Zend\View\Model\ViewModel, Zend\View\PhpRenderer, Zend\View\Renderer, @@ -120,7 +121,43 @@ public function testRendersTreeOfModels() public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() { - $this->markTestIncomplete(); + $this->view->addRenderingStrategy(function ($e) { + $model = $e->getModel(); + if (!$model instanceof ViewModel) { + return; + } + return new TestAsset\Renderer\VarExportRenderer(); + }); + $this->view->addRenderingStrategy(function ($e) { + $model = $e->getModel(); + if (!$model instanceof Model\JsonModel) { + return; + } + return new Renderer\JsonRenderer(); + }, 10); // higher priority, so it matches earlier + $this->result = $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new Model\JsonModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $expected = var_export(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + 'child2' => json_encode(array('bar' => 'baz')), + ), true); + $this->assertEquals($expected, $this->result->content); } public function testTerminalChildRaisesException() From 56228030e7f5187d1608c0f5f5a74009cabe819f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 10:04:34 -0600 Subject: [PATCH 20/69] Added child termination checking - If a child is marked as terminal, an exception is now raised --- src/View.php | 3 +++ test/ViewTest.php | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/View.php b/src/View.php index ddc2b97d..d2d0af14 100644 --- a/src/View.php +++ b/src/View.php @@ -271,6 +271,9 @@ public function render(Model $model) protected function renderChildren(Model $model) { foreach ($model as $child) { + if ($child->terminate()) { + throw new Exception\DomainException('Inconsistent state; child view model is marked as terminal'); + } $child->setOption('has_parent', true); $result = $this->render($child); $child->setOption('has_parent', null); diff --git a/test/ViewTest.php b/test/ViewTest.php index ecd6e7aa..654e71a9 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -162,7 +162,17 @@ public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() public function testTerminalChildRaisesException() { - $this->markTestIncomplete(); + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + $child1->setTerminal(true); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + + $this->setExpectedException('Zend\View\Exception\DomainException'); + $this->view->render($this->model); } public function testChildrenAreCapturedToParentVariables() From a7cde37c84a0d76649848476ed67ae5d033cadfa Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 10:08:45 -0600 Subject: [PATCH 21/69] Test omitting capture to declaration in view model --- test/ViewTest.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test/ViewTest.php b/test/ViewTest.php index 654e71a9..cfdffb68 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -177,12 +177,32 @@ public function testTerminalChildRaisesException() public function testChildrenAreCapturedToParentVariables() { - $this->markTestIncomplete(); + // I wish there were a "markTestRedundant()" method in PHPUnit + $this->testRendersViewModelWithChildren(); } public function testOmittingCaptureToValueInChildLeadsToOmissionInParent() { - $this->markTestIncomplete(); + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + // Deliberately disable the "capture to" declaration + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo(null); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $expected = var_export(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + ), true); + $this->assertEquals($expected, $this->result->content); } public function testResponseStrategyIsTriggeredForParentModel() From 6a305949bd66bc0ef27060d93084a4fcaee87209 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 10:21:04 -0600 Subject: [PATCH 22/69] Added test ensuring response strategy is only triggered for top-level model --- test/ViewTest.php | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/ViewTest.php b/test/ViewTest.php index cfdffb68..768b3c39 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -21,7 +21,8 @@ namespace ZendTest\View; -use PHPUnit_Framework_TestCase as TestCase, +use ArrayObject, + PHPUnit_Framework_TestCase as TestCase, stdClass, Zend\Http\Request, Zend\Http\Response, @@ -207,11 +208,33 @@ public function testOmittingCaptureToValueInChildLeadsToOmissionInParent() public function testResponseStrategyIsTriggeredForParentModel() { - $this->markTestIncomplete(); + // I wish there were a "markTestRedundant()" method in PHPUnit + $this->testRendersViewModelWithChildren(); } public function testResponseStrategyIsNotTriggeredForChildModel() { - $this->markTestIncomplete(); + $this->view->addRenderingStrategy(function ($e) { + return new Renderer\JsonRenderer(); + }); + + $result = new ArrayObject; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result[] = $e->getResult(); + }); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $this->assertEquals(1, count($result)); } } From e25abc4527430d08a2c3fcfa28ab3f0a1a4aafff Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 10 Feb 2012 17:07:28 -0600 Subject: [PATCH 23/69] Created JsonStrategy - Selects JsonRenderer when: - JsonModel present, OR - application/json Accept header present - Injects response when: - Event renderer matches its internal renderer, AND - string result present, resulting in - injecting result into response content, and application/json header into response headers --- src/Strategy/JsonStrategy.php | 150 +++++++++++++++++++++++++++++ test/Strategy/JsonStrategyTest.php | 119 +++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 src/Strategy/JsonStrategy.php create mode 100644 test/Strategy/JsonStrategyTest.php diff --git a/src/Strategy/JsonStrategy.php b/src/Strategy/JsonStrategy.php new file mode 100644 index 00000000..bf490b47 --- /dev/null +++ b/src/Strategy/JsonStrategy.php @@ -0,0 +1,150 @@ +renderer = $renderer; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function attach(EventCollection $events) + { + $this->listeners[] = $events->attach('renderer', array($this, 'selectJsonRenderer')); + $this->listeners[] = $events->attach('response', array($this, 'injectJsonResponse')); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function detach(EventCollection $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Detect if we should use the JsonRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|JsonRenderer + */ + public function selectJsonRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if ($model instanceof Model\JsonModel) { + // JsonModel found + return $this->renderer; + } + + $request = $e->getRequest(); + if (!$request instanceof HttpRequest) { + // Not an HTTP request; cannot autodetermine + return; + } + + $headers = $request->headers(); + $accept = $headers->get('Accept'); + foreach ($accept->getPrioritized() as $mediaType) { + if (0 === strpos($mediaType, 'application/json')) { + // application/json Accept header found + return $this->renderer; + } + } + + // Not matched! + return; + } + + /** + * Inject the response with the JSON payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectJsonResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result)) { + // We don't have a string, and thus, no JSON + return; + } + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->headers(); + $headers->addHeaderLine('content-type', 'application/json'); + } +} diff --git a/test/Strategy/JsonStrategyTest.php b/test/Strategy/JsonStrategyTest.php new file mode 100644 index 00000000..2fc1b550 --- /dev/null +++ b/test/Strategy/JsonStrategyTest.php @@ -0,0 +1,119 @@ +renderer = new JsonRenderer; + $this->strategy = new JsonStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testJsonModelSelectsJsonStrategy() + { + $this->event->setModel(new Model\JsonModel()); + $result = $this->strategy->selectJsonRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testJsonAcceptHeaderSelectsJsonStrategy() + { + $request = new HttpRequest(); + $request->headers()->addHeaderLine('Accept', 'application/json'); + $this->event->setRequest($request); + $result = $this->strategy->selectJsonRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testLackOfJsonModelOrAcceptHeaderDoesNotSelectJsonStrategy() + { + $result = $this->strategy->selectJsonRenderer($this->event); + $this->assertNotSame($this->renderer, $result); + $this->assertNull($result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectJsonResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new JsonRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectJsonResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testNonStringResultDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($this->response); + + $this->strategy->injectJsonResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testMatchingRendererAndStringResultInjectsResponse() + { + $expected = json_encode(array('foo' => 'bar')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectJsonResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertEquals($expected, $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/json', $headers->get('content-type')->getFieldValue()); + } +} From 8d55e58cf4d6db5bbbffe9e0c1b714a1500ecceb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 11 Feb 2012 13:44:58 -0600 Subject: [PATCH 24/69] Renamed methods - s/selectJsonRenderer/selectRenderer/g - s/injectJsonResponse/injectResponse/g --- src/Strategy/JsonStrategy.php | 8 ++++---- test/Strategy/JsonStrategyTest.php | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Strategy/JsonStrategy.php b/src/Strategy/JsonStrategy.php index bf490b47..fa566952 100644 --- a/src/Strategy/JsonStrategy.php +++ b/src/Strategy/JsonStrategy.php @@ -67,8 +67,8 @@ public function __construct(JsonRenderer $renderer) */ public function attach(EventCollection $events) { - $this->listeners[] = $events->attach('renderer', array($this, 'selectJsonRenderer')); - $this->listeners[] = $events->attach('response', array($this, 'injectJsonResponse')); + $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); + $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); } /** @@ -93,7 +93,7 @@ public function detach(EventCollection $events) * @param ViewEvent $e * @return null|JsonRenderer */ - public function selectJsonRenderer(ViewEvent $e) + public function selectRenderer(ViewEvent $e) { $model = $e->getModel(); @@ -127,7 +127,7 @@ public function selectJsonRenderer(ViewEvent $e) * @param ViewEvent $e * @return void */ - public function injectJsonResponse(ViewEvent $e) + public function injectResponse(ViewEvent $e) { $renderer = $e->getRenderer(); if ($renderer !== $this->renderer) { diff --git a/test/Strategy/JsonStrategyTest.php b/test/Strategy/JsonStrategyTest.php index 2fc1b550..1b1908fe 100644 --- a/test/Strategy/JsonStrategyTest.php +++ b/test/Strategy/JsonStrategyTest.php @@ -49,7 +49,7 @@ public function setUp() public function testJsonModelSelectsJsonStrategy() { $this->event->setModel(new Model\JsonModel()); - $result = $this->strategy->selectJsonRenderer($this->event); + $result = $this->strategy->selectRenderer($this->event); $this->assertSame($this->renderer, $result); } @@ -58,13 +58,13 @@ public function testJsonAcceptHeaderSelectsJsonStrategy() $request = new HttpRequest(); $request->headers()->addHeaderLine('Accept', 'application/json'); $this->event->setRequest($request); - $result = $this->strategy->selectJsonRenderer($this->event); + $result = $this->strategy->selectRenderer($this->event); $this->assertSame($this->renderer, $result); } public function testLackOfJsonModelOrAcceptHeaderDoesNotSelectJsonStrategy() { - $result = $this->strategy->selectJsonRenderer($this->event); + $result = $this->strategy->selectRenderer($this->event); $this->assertNotSame($this->renderer, $result); $this->assertNull($result); } @@ -82,13 +82,13 @@ public function testNonMatchingRendererDoesNotInjectResponse() $this->event->setResponse($this->response); // test empty renderer - $this->strategy->injectJsonResponse($this->event); + $this->strategy->injectResponse($this->event); $this->assertResponseNotInjected(); // test non-matching renderer $renderer = new JsonRenderer(); $this->event->setRenderer($renderer); - $this->strategy->injectJsonResponse($this->event); + $this->strategy->injectResponse($this->event); $this->assertResponseNotInjected(); } @@ -98,7 +98,7 @@ public function testNonStringResultDoesNotInjectResponse() $this->event->setRenderer($this->renderer); $this->event->setResult($this->response); - $this->strategy->injectJsonResponse($this->event); + $this->strategy->injectResponse($this->event); $this->assertResponseNotInjected(); } @@ -109,7 +109,7 @@ public function testMatchingRendererAndStringResultInjectsResponse() $this->event->setRenderer($this->renderer); $this->event->setResult($expected); - $this->strategy->injectJsonResponse($this->event); + $this->strategy->injectResponse($this->event); $content = $this->response->getContent(); $headers = $this->response->headers(); $this->assertEquals($expected, $content); From 7c00f544f69aa95b2277a80982e4b6931fd14fb1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 11 Feb 2012 14:03:24 -0600 Subject: [PATCH 25/69] Added feed strategy - Added feed strategy, which handles RSS and Atom feed types --- src/Strategy/FeedStrategy.php | 168 ++++++++++++++++++++++++ test/Strategy/FeedStrategyTest.php | 203 +++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 src/Strategy/FeedStrategy.php create mode 100644 test/Strategy/FeedStrategyTest.php diff --git a/src/Strategy/FeedStrategy.php b/src/Strategy/FeedStrategy.php new file mode 100644 index 00000000..fc1c3c3b --- /dev/null +++ b/src/Strategy/FeedStrategy.php @@ -0,0 +1,168 @@ +renderer = $renderer; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function attach(EventCollection $events) + { + $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); + $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function detach(EventCollection $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Detect if we should use the FeedRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|FeedRenderer + */ + public function selectRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if ($model instanceof Model\FeedModel) { + // FeedModel found + return $this->renderer; + } + + $request = $e->getRequest(); + if (!$request instanceof HttpRequest) { + // Not an HTTP request; cannot autodetermine + return; + } + + $headers = $request->headers(); + $accept = $headers->get('Accept'); + foreach ($accept->getPrioritized() as $mediaType) { + if (0 === strpos($mediaType, 'application/rss+xml')) { + // application/rss+xml Accept header found + $this->renderer->setFeedType('rss'); + return $this->renderer; + } + if (0 === strpos($mediaType, 'application/atom+xml')) { + // application/atom+xml Accept header found + $this->renderer->setFeedType('atom'); + return $this->renderer; + } + } + + // Not matched! + return; + } + + /** + * Inject the response with the feed payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result) && !$result instanceof Feed) { + // We don't have a string, and thus, no feed + return; + } + + // If the result is a feed, export it + if ($result instanceof Feed) { + $result = $result->export($renderer->getFeedType()); + } + + // Get the content-type header based on feed type + $feedType = $renderer->getFeedType(); + $feedType = ('rss' == $feedType) + ? 'application/rss+xml' + : 'application/atom+xml'; + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->headers(); + $headers->addHeaderLine('content-type', $feedType); + } +} diff --git a/test/Strategy/FeedStrategyTest.php b/test/Strategy/FeedStrategyTest.php new file mode 100644 index 00000000..51076771 --- /dev/null +++ b/test/Strategy/FeedStrategyTest.php @@ -0,0 +1,203 @@ +renderer = new FeedRenderer; + $this->strategy = new FeedStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testFeedModelSelectsFeedStrategy() + { + $this->event->setModel(new Model\FeedModel()); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testRssAcceptHeaderSelectsFeedStrategy() + { + $request = new HttpRequest(); + $request->headers()->addHeaderLine('Accept', 'application/rss+xml'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testAtomAcceptHeaderSelectsFeedStrategy() + { + $request = new HttpRequest(); + $request->headers()->addHeaderLine('Accept', 'application/atom+xml'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testLackOfFeedModelOrAcceptHeaderDoesNotSelectFeedStrategy() + { + $result = $this->strategy->selectRenderer($this->event); + $this->assertNotSame($this->renderer, $result); + $this->assertNull($result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new FeedRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testNonStringOrFeedResultDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($this->response); + + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testMatchingRendererAndStringResultInjectsResponse() + { + $this->renderer->setFeedType('atom'); + $expected = 'content'; + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertEquals($expected, $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/atom+xml', $headers->get('content-type')->getFieldValue()); + } + + protected function getFeedData($type) + { + return array( + 'copyright' => date('Y'), + 'date_created' => time(), + 'date_modified' => time(), + 'last_build_date' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/', + 'language' => 'en_US', + 'feed_link' => array( + 'link' => 'http://framework.zend.com/feed.xml', + 'type' => $type, + ), + 'link' => 'http://framework.zend.com/feed.xml', + 'title' => 'Testing', + 'encoding' => 'UTF-8', + 'base_url' => 'http://framework.zend.com/', + 'entries' => array( + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/1', + 'link' => 'http://framework.zend.com/1', + 'title' => 'Test 1', + ), + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/2', + 'link' => 'http://framework.zend.com/2', + 'title' => 'Test 2', + ), + ), + ); + } + + public function testMatchingRendererAndFeedResultInjectsResponse() + { + $this->renderer->setFeedType('atom'); + $expected = FeedFactory::factory($this->getFeedData('atom')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertEquals($expected->export('atom'), $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/atom+xml', $headers->get('content-type')->getFieldValue()); + } + + public function testResponseContentTypeIsBasedOnFeedType() + { + $this->renderer->setFeedType('rss'); + $expected = FeedFactory::factory($this->getFeedData('rss')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertEquals($expected->export('rss'), $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/rss+xml', $headers->get('content-type')->getFieldValue()); + } +} From 1f5ef441664832f06856f1c4b0ab49d1929af11b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 11 Feb 2012 15:25:17 -0600 Subject: [PATCH 26/69] Created PhpRendererStrategy - Always selects renderer - Populates response from: - result, if non-empty - one of a list of placeholders, otherwise --- src/Strategy/PhpRendererStrategy.php | 164 ++++++++++++++++++++++ test/Strategy/PhpRendererStrategyTest.php | 116 +++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 src/Strategy/PhpRendererStrategy.php create mode 100644 test/Strategy/PhpRendererStrategyTest.php diff --git a/src/Strategy/PhpRendererStrategy.php b/src/Strategy/PhpRendererStrategy.php new file mode 100644 index 00000000..3ede040a --- /dev/null +++ b/src/Strategy/PhpRendererStrategy.php @@ -0,0 +1,164 @@ +renderer = $renderer; + } + + /** + * Set list of possible content placeholders + * + * @param array contentPlaceholders + * @return PhpRendererStrategy + */ + public function setContentPlaceholders(array $contentPlaceholders) + { + $this->contentPlaceholders = $contentPlaceholders; + return $this; + } + + /** + * Get list of possible content placeholders + * + * @return array + */ + public function getContentPlaceholders() + { + return $this->contentPlaceholders; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function attach(EventCollection $events) + { + $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); + $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventCollection $events + * @return void + */ + public function detach(EventCollection $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Select the PhpRenderer; typically, this will be registered last or at + * low priority. + * + * @param ViewEvent $e + * @return PhpRenderer + */ + public function selectRenderer(ViewEvent $e) + { + return $this->renderer; + } + + /** + * Populate the response object from the View + * + * Populates the content of the response object from the view rendering + * results. + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + return; + } + + $result = $e->getResult(); + $response = $e->getResponse(); + + // Set content + // If content is empty, check common placeholders to determine if they are + // populated, and set the content from them. + if (empty($result)) { + $placeholders = $renderer->plugin('placeholder'); + $registry = $placeholders->getRegistry(); + foreach ($this->contentPlaceholders as $placeholder) { + if ($registry->containerExists($placeholder)) { + $result = (string) $registry->getContainer($placeholder); + break; + } + } + } + $response->setContent($result); + } +} diff --git a/test/Strategy/PhpRendererStrategyTest.php b/test/Strategy/PhpRendererStrategyTest.php new file mode 100644 index 00000000..2d06ee0b --- /dev/null +++ b/test/Strategy/PhpRendererStrategyTest.php @@ -0,0 +1,116 @@ +renderer = new PhpRenderer; + $this->strategy = new PhpRendererStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testSelectRendererAlwaysSelectsPhpRenderer() + { + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->headers(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new PhpRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testResponseContentSetToContentPlaceholderWhenResultAndArticlePlaceholderAreEmpty() + { + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Content', $content); + } + + public function testResponseContentSetToArticlePlaceholderWhenResultIsEmptyAndBothArticleAndContentPlaceholdersSet() + { + $this->renderer->placeholder('article')->set('Article Content'); + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Article Content', $content); + } + + public function testResponseContentSetToResultIfNotEmpty() + { + $this->renderer->placeholder('article')->set('Article Content'); + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer) + ->setResult('Result Content'); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Result Content', $content); + } +} From 1a6cf26dc4c6fc021b246fd66c08384d49065505 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 11 Feb 2012 15:33:01 -0600 Subject: [PATCH 27/69] Revised DefaultRenderingStrategy - Removed all but the ability to render the view from the strategy - Moved placeholder checks to PhpRendererStrategyTest --- test/Strategy/PhpRendererStrategyTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Strategy/PhpRendererStrategyTest.php b/test/Strategy/PhpRendererStrategyTest.php index 2d06ee0b..75d02418 100644 --- a/test/Strategy/PhpRendererStrategyTest.php +++ b/test/Strategy/PhpRendererStrategyTest.php @@ -24,6 +24,8 @@ use PHPUnit_Framework_TestCase as TestCase, Zend\Http\Request as HttpRequest, Zend\Http\Response as HttpResponse, + Zend\Registry, + Zend\View\Helper\Placeholder\Registry as PlaceholderRegistry, Zend\View\Model, Zend\View\PhpRenderer, Zend\View\Strategy\PhpRendererStrategy, @@ -40,6 +42,11 @@ class PhpRendererStrategyTest extends TestCase { public function setUp() { + // Necessary to ensure placeholders do not persist between individual tests + if (Registry::isRegistered(PlaceholderRegistry::REGISTRY_KEY)) { + Registry::getInstance()->offsetUnset(PlaceholderRegistry::REGISTRY_KEY); + } + $this->renderer = new PhpRenderer; $this->strategy = new PhpRendererStrategy($this->renderer); $this->event = new ViewEvent(); From fd29f728513413685727bb3b02ff66f215d420ed Mon Sep 17 00:00:00 2001 From: Adam Lundrigan Date: Sat, 11 Feb 2012 20:14:17 -0330 Subject: [PATCH 28/69] Fixed typo in Zend\View\View: RuntimeException -> Exception\RuntimeException --- src/View.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View.php b/src/View.php index d2d0af14..0ff4f660 100644 --- a/src/View.php +++ b/src/View.php @@ -241,7 +241,7 @@ public function render(Model $model) }); $renderer = $results->last(); if (!$renderer) { - throw new RuntimeException(sprintf( + throw new Exception\RuntimeException(sprintf( '%s: no renderer selected!', __METHOD__ )); From 566cdb81cdbb40fd70eb413830510c4e99ffb6d0 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 08:33:27 -0600 Subject: [PATCH 29/69] Moved PhpRenderer under Zend\View\Renderer namespace - Moved PhpRenderer under Zend\View\Renderer namespace - Updated all consumers, and related tests --- src/Helper/Json.php | 10 +++++----- src/{ => Renderer}/PhpRenderer.php | 14 ++++++++++---- src/Strategy/PhpRendererStrategy.php | 2 +- test/Helper/DeclareVarsTest.php | 2 +- test/Helper/FieldsetTest.php | 2 +- test/Helper/FormButtonTest.php | 2 +- test/Helper/FormCheckboxTest.php | 2 +- test/Helper/FormCsrfTest.php | 2 +- test/Helper/FormErrorsTest.php | 2 +- test/Helper/FormFileTest.php | 2 +- test/Helper/FormImageTest.php | 2 +- test/Helper/FormLabelTest.php | 2 +- test/Helper/FormMultiCheckboxTest.php | 2 +- test/Helper/FormPasswordTest.php | 2 +- test/Helper/FormRadioTest.php | 2 +- test/Helper/FormResetTest.php | 2 +- test/Helper/FormSelectTest.php | 2 +- test/Helper/FormSubmitTest.php | 2 +- test/Helper/FormTest.php | 2 +- test/Helper/FormTextTest.php | 2 +- test/Helper/FormTextareaTest.php | 2 +- test/Helper/GravatarTest.php | 2 +- test/Helper/HeadLinkTest.php | 2 +- test/Helper/HeadMetaTest.php | 2 +- test/Helper/HtmlFlashTest.php | 2 +- test/Helper/HtmlListTest.php | 2 +- test/Helper/HtmlObjectTest.php | 2 +- test/Helper/HtmlPageTest.php | 2 +- test/Helper/HtmlQuicktimeTest.php | 2 +- test/Helper/Navigation/AbstractTest.php | 2 +- test/Helper/PaginationControlTest.php | 2 +- test/Helper/PartialLoopTest.php | 2 +- test/Helper/PartialTest.php | 2 +- .../Placeholder/StandaloneContainerTest.php | 2 +- test/Helper/PlaceholderTest.php | 2 +- test/Helper/RenderToPlaceholderTest.php | 2 +- test/HelperBrokerTest.php | 4 ++-- test/PhpRendererTest.php | 2 +- test/Strategy/PhpRendererStrategyTest.php | 2 +- test/TemplatePathStackTest.php | 16 ++++++++-------- test/ViewEventTest.php | 2 +- test/ViewTest.php | 2 +- 42 files changed, 63 insertions(+), 57 deletions(-) rename src/{ => Renderer}/PhpRenderer.php (98%) diff --git a/src/Helper/Json.php b/src/Helper/Json.php index 1f324db9..b0252bcc 100644 --- a/src/Helper/Json.php +++ b/src/Helper/Json.php @@ -26,7 +26,7 @@ use Zend\Http\Response, Zend\Json\Json as JsonFormatter, - Zend\Layout\Layout; + Zend\Layout\Layout as LayoutComponent; /** * Helper for simplifying JSON responses @@ -39,7 +39,7 @@ class Json extends AbstractHelper { /** - * @var Layout + * @var LayoutComponent */ protected $layout; @@ -51,10 +51,10 @@ class Json extends AbstractHelper /** * Set the layout object * - * @param Layout $layout + * @param LayoutComponent $layout * @return Json */ - public function setLayout(Layout $layout) + public function setLayout(LayoutComponent $layout) { $this->layout = $layout; return $this; @@ -99,7 +99,7 @@ public function __invoke($data, $keepLayouts = false) $data = JsonFormatter::encode($data, null, $options); - if (!$keepLayouts && ($this->layout instanceof Layout)) { + if (!$keepLayouts && ($this->layout instanceof LayoutComponent)) { $this->layout->disableLayout(); } diff --git a/src/PhpRenderer.php b/src/Renderer/PhpRenderer.php similarity index 98% rename from src/PhpRenderer.php rename to src/Renderer/PhpRenderer.php index 1e1c0d3c..86d0ff1c 100644 --- a/src/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -21,11 +21,17 @@ /** * @namespace */ -namespace Zend\View; +namespace Zend\View\Renderer; -use Zend\Filter\FilterChain, +use ArrayAccess, + Zend\Filter\FilterChain, Zend\Loader\Pluggable, - ArrayAccess; + Zend\View\Exception, + Zend\View\HelperBroker, + Zend\View\Model, + Zend\View\Renderer, + Zend\View\Resolver, + Zend\View\Variables; /** * Abstract class for Zend_View to help enforce private constructs. @@ -105,7 +111,7 @@ public function __construct($config = array()) * * Returns the object instance, as it is its own template engine * - * @return \Zend\View\PhpRenderer + * @return PhpRenderer */ public function getEngine() { diff --git a/src/Strategy/PhpRendererStrategy.php b/src/Strategy/PhpRendererStrategy.php index 3ede040a..4b89b2bb 100644 --- a/src/Strategy/PhpRendererStrategy.php +++ b/src/Strategy/PhpRendererStrategy.php @@ -26,7 +26,7 @@ Zend\Http\Request as HttpRequest, Zend\Http\Response as HttpResponse, Zend\View\Model, - Zend\View\PhpRenderer, + Zend\View\Renderer\PhpRenderer, Zend\View\ViewEvent; /** diff --git a/test/Helper/DeclareVarsTest.php b/test/Helper/DeclareVarsTest.php index 40f6da81..aca934d1 100644 --- a/test/Helper/DeclareVarsTest.php +++ b/test/Helper/DeclareVarsTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\DeclareVars; /** diff --git a/test/Helper/FieldsetTest.php b/test/Helper/FieldsetTest.php index b088d8e6..5e78442e 100644 --- a/test/Helper/FieldsetTest.php +++ b/test/Helper/FieldsetTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\Fieldset; /** diff --git a/test/Helper/FormButtonTest.php b/test/Helper/FormButtonTest.php index 14bff0eb..032a15ed 100644 --- a/test/Helper/FormButtonTest.php +++ b/test/Helper/FormButtonTest.php @@ -46,7 +46,7 @@ class FormButtonTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormButton(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormCheckboxTest.php b/test/Helper/FormCheckboxTest.php index ad8bddca..ce995f8f 100644 --- a/test/Helper/FormCheckboxTest.php +++ b/test/Helper/FormCheckboxTest.php @@ -46,7 +46,7 @@ public function setUp() $registry = \Zend\Registry::getInstance(); unset($registry['Zend_View_Helper_Doctype']); } - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormCheckbox(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormCsrfTest.php b/test/Helper/FormCsrfTest.php index da850704..e20bc739 100644 --- a/test/Helper/FormCsrfTest.php +++ b/test/Helper/FormCsrfTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use PHPUnit_Framework_TestCase as TestCase, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\FormCsrf; /** diff --git a/test/Helper/FormErrorsTest.php b/test/Helper/FormErrorsTest.php index e393747e..1b8236a3 100644 --- a/test/Helper/FormErrorsTest.php +++ b/test/Helper/FormErrorsTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\FormErrors; /** diff --git a/test/Helper/FormFileTest.php b/test/Helper/FormFileTest.php index 889a9b76..40617334 100644 --- a/test/Helper/FormFileTest.php +++ b/test/Helper/FormFileTest.php @@ -52,7 +52,7 @@ protected function setUp() $registry = \Zend\Registry::getInstance(); unset($registry['Zend_View_Helper_Doctype']); } - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormFile(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormImageTest.php b/test/Helper/FormImageTest.php index 38188983..acf32722 100644 --- a/test/Helper/FormImageTest.php +++ b/test/Helper/FormImageTest.php @@ -46,7 +46,7 @@ class FormImageTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormImage(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormLabelTest.php b/test/Helper/FormLabelTest.php index 57ff6d3a..1dcce4d0 100644 --- a/test/Helper/FormLabelTest.php +++ b/test/Helper/FormLabelTest.php @@ -47,7 +47,7 @@ class FormLabelTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormLabel(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormMultiCheckboxTest.php b/test/Helper/FormMultiCheckboxTest.php index 743318ee..48620e9f 100644 --- a/test/Helper/FormMultiCheckboxTest.php +++ b/test/Helper/FormMultiCheckboxTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use Zend\Registry, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\FormMultiCheckbox; /** diff --git a/test/Helper/FormPasswordTest.php b/test/Helper/FormPasswordTest.php index 0d12cd63..4d8dcbc5 100644 --- a/test/Helper/FormPasswordTest.php +++ b/test/Helper/FormPasswordTest.php @@ -52,7 +52,7 @@ protected function setUp() $registry = \Zend\Registry::getInstance(); unset($registry['Zend_View_Helper_Doctype']); } - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormPassword(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormRadioTest.php b/test/Helper/FormRadioTest.php index 5b7e5e76..386e5aca 100644 --- a/test/Helper/FormRadioTest.php +++ b/test/Helper/FormRadioTest.php @@ -43,7 +43,7 @@ class FormRadioTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormRadio(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormResetTest.php b/test/Helper/FormResetTest.php index 2d90a4a2..33ccfd9c 100644 --- a/test/Helper/FormResetTest.php +++ b/test/Helper/FormResetTest.php @@ -50,7 +50,7 @@ public function setUp() $registry = \Zend\Registry::getInstance(); unset($registry['Zend_View_Helper_Doctype']); } - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormReset(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormSelectTest.php b/test/Helper/FormSelectTest.php index b3bda31c..f914c281 100644 --- a/test/Helper/FormSelectTest.php +++ b/test/Helper/FormSelectTest.php @@ -46,7 +46,7 @@ class FormSelectTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormSelect(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormSubmitTest.php b/test/Helper/FormSubmitTest.php index 30b62e0c..631fef95 100644 --- a/test/Helper/FormSubmitTest.php +++ b/test/Helper/FormSubmitTest.php @@ -50,7 +50,7 @@ public function setUp() $registry = \Zend\Registry::getInstance(); unset($registry['Zend_View_Helper_Doctype']); } - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormSubmit(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormTest.php b/test/Helper/FormTest.php index a31ccd5d..a58b1da0 100644 --- a/test/Helper/FormTest.php +++ b/test/Helper/FormTest.php @@ -46,7 +46,7 @@ class FormTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\Form(); $this->helper->setView($this->view); } diff --git a/test/Helper/FormTextTest.php b/test/Helper/FormTextTest.php index 3ec5f392..2d20450f 100644 --- a/test/Helper/FormTextTest.php +++ b/test/Helper/FormTextTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use Zend\Registry, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\FormText as FormTextHelper; /** diff --git a/test/Helper/FormTextareaTest.php b/test/Helper/FormTextareaTest.php index 37e3fa78..e6f50f18 100644 --- a/test/Helper/FormTextareaTest.php +++ b/test/Helper/FormTextareaTest.php @@ -51,7 +51,7 @@ class FormTextareaTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->view = new \Zend\View\PhpRenderer(); + $this->view = new \Zend\View\Renderer\PhpRenderer(); $this->helper = new \Zend\View\Helper\FormTextarea(); $this->helper->setView($this->view); } diff --git a/test/Helper/GravatarTest.php b/test/Helper/GravatarTest.php index a8780c25..21d4ed5c 100644 --- a/test/Helper/GravatarTest.php +++ b/test/Helper/GravatarTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use PHPUnit_Framework_TestCase as TestCase, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\Gravatar; /** diff --git a/test/Helper/HeadLinkTest.php b/test/Helper/HeadLinkTest.php index ab44c516..838a7710 100644 --- a/test/Helper/HeadLinkTest.php +++ b/test/Helper/HeadLinkTest.php @@ -26,7 +26,7 @@ use Zend\Registry, Zend\View\Helper\Placeholder\Registry as PlaceholderRegistry, Zend\View\Helper, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Exception as ViewException; /** diff --git a/test/Helper/HeadMetaTest.php b/test/Helper/HeadMetaTest.php index f5d059d1..c6757ddc 100644 --- a/test/Helper/HeadMetaTest.php +++ b/test/Helper/HeadMetaTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use Zend\Registry, Zend\View\Helper\Placeholder\Registry as PlaceholderRegistry, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper, Zend\View\Exception as ViewException; diff --git a/test/Helper/HtmlFlashTest.php b/test/Helper/HtmlFlashTest.php index 7af3e824..43dd841f 100644 --- a/test/Helper/HtmlFlashTest.php +++ b/test/Helper/HtmlFlashTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\HtmlFlash; /** diff --git a/test/Helper/HtmlListTest.php b/test/Helper/HtmlListTest.php index 75218a9c..e76b23fa 100644 --- a/test/Helper/HtmlListTest.php +++ b/test/Helper/HtmlListTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use Zend\View\Helper, - Zend\View\PhpRenderer as View; + Zend\View\Renderer\PhpRenderer as View; /** * @category Zend diff --git a/test/Helper/HtmlObjectTest.php b/test/Helper/HtmlObjectTest.php index 8b16e49f..98c89ded 100644 --- a/test/Helper/HtmlObjectTest.php +++ b/test/Helper/HtmlObjectTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\Doctype, Zend\View\Helper\HtmlObject; diff --git a/test/Helper/HtmlPageTest.php b/test/Helper/HtmlPageTest.php index 8a1ea2aa..8b143d42 100644 --- a/test/Helper/HtmlPageTest.php +++ b/test/Helper/HtmlPageTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\HtmlPage; /** diff --git a/test/Helper/HtmlQuicktimeTest.php b/test/Helper/HtmlQuicktimeTest.php index 84d3feb0..63cc573c 100644 --- a/test/Helper/HtmlQuicktimeTest.php +++ b/test/Helper/HtmlQuicktimeTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\HtmlQuicktime; /** diff --git a/test/Helper/Navigation/AbstractTest.php b/test/Helper/Navigation/AbstractTest.php index 77034eed..0485ac5b 100644 --- a/test/Helper/Navigation/AbstractTest.php +++ b/test/Helper/Navigation/AbstractTest.php @@ -30,7 +30,7 @@ Zend\Acl\Resource\GenericResource, Zend\Config\Xml as XmlConfig, Zend\Translator\Translator, - Zend\View\PhpRenderer; + Zend\View\Renderer\PhpRenderer; /** * Base class for navigation view helper tests diff --git a/test/Helper/PaginationControlTest.php b/test/Helper/PaginationControlTest.php index f2e0e41a..b0e17ca8 100644 --- a/test/Helper/PaginationControlTest.php +++ b/test/Helper/PaginationControlTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper, Zend\Paginator; diff --git a/test/Helper/PartialLoopTest.php b/test/Helper/PartialLoopTest.php index 34d362e8..08be7ec1 100644 --- a/test/Helper/PartialLoopTest.php +++ b/test/Helper/PartialLoopTest.php @@ -28,7 +28,7 @@ Iterator, PHPUnit_Framework_TestCase as TestCase, Zend\View\Helper\PartialLoop, - Zend\View\PhpRenderer as View; + Zend\View\Renderer\PhpRenderer as View; /** * Test class for Zend_View_Helper_PartialLoop. diff --git a/test/Helper/PartialTest.php b/test/Helper/PartialTest.php index c6318de4..78fe27e5 100644 --- a/test/Helper/PartialTest.php +++ b/test/Helper/PartialTest.php @@ -26,7 +26,7 @@ use PHPUnit_Framework_TestCase as TestCase, Zend\View\Helper\Partial, - Zend\View\PhpRenderer as View; + Zend\View\Renderer\PhpRenderer as View; /** * Test class for Partial view helper. diff --git a/test/Helper/Placeholder/StandaloneContainerTest.php b/test/Helper/Placeholder/StandaloneContainerTest.php index 145b00c6..0e744728 100644 --- a/test/Helper/Placeholder/StandaloneContainerTest.php +++ b/test/Helper/Placeholder/StandaloneContainerTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper\Placeholder; -use Zend\View\PhpRenderer as View; +use Zend\View\Renderer\PhpRenderer as View; /** * Test class for Zend_View_Helper_Placeholder_StandaloneContainer. diff --git a/test/Helper/PlaceholderTest.php b/test/Helper/PlaceholderTest.php index 82994fc9..8f36d8b2 100644 --- a/test/Helper/PlaceholderTest.php +++ b/test/Helper/PlaceholderTest.php @@ -25,7 +25,7 @@ namespace ZendTest\View\Helper; use Zend\Registry, - Zend\View\PhpRenderer as View, + Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper, Zend\View\Helper\Placeholder\Registry as PlaceholderRegistry; diff --git a/test/Helper/RenderToPlaceholderTest.php b/test/Helper/RenderToPlaceholderTest.php index a8f20af0..aefe332c 100644 --- a/test/Helper/RenderToPlaceholderTest.php +++ b/test/Helper/RenderToPlaceholderTest.php @@ -24,7 +24,7 @@ */ namespace ZendTest\View\Helper; -use Zend\View\PhpRenderer as View, +use Zend\View\Renderer\PhpRenderer as View, Zend\View\Helper\Placeholder as PlaceholderHelper; /** diff --git a/test/HelperBrokerTest.php b/test/HelperBrokerTest.php index e2f5a2dc..d4223718 100644 --- a/test/HelperBrokerTest.php +++ b/test/HelperBrokerTest.php @@ -22,7 +22,7 @@ namespace ZendTest\View; use Zend\View\HelperBroker, - Zend\View\PhpRenderer; + Zend\View\Renderer\PhpRenderer; /** * @category Zend @@ -32,7 +32,7 @@ * @license http://framework.zend.com/license/new-bsd New BSD License * @group Zend_View */ -class ViewTest extends \PHPUnit_Framework_TestCase +class HelperBrokerTest extends \PHPUnit_Framework_TestCase { public function setUp() { diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index f665a081..4702945a 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -21,7 +21,7 @@ namespace ZendTest\View; -use Zend\View\PhpRenderer, +use Zend\View\Renderer\PhpRenderer, Zend\View\Model\ViewModel, Zend\View\Resolver\TemplateMapResolver, Zend\View\Resolver\TemplatePathStack, diff --git a/test/Strategy/PhpRendererStrategyTest.php b/test/Strategy/PhpRendererStrategyTest.php index 75d02418..ff2cd76c 100644 --- a/test/Strategy/PhpRendererStrategyTest.php +++ b/test/Strategy/PhpRendererStrategyTest.php @@ -27,7 +27,7 @@ Zend\Registry, Zend\View\Helper\Placeholder\Registry as PlaceholderRegistry, Zend\View\Model, - Zend\View\PhpRenderer, + Zend\View\Renderer\PhpRenderer, Zend\View\Strategy\PhpRendererStrategy, Zend\View\ViewEvent; diff --git a/test/TemplatePathStackTest.php b/test/TemplatePathStackTest.php index 6a3c0b64..1ceed6dd 100644 --- a/test/TemplatePathStackTest.php +++ b/test/TemplatePathStackTest.php @@ -21,7 +21,7 @@ namespace ZendTest\View; -use Zend\View\TemplatePathStack; +use Zend\View\Resolver\TemplatePathStack; /** * @category Zend @@ -128,7 +128,7 @@ public function testDoesNotAllowParentDirectoryTraversalByDefault() $this->stack->addPath(__DIR__ . '/_templates'); $this->setExpectedException('Zend\View\Exception', 'parent directory traversal'); - $test = $this->stack->getScriptPath('../_stubs/scripts/LfiProtectionCheck.phtml'); + $test = $this->stack->resolve('../_stubs/scripts/LfiProtectionCheck.phtml'); } public function testDisablingLfiProtectionAllowsParentDirectoryTraversal() @@ -136,28 +136,28 @@ public function testDisablingLfiProtectionAllowsParentDirectoryTraversal() $this->stack->setLfiProtection(false) ->addPath(__DIR__ . '/_templates'); - $test = $this->stack->getScriptPath('../_stubs/scripts/LfiProtectionCheck.phtml'); + $test = $this->stack->resolve('../_stubs/scripts/LfiProtectionCheck.phtml'); $this->assertContains('LfiProtectionCheck.phtml', $test); } public function testRaisesExceptionWhenRetrievingScriptIfNoPathsRegistered() { $this->setExpectedException('Zend\View\Exception', 'unable to determine'); - $this->stack->getScriptPath('test.phtml'); + $this->stack->resolve('test.phtml'); } public function testRaisesExceptionWhenUnableToResolveScriptToPath() { $this->stack->addPath(__DIR__ . '/_templates'); $this->setExpectedException('Zend\View\Exception', 'not found'); - $this->stack->getScriptPath('bogus-script.txt'); + $this->stack->resolve('bogus-script.txt'); } public function testReturnsFullPathNameWhenAbleToResolveScriptPath() { $this->stack->addPath(__DIR__ . '/_templates'); $expected = realpath(__DIR__ . '/_templates/test.phtml'); - $test = $this->stack->getScriptPath('test.phtml'); + $test = $this->stack->resolve('test.phtml'); $this->assertEquals($expected, $test); } @@ -170,7 +170,7 @@ public function testReturnsPathWithStreamProtocolWhenStreamWrapperEnabled() $this->stack->setUseStreamWrapper(true) ->addPath(__DIR__ . '/_templates'); $expected = 'zend.view://' . realpath(__DIR__ . '/_templates/test.phtml'); - $test = $this->stack->getScriptPath('test.phtml'); + $test = $this->stack->resolve('test.phtml'); $this->assertEquals($expected, $test); } @@ -240,7 +240,7 @@ public function testAllowsRelativePharPath() { $path = 'phar://' . __DIR__ . '/_templates/view.phar/start/../views'; $this->stack->addPath($path); - $test = $this->stack->getScriptPath('foo/hello.phtml'); + $test = $this->stack->resolve('foo/hello.phtml'); $this->assertEquals($path . '/foo/hello.phtml', $test); } } diff --git a/test/ViewEventTest.php b/test/ViewEventTest.php index e07f9531..2598a5e3 100644 --- a/test/ViewEventTest.php +++ b/test/ViewEventTest.php @@ -6,7 +6,7 @@ Zend\Http\Request, Zend\Http\Response, Zend\View\Model\ViewModel, - Zend\View\PhpRenderer, + Zend\View\Renderer\PhpRenderer, Zend\View\ViewEvent; class ViewEventTest extends TestCase diff --git a/test/ViewTest.php b/test/ViewTest.php index 768b3c39..8616cf0b 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -28,7 +28,7 @@ Zend\Http\Response, Zend\View\Model, Zend\View\Model\ViewModel, - Zend\View\PhpRenderer, + Zend\View\Renderer\PhpRenderer, Zend\View\Renderer, Zend\View\Resolver, Zend\View\View; From 738b8e6f3287d880a2bcf1ce3fac48a13b4943a4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 08:40:50 -0600 Subject: [PATCH 30/69] Updated PhpRenderer to use getTemplate - Modified View Model handling to now use getTemplate() instead of a renderer option --- src/Renderer/PhpRenderer.php | 11 +++++------ test/PhpRendererTest.php | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php index 86d0ff1c..9e815107 100644 --- a/src/Renderer/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -428,16 +428,15 @@ public function getFilterChain() public function render($nameOrModel, $values = null) { if ($nameOrModel instanceof Model) { - $model = $nameOrModel; - $options = $model->getOptions(); - if (!isset($options['template']) || empty($options['template'])) { + $model = $nameOrModel; + $nameOrModel = $model->getTemplate(); + if (empty($nameOrModel)) { throw new Exception\DomainException(sprintf( - '%s: received View Model argument, but missing "template" option', + '%s: received View Model argument, but template is empty', __METHOD__ )); } - $nameOrModel = $options['template']; - unset($options['template']); + $options = $model->getOptions(); foreach ($options as $setting => $value) { $method = 'set' . $setting; if (method_exists($this, $method)) { diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index 4702945a..333614f4 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -310,7 +310,7 @@ public function testCanRenderViewModel() $this->renderer->setResolver($resolver); $model = new ViewModel(); - $model->setOption('template', 'empty'); + $model->setTemplate('empty'); $content = $this->renderer->render($model); $this->assertRegexp('/\s*Empty view\s*/s', $content); @@ -337,7 +337,7 @@ public function testRendersViewModelWithVariablesSpecified() $this->renderer->setResolver($resolver); $model = new ViewModel(); - $model->setOption('template', 'test'); + $model->setTemplate('test'); $model->setVariable('bar', 'bar'); $content = $this->renderer->render($model); From f9addbaa76856f838960b7cfc0a1374f58019075 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 08:45:01 -0600 Subject: [PATCH 31/69] Renamed stacking property and method - Renamed $extensions to $templates - Renamed enqueue() to addTemplate() --- src/Renderer/PhpRenderer.php | 10 +++++----- test/_templates/block.phtml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php index 9e815107..6ee7e227 100644 --- a/src/Renderer/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -53,7 +53,7 @@ class PhpRenderer implements Renderer, Pluggable * Queue of templates to render * @var array */ - private $extensions = array(); + private $templates = array(); /** * Template resolver @@ -451,7 +451,7 @@ public function render($nameOrModel, $values = null) } // find the script file name using the parent private method - $this->enqueue($nameOrModel); + $this->addTemplate($nameOrModel); unset($nameOrModel); // remove $name from local scope $this->varsCache[] = $this->vars(); @@ -470,7 +470,7 @@ public function render($nameOrModel, $values = null) extract($__vars); unset($__vars); // remove $__vars from local scope - while ($this->file = array_pop($this->extensions)) { + while ($this->file = array_pop($this->templates)) { $this->file = $this->resolver($this->file); ob_start(); include $this->file; @@ -488,9 +488,9 @@ public function render($nameOrModel, $values = null) * @param string $template * @return PhpRenderer */ - public function enqueue($template) + public function addTemplate($template) { - $this->extensions[] = $template; + $this->templates[] = $template; return $this; } diff --git a/test/_templates/block.phtml b/test/_templates/block.phtml index 983af791..8aec0cd0 100644 --- a/test/_templates/block.phtml +++ b/test/_templates/block.phtml @@ -1,5 +1,5 @@ enqueue('layout'); +$this->addTemplate('layout'); $this->placeholder('block')->captureStart(); ?> Block content From de4336e24d10ab19960645be052dfb31f7d705a3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 14:56:31 -0600 Subject: [PATCH 32/69] Removed renderer aggregation from View; obsolete --- src/View.php | 50 -------------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/src/View.php b/src/View.php index d2d0af14..45c4851e 100644 --- a/src/View.php +++ b/src/View.php @@ -38,11 +38,6 @@ class View */ protected $events; - /** - * @var Renderer[] - */ - protected $renderers = array(); - /** * @var Request */ @@ -97,51 +92,6 @@ public function getResponse() return $this->response; } - - /** - * Add a renderer - * - * @param Renderer $renderer - * @return View - */ - public function addRenderer(Renderer $renderer) - { - $class = get_class($renderer); - if (array_key_exists($class, $this->renderers)) { - throw new Exception\RuntimeException(sprintf( - 'Unable to add renderer of type "%s"; another renderer of that type is already attached', - $class - )); - } - $this->renderers[$class] = $renderer; - return $this; - } - - /** - * Does a renderer for the given class exist? - * - * @param string $class - * @return bool - */ - public function hasRenderer($class) - { - return array_key_exists($class, $this->renderers); - } - - /** - * Retrieve renderer by class - * - * @param string $class - * @return false|Renderer - */ - public function getRenderer($class) - { - if (!$this->hasRenderer($class)) { - return false; - } - return $this->renderers[$class]; - } - /** * Set the event manager instance * From 3920c4168347d7ea6aed9a5751faf3e9c9a74b5f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 14:57:25 -0600 Subject: [PATCH 33/69] Explicit check for renderer after triggering renderer event - What you did --- src/View.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View.php b/src/View.php index 45c4851e..22591a8c 100644 --- a/src/View.php +++ b/src/View.php @@ -190,7 +190,7 @@ public function render(Model $model) return ($result instanceof Renderer); }); $renderer = $results->last(); - if (!$renderer) { + if (!$renderer instanceof Renderer) { throw new RuntimeException(sprintf( '%s: no renderer selected!', __METHOD__ From a461aff2669940bd896d9b17ca6f20d770d2c72c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 12 Feb 2012 19:24:28 -0600 Subject: [PATCH 34/69] Fixes to make view layer work in functional testing - MVC event must be assigned to a local property; otherwise, base view model does not persist - DefaultRenderingStrategy needs to pull the view model from the event, not the result - TemplatePathStack now checks for a file extension; if none found, appends ".phtml" --- src/Resolver/TemplatePathStack.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php index 61bc18bd..d20115e0 100644 --- a/src/Resolver/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -270,6 +270,11 @@ public function resolve($name, Renderer $renderer = null) ); } + // Ensure we have a file extension + if (pathinfo($name, \PATHINFO_EXTENSION) == '') { + $name .= '.phtml'; + } + $paths = PATH_SEPARATOR; foreach ($this->paths as $path) { $file = new SplFileInfo($path . $name); From 8a9fee0d8207adf3c59463add74fd6c5d677b076 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 13 Feb 2012 21:41:18 -0600 Subject: [PATCH 35/69] No need to fully qualify constants --- src/Resolver/TemplatePathStack.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php index d20115e0..83ef1f33 100644 --- a/src/Resolver/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -271,7 +271,7 @@ public function resolve($name, Renderer $renderer = null) } // Ensure we have a file extension - if (pathinfo($name, \PATHINFO_EXTENSION) == '') { + if (pathinfo($name, PATHINFO_EXTENSION) == '') { $name .= '.phtml'; } From ed92ae2647d5fab161960c2c9f534794d6b38c46 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 13 Feb 2012 21:53:11 -0600 Subject: [PATCH 36/69] Add default suffix property to TemplatePathStack - Instead of hard-coding .phtml as expected suffix, allow setting it as an option - Add configuration to Bootstrap to simplify passing default suffix and path properties to TemplatePathStack --- src/Resolver/TemplatePathStack.php | 39 +++++++++++++++++++++++++++--- test/TemplatePathStackTest.php | 17 +++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php index 83ef1f33..c213d879 100644 --- a/src/Resolver/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -38,6 +38,15 @@ */ class TemplatePathStack implements Resolver { + /** + * Default suffix to use + * + * Appends this suffix if the template requested does not use it. + * + * @var string + */ + protected $defaultSuffix = 'phtml'; + /** * @var SplStack */ @@ -111,6 +120,29 @@ public function setOptions($options) } } + /** + * Set default file suffix + * + * @param string $defaultSuffix + * @return TemplatePathStack + */ + public function setDefaultSuffix($defaultSuffix) + { + $this->defaultSuffix = (string) $defaultSuffix; + $this->defaultSuffix = ltrim($this->defaultSuffix, '.'); + return $this; + } + + /** + * Get default file suffix + * + * @return string + */ + public function getDefaultSuffix() + { + return $this->defaultSuffix; + } + /** * Add many paths to the stack at once * @@ -270,9 +302,10 @@ public function resolve($name, Renderer $renderer = null) ); } - // Ensure we have a file extension - if (pathinfo($name, PATHINFO_EXTENSION) == '') { - $name .= '.phtml'; + // Ensure we have the expected file extension + $defaultSuffix = $this->getDefaultSuffix(); + if (pathinfo($name, PATHINFO_EXTENSION) != $defaultSuffix) {; + $name .= '.' . $defaultSuffix; } $paths = PATH_SEPARATOR; diff --git a/test/TemplatePathStackTest.php b/test/TemplatePathStackTest.php index 1ceed6dd..d0198c7f 100644 --- a/test/TemplatePathStackTest.php +++ b/test/TemplatePathStackTest.php @@ -243,4 +243,21 @@ public function testAllowsRelativePharPath() $test = $this->stack->resolve('foo/hello.phtml'); $this->assertEquals($path . '/foo/hello.phtml', $test); } + + public function testDefaultFileSuffixIsPhtml() + { + $this->assertEquals('phtml', $this->stack->getDefaultSuffix()); + } + + public function testDefaultFileSuffixIsMutable() + { + $this->stack->setDefaultSuffix('php'); + $this->assertEquals('php', $this->stack->getDefaultSuffix()); + } + + public function testSettingDefaultSuffixStripsLeadingDot() + { + $this->stack->setDefaultSuffix('.config.php'); + $this->assertEquals('config.php', $this->stack->getDefaultSuffix()); + } } From f74063ad75f190a18423d3eb5b5ee8d893aac37b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 14 Feb 2012 15:14:57 -0600 Subject: [PATCH 37/69] Add root/current view model awareness to PhpRenderer - New view helper, ViewModel, for composing the root and current view model - Added getRenderer() method to PhpRendererStrategy - Modified Bootstrap to inject ViewModel helper with base view model instance - Use cases: - Changing layout template - Injecting layout with variables from child views --- src/Helper/ViewModel.php | 112 +++++++++++++++++++++++++++ src/HelperLoader.php | 2 + src/Renderer/PhpRenderer.php | 4 + src/Strategy/PhpRendererStrategy.php | 10 +++ test/PhpRendererTest.php | 19 +++++ 5 files changed, 147 insertions(+) create mode 100644 src/Helper/ViewModel.php diff --git a/src/Helper/ViewModel.php b/src/Helper/ViewModel.php new file mode 100644 index 00000000..53dc842c --- /dev/null +++ b/src/Helper/ViewModel.php @@ -0,0 +1,112 @@ +root; + } + + /** + * Is a root view model composed? + * + * @return bool + */ + public function hasRoot() + { + return ($this->root instanceof Model); + } + + /** + * Get the current view model + * + * @return null|Model + */ + public function getCurrent() + { + return $this->current; + } + + /** + * Is a current view model composed? + * + * @return bool + */ + public function hasCurrent() + { + return ($this->current instanceof Model); + } + + /** + * Set the root view model + * + * @param Model $model + * @return ViewModel + */ + public function setRoot(Model $model) + { + $this->root = $model; + return $this; + } + + /** + * Set the current view model + * + * @param Model $model + * @return ViewModel + */ + public function setCurrent(Model $model) + { + $this->current = $model; + return $this; + } +} diff --git a/src/HelperLoader.php b/src/HelperLoader.php index 74152e09..6fb08200 100644 --- a/src/HelperLoader.php +++ b/src/HelperLoader.php @@ -86,5 +86,7 @@ class HelperLoader extends PluginClassLoader 'serverurl' => 'Zend\View\Helper\ServerUrl', 'translator' => 'Zend\View\Helper\Translator', 'url' => 'Zend\View\Helper\Url', + 'viewmodel' => 'Zend\View\Helper\ViewModel', + 'view_model' => 'Zend\View\Helper\ViewModel', ); } diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php index 6ee7e227..330e3ae8 100644 --- a/src/Renderer/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -446,6 +446,10 @@ public function render($nameOrModel, $values = null) } unset($options); + // Give view model awareness via ViewModel helper + $helper = $this->plugin('view_model'); + $helper->setCurrent($model); + $values = $model->getVariables(); unset($model); } diff --git a/src/Strategy/PhpRendererStrategy.php b/src/Strategy/PhpRendererStrategy.php index 4b89b2bb..0c957d5a 100644 --- a/src/Strategy/PhpRendererStrategy.php +++ b/src/Strategy/PhpRendererStrategy.php @@ -66,6 +66,16 @@ public function __construct(PhpRenderer $renderer) $this->renderer = $renderer; } + /** + * Retrieve the composed renderer + * + * @return PhpRenderer + */ + public function getRenderer() + { + return $this->renderer; + } + /** * Set list of possible content placeholders * diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index 333614f4..bf140385 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -343,4 +343,23 @@ public function testRendersViewModelWithVariablesSpecified() $content = $this->renderer->render($model); $this->assertRegexp('/\s*foo bar baz\s*/s', $content); } + + /** + * @group view-model + */ + public function testRenderedViewModelIsRegisteredAsCurrentViewModel() + { + $resolver = new TemplateMapResolver(array( + 'empty' => __DIR__ . '/_templates/empty.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setTemplate('empty'); + + $content = $this->renderer->render($model); + $helper = $this->renderer->plugin('view_model'); + $this->assertTrue($helper->hasCurrent()); + $this->assertSame($model, $helper->getCurrent()); + } } From d784120de059705e65926536c5098ae6b14b67e8 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 14 Feb 2012 16:47:01 -0600 Subject: [PATCH 38/69] [zendframework/zf2#1] Created AggregateResolver - Allows aggregating several resolvers in order to provide multiple strategies to resolving templates --- src/Resolver/AggregateResolver.php | 142 +++++++++++++++++++++++ test/Resolver/AggregateResolverTest.php | 147 ++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 src/Resolver/AggregateResolver.php create mode 100644 test/Resolver/AggregateResolverTest.php diff --git a/src/Resolver/AggregateResolver.php b/src/Resolver/AggregateResolver.php new file mode 100644 index 00000000..7175bec1 --- /dev/null +++ b/src/Resolver/AggregateResolver.php @@ -0,0 +1,142 @@ +queue = new PriorityQueue(); + } + + /** + * Return count of attached resolvers + * + * @return void + */ + public function count() + { + return $this->queue->count(); + } + + /** + * IteratorAggregate: return internal iterator + * + * @return Traversable + */ + public function getIterator() + { + return $this->queue; + } + + /** + * Attach a resolver + * + * @param Resolver $resolver + * @param int $priority + * @return AggregateResolver + */ + public function attach(Resolver $resolver, $priority = 1) + { + $this->queue->insert($resolver, $priority); + return $this; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return string + * @throws Exception\RuntimeException when no resolver succeeds + */ + public function resolve($name, Renderer $renderer = null) + { + $this->lastSuccessfulResolver = null; + + if (0 === count($this->queue)) { + throw new Exception\RuntimeException('No resolvers attached to AggregateResolver'); + } + + foreach ($this->queue as $resolver) { + try { + $resource = $resolver->resolve($name, $renderer); + if (!$resource) { + // No resource found; try next resolver + continue; + } + + // Resource found; return it + $this->lastSuccessfulResolver = $resolver; + return $resource; + } catch (\Exception $e) { + // non-resolution is normal in an aggregate + } + } + + throw new Exception\RuntimeException(sprintf( + 'Script "%s" not resolved by any attached resolvers', + $name + )); + } + + /** + * Return the last successful resolver, if any + * + * @return Resolver + */ + public function getLastSuccessfulResolver() + { + return $this->lastSuccessfulResolver; + } +} diff --git a/test/Resolver/AggregateResolverTest.php b/test/Resolver/AggregateResolverTest.php new file mode 100644 index 00000000..3eec19d0 --- /dev/null +++ b/test/Resolver/AggregateResolverTest.php @@ -0,0 +1,147 @@ +assertEquals(0, count($resolver)); + } + + public function testCanAttachResolvers() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver); + $this->assertEquals(1, count($resolver)); + $resolver->attach(new Resolver\TemplateMapResolver); + $this->assertEquals(2, count($resolver)); + } + + public function testReturnsNonFalseValueWhenAtLeastOneResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + ))); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + ))); + $test = $resolver->resolve('bar'); + $this->assertEquals('baz', $test); + } + + public function testLastSuccessfulResolverIsNullInitially() + { + $resolver = new Resolver\AggregateResolver(); + $this->assertNull($resolver->getLastSuccessfulResolver()); + } + + public function testCanAccessResolverThatLastSucceeded() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + )); + $barResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + )); + $bazResolver = new Resolver\TemplateMapResolver(array( + 'baz' => 'bat', + )); + $resolver->attach($fooResolver) + ->attach($barResolver) + ->attach($bazResolver); + + $test = $resolver->resolve('bar'); + $this->assertEquals('baz', $test); + $this->assertSame($barResolver, $resolver->getLastSuccessfulResolver()); + } + + public function testRaisesExceptionWhenNoResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + ))); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'not resolved'); + $test = $resolver->resolve('bar'); + } + + public function testLastSuccessfulResolverIsNullWhenNoResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + )); + $resolver->attach($fooResolver); + $test = $resolver->resolve('foo'); + $this->assertSame($fooResolver, $resolver->getLastSuccessfulResolver()); + + try { + $test = $resolver->resolve('bar'); + $this->fail('Should not have resolved!'); + } catch (\Exception $e) { + // exception is expected + } + $this->assertNull($resolver->getLastSuccessfulResolver()); + } + + public function testResolvesInOrderOfPriorityProvided() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'foo', + )); + $barResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'bar', + )); + $bazResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + )); + $resolver->attach($fooResolver, -1) + ->attach($barResolver, 100) + ->attach($bazResolver); + + $test = $resolver->resolve('bar'); + $this->assertEquals('bar', $test); + } + + public function testRaisesExceptionWhenAttemptingToResolveWhenNoResolversAreAttached() + { + $resolver = new Resolver\AggregateResolver(); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'attached'); + $resolver->resolve('foo'); + } +} From aec2eb1001c736cdf7731616194600005a5a9d6e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 15 Feb 2012 11:10:18 -0600 Subject: [PATCH 39/69] [zendframework/zf2#1] Resolvers should not raise exceptions - Resolvers no longer raise exceptions; instead, they return false - Some resolvers now store the last "error" type, to allow introspecting to determine cause - PhpRenderer now raises an exception if a boolean false is returned by the resolver --- src/Renderer/PhpRenderer.php | 18 +++++++- src/Resolver/AggregateResolver.php | 52 ++++++++++++++--------- src/Resolver/TemplateMapResolver.php | 8 +--- src/Resolver/TemplatePathStack.php | 35 +++++++++++---- test/PhpRendererTest.php | 8 ++++ test/Resolver/AggregateResolverTest.php | 12 +++--- test/Resolver/TemplateMapResolverTest.php | 10 ++--- test/TemplatePathStackTest.php | 12 +++--- 8 files changed, 101 insertions(+), 54 deletions(-) diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php index 330e3ae8..8981493b 100644 --- a/src/Renderer/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -49,6 +49,13 @@ class PhpRenderer implements Renderer, Pluggable */ private $content = ''; + /** + * Template being rendered + * + * @var null|string + */ + private $template = null; + /** * Queue of templates to render * @var array @@ -474,8 +481,15 @@ public function render($nameOrModel, $values = null) extract($__vars); unset($__vars); // remove $__vars from local scope - while ($this->file = array_pop($this->templates)) { - $this->file = $this->resolver($this->file); + while ($this->template = array_pop($this->templates)) { + $this->file = $this->resolver($this->template); + if (!$this->file) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to render template "%s"; resolver could not resolve to a file', + __METHOD__, + $this->template + )); + } ob_start(); include $this->file; $this->content = ob_get_clean(); diff --git a/src/Resolver/AggregateResolver.php b/src/Resolver/AggregateResolver.php index 7175bec1..71bbaa14 100644 --- a/src/Resolver/AggregateResolver.php +++ b/src/Resolver/AggregateResolver.php @@ -37,6 +37,15 @@ */ class AggregateResolver implements Countable, IteratorAggregate, Resolver { + const FAILURE_NO_RESOLVERS = 'AggregateResolver_Failure_No_Resolvers'; + const FAILURE_NOT_FOUND = 'AggregateResolver_Failure_Not_Found'; + + /** + * Last lookup failure + * @var false|string + */ + protected $lastLookupFailure = false; + /** * @var Resolver */ @@ -97,37 +106,32 @@ public function attach(Resolver $resolver, $priority = 1) * * @param string $name * @param null|Renderer $renderer - * @return string - * @throws Exception\RuntimeException when no resolver succeeds + * @return false|string */ public function resolve($name, Renderer $renderer = null) { + $this->lastLookupFailure = false; $this->lastSuccessfulResolver = null; if (0 === count($this->queue)) { - throw new Exception\RuntimeException('No resolvers attached to AggregateResolver'); + $this->lastLookupFailure = static::FAILURE_NO_RESOLVERS; + return false; } foreach ($this->queue as $resolver) { - try { - $resource = $resolver->resolve($name, $renderer); - if (!$resource) { - // No resource found; try next resolver - continue; - } - - // Resource found; return it - $this->lastSuccessfulResolver = $resolver; - return $resource; - } catch (\Exception $e) { - // non-resolution is normal in an aggregate + $resource = $resolver->resolve($name, $renderer); + if (!$resource) { + // No resource found; try next resolver + continue; } + + // Resource found; return it + $this->lastSuccessfulResolver = $resolver; + return $resource; } - throw new Exception\RuntimeException(sprintf( - 'Script "%s" not resolved by any attached resolvers', - $name - )); + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; } /** @@ -139,4 +143,14 @@ public function getLastSuccessfulResolver() { return $this->lastSuccessfulResolver; } + + /** + * Get last lookup failure + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; + } } diff --git a/src/Resolver/TemplateMapResolver.php b/src/Resolver/TemplateMapResolver.php index a80defcb..b7798d30 100644 --- a/src/Resolver/TemplateMapResolver.php +++ b/src/Resolver/TemplateMapResolver.php @@ -164,17 +164,13 @@ public function has($name) * Retrieve a template path by name * * @param string $name - * @return string + * @return false|string * @throws Exception\DomainException if no entry exists */ public function get($name) { if (!$this->has($name)) { - throw new Exception\DomainException(sprintf( - '%s: no template by name of "%s" found', - __METHOD__, - (string) $name - )); + return false; } return $this->map[$name]; } diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php index c213d879..bc932bfa 100644 --- a/src/Resolver/TemplatePathStack.php +++ b/src/Resolver/TemplatePathStack.php @@ -38,6 +38,9 @@ */ class TemplatePathStack implements Resolver { + const FAILURE_NO_PATHS = 'TemplatePathStack_Failure_No_Paths'; + const FAILURE_NOT_FOUND = 'TemplatePathStack_Failure_Not_Found'; + /** * Default suffix to use * @@ -52,6 +55,13 @@ class TemplatePathStack implements Resolver */ protected $paths; + /** + * Reason for last lookup failure + * + * @var false|string + */ + protected $lastLookupFailure = false; + /** * Flag indicating whether or not LFI protection for rendering view scripts is enabled * @var bool @@ -290,6 +300,8 @@ public function useStreamWrapper() */ public function resolve($name, Renderer $renderer = null) { + $this->lastLookupFailure = false; + if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { throw new Exception\DomainException( 'Requested scripts may not include parent directory traversal ("../", "..\\" notation)' @@ -297,9 +309,8 @@ public function resolve($name, Renderer $renderer = null) } if (!count($this->paths)) { - throw new Exception\RuntimeException( - 'No view script directory set; unable to determine location for view script' - ); + $this->lastLookupFailure = static::FAILURE_NO_PATHS; + return false; } // Ensure we have the expected file extension @@ -308,7 +319,6 @@ public function resolve($name, Renderer $renderer = null) $name .= '.' . $defaultSuffix; } - $paths = PATH_SEPARATOR; foreach ($this->paths as $path) { $file = new SplFileInfo($path . $name); if ($file->isReadable()) { @@ -326,12 +336,19 @@ public function resolve($name, Renderer $renderer = null) } return $filePath; } - $paths .= $path . PATH_SEPARATOR; } - throw new Exception\RuntimeException(sprintf( - 'Script "%s" not found in path (%s)', - $name, trim($paths, PATH_SEPARATOR) - )); + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; + } + + /** + * Get the last lookup failure message, if any + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; } } diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index bf140385..e0d082a7 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -362,4 +362,12 @@ public function testRenderedViewModelIsRegisteredAsCurrentViewModel() $this->assertTrue($helper->hasCurrent()); $this->assertSame($model, $helper->getCurrent()); } + + public function testRendererRaisesExceptionIfResolverCannotResolveTemplate() + { + $expected = '10 > 9'; + $this->renderer->vars()->assign(array('foo' => '10 > 9')); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'could not resolve'); + $test = $this->renderer->render('should-not-find-this'); + } } diff --git a/test/Resolver/AggregateResolverTest.php b/test/Resolver/AggregateResolverTest.php index 3eec19d0..4f3d661a 100644 --- a/test/Resolver/AggregateResolverTest.php +++ b/test/Resolver/AggregateResolverTest.php @@ -89,14 +89,14 @@ public function testCanAccessResolverThatLastSucceeded() $this->assertSame($barResolver, $resolver->getLastSuccessfulResolver()); } - public function testRaisesExceptionWhenNoResolverSucceeds() + public function testReturnsFalseWhenNoResolverSucceeds() { $resolver = new Resolver\AggregateResolver(); $resolver->attach(new Resolver\TemplateMapResolver(array( 'foo' => 'bar', ))); - $this->setExpectedException('Zend\View\Exception\RuntimeException', 'not resolved'); - $test = $resolver->resolve('bar'); + $this->assertFalse($resolver->resolve('bar')); + $this->assertEquals(Resolver\AggregateResolver::FAILURE_NOT_FOUND, $resolver->getLastLookupFailure()); } public function testLastSuccessfulResolverIsNullWhenNoResolverSucceeds() @@ -138,10 +138,10 @@ public function testResolvesInOrderOfPriorityProvided() $this->assertEquals('bar', $test); } - public function testRaisesExceptionWhenAttemptingToResolveWhenNoResolversAreAttached() + public function testReturnsFalseWhenAttemptingToResolveWhenNoResolversAreAttached() { $resolver = new Resolver\AggregateResolver(); - $this->setExpectedException('Zend\View\Exception\RuntimeException', 'attached'); - $resolver->resolve('foo'); + $this->assertFalse($resolver->resolve('foo')); + $this->assertEquals(Resolver\AggregateResolver::FAILURE_NO_RESOLVERS, $resolver->getLastLookupFailure()); } } diff --git a/test/Resolver/TemplateMapResolverTest.php b/test/Resolver/TemplateMapResolverTest.php index 1c72254e..02dc9c97 100644 --- a/test/Resolver/TemplateMapResolverTest.php +++ b/test/Resolver/TemplateMapResolverTest.php @@ -193,12 +193,11 @@ public function testGetReturnsPathWhenNameHasMatch() $this->assertEquals($map['foo/bar'], $resolver->get('foo/bar')); } - public function testGetRaisesExceptionWhenNameHasNoMatch() + public function testGetReturnsFalseWhenNameHasNoMatch() { $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); $resolver = new TemplateMapResolver($map); - $this->setExpectedException('Zend\View\Exception\DomainException', 'no template'); - $resolver->get('bar/baz'); + $this->assertFalse($resolver->get('bar/baz')); } public function testResolveReturnsPathWhenNameHasMatch() @@ -208,11 +207,10 @@ public function testResolveReturnsPathWhenNameHasMatch() $this->assertEquals($map['foo/bar'], $resolver->resolve('foo/bar')); } - public function testResolveRaisesExceptionWhenNameHasNoMatch() + public function testResolveReturnsFalseWhenNameHasNoMatch() { $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); $resolver = new TemplateMapResolver($map); - $this->setExpectedException('Zend\View\Exception\DomainException', 'no template'); - $resolver->resolve('bar/baz'); + $this->assertFalse($resolver->resolve('bar/baz')); } } diff --git a/test/TemplatePathStackTest.php b/test/TemplatePathStackTest.php index d0198c7f..b879481b 100644 --- a/test/TemplatePathStackTest.php +++ b/test/TemplatePathStackTest.php @@ -140,17 +140,17 @@ public function testDisablingLfiProtectionAllowsParentDirectoryTraversal() $this->assertContains('LfiProtectionCheck.phtml', $test); } - public function testRaisesExceptionWhenRetrievingScriptIfNoPathsRegistered() + public function testReturnsFalseWhenRetrievingScriptIfNoPathsRegistered() { - $this->setExpectedException('Zend\View\Exception', 'unable to determine'); - $this->stack->resolve('test.phtml'); + $this->assertFalse($this->stack->resolve('test.phtml')); + $this->assertEquals(TemplatePathStack::FAILURE_NO_PATHS, $this->stack->getLastLookupFailure()); } - public function testRaisesExceptionWhenUnableToResolveScriptToPath() + public function testReturnsFalseWhenUnableToResolveScriptToPath() { $this->stack->addPath(__DIR__ . '/_templates'); - $this->setExpectedException('Zend\View\Exception', 'not found'); - $this->stack->resolve('bogus-script.txt'); + $this->assertFalse($this->stack->resolve('bogus-script.txt')); + $this->assertEquals(TemplatePathStack::FAILURE_NOT_FOUND, $this->stack->getLastLookupFailure()); } public function testReturnsFullPathNameWhenAbleToResolveScriptPath() From 10fd8fb8aad3117efd8c89564fe6a18c09bc2e61 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 15 Feb 2012 13:03:56 -0600 Subject: [PATCH 40/69] [zendframework/zf2#1] Added ability to recurse child models in JsonRenderer - Tests for all ViewModel behaviors now implemented - Have marked tests incomplete for non-ViewModel rendering behaviors --- src/Renderer/JsonRenderer.php | 69 ++++++++++++++- test/Renderer/JsonRendererTest.php | 138 +++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 test/Renderer/JsonRendererTest.php diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index 06a915bd..fe416199 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -41,6 +41,17 @@ */ class JsonRenderer implements Renderer { + /** + * Whether or not to merge child models with no capture-to value set + * @var bool + */ + protected $mergeUnnamedChildren = false; + + /** + * @var Resolver + */ + protected $resolver; + /** * Return the template engine object, if any * @@ -67,6 +78,28 @@ public function setResolver(Resolver $resolver) $this->resolver = $resolver; } + /** + * Set flag indicating whether or not to merge unnamed children + * + * @param bool $mergeUnnamedChildren + * @return JsonRenderer + */ + public function setMergeUnnamedChildren($mergeUnnamedChildren) + { + $this->mergeUnnamedChildren = (bool) $mergeUnnamedChildren; + return $this; + } + + /** + * Should we merge unnamed children? + * + * @return bool + */ + public function mergeUnnamedChildren() + { + return $this->mergeUnnamedChildren; + } + /** * Renders values as JSON * @@ -83,7 +116,7 @@ public function render($nameOrModel, $values = null) if ($nameOrModel instanceof Model\JsonModel) { $values = $nameOrModel->serialize(); } else { - $values = $nameOrModel->getVariables(); + $values = $this->recurseModel($nameOrModel); $values = json_encode($values); } @@ -115,5 +148,37 @@ public function render($nameOrModel, $values = null) __METHOD__ )); } -} + /** + * Retrieve values from a model and recurse its children to build a data structure + * + * @param Model $model + * @return array + */ + protected function recurseModel(Model $model) + { + $values = $model->getVariables(); + if (!$model->hasChildren()) { + return $values; + } + + $mergeChildren = $this->mergeUnnamedChildren(); + foreach ($model as $child) { + $captureTo = $child->captureTo(); + if (!$captureTo && !$mergeChildren) { + // We don't want to do anything with this child + continue; + } + + $childValues = $this->recurseModel($child); + if ($captureTo) { + // Capturing to a specific key + $values[$captureTo] = $childValues; + } elseif ($mergeChildren) { + // Merging values with parent + $values = array_replace_recursive($values, $childValues); + } + } + return $values; + } +} diff --git a/test/Renderer/JsonRendererTest.php b/test/Renderer/JsonRendererTest.php new file mode 100644 index 00000000..71ce8910 --- /dev/null +++ b/test/Renderer/JsonRendererTest.php @@ -0,0 +1,138 @@ +renderer = new JsonRenderer(); + } + + public function testRendersViewModelsWithoutChildren() + { + $model = new ViewModel(array('foo' => 'bar')); + $test = $this->renderer->render($model); + $this->assertEquals(json_encode(array('foo' => 'bar')), $test); + } + + public function testRendersViewModelsWithChildrenUsingCaptureToValue() + { + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'bar')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'bar', + 'child1' => array( + 'foo' => 'bar', + ), + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function testThrowsAwayChildModelsWithoutCaptureToValueByDefault() + { + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'baz')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo(false); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'bar', + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function testCanMergeChildModelsWithoutCaptureToValues() + { + $this->renderer->setMergeUnnamedChildren(true); + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'baz')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo(false); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'baz', + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function testRendersNonObjectModelAsJson() + { + $this->markTestIncomplete(); + } + + public function testRendersJsonSerializableModelsAsJson() + { + $this->markTestIncomplete(); + } + + public function testRendersTraversableObjectsAsJsonObjects() + { + $this->markTestIncomplete(); + } + + public function testRendersNonTraversableNonJsonSerializableObjectsAsJsonObjects() + { + $this->markTestIncomplete(); + } + + public function testNonViewModelInitialArgumentWithValuesRaisesException() + { + $this->markTestIncomplete(); + } +} From 3c5dd736c42a22f20a611e24b16e9700bdb40b78 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 16 Feb 2012 14:14:06 -0600 Subject: [PATCH 41/69] [zendframework/zf2#1] Completed testing JsonRenderer --- src/Renderer/JsonRenderer.php | 6 +--- test/Renderer/JsonRendererTest.php | 51 +++++++++++++++++++++++---- test/Renderer/TestAsset/JsonModel.php | 41 +++++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 test/Renderer/TestAsset/JsonModel.php diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index fe416199..74366475 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -126,14 +126,10 @@ public function render($nameOrModel, $values = null) // use case 2: $nameOrModel is populated, $values is not // Serialize $nameOrModel if (null === $values) { - if (!is_object($nameOrModel)) { + if (!is_object($nameOrModel) || $nameOrModel instanceof JsonSerializable) { return json_encode($nameOrModel); } - if ($nameOrModel instanceof JsonSerializable) { - return $nameOrModel->jsonSerialize(); - } - if ($nameOrModel instanceof Traversable) { $nameOrModel = IteratorToArray::convert($nameOrModel); return json_encode($nameOrModel); diff --git a/test/Renderer/JsonRendererTest.php b/test/Renderer/JsonRendererTest.php index 71ce8910..b6fa4fc7 100644 --- a/test/Renderer/JsonRendererTest.php +++ b/test/Renderer/JsonRendererTest.php @@ -21,7 +21,9 @@ namespace ZendTest\View\Renderer; -use PHPUnit_Framework_TestCase as TestCase, +use ArrayObject, + PHPUnit_Framework_TestCase as TestCase, + stdClass, Zend\View\Renderer\JsonRenderer, Zend\View\Model\ViewModel; @@ -111,28 +113,63 @@ public function testCanMergeChildModelsWithoutCaptureToValues() $this->assertEquals(json_encode($expected), $test); } - public function testRendersNonObjectModelAsJson() + public function getNonObjectModels() { - $this->markTestIncomplete(); + return array( + array('string'), + array(1), + array(1.0), + array(array('foo', 'bar')), + array(array('foo' => 'bar')), + ); + } + + /** + * @dataProvider getNonObjectModels + */ + public function testRendersNonObjectModelAsJson($model) + { + $expected = json_encode($model); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); } public function testRendersJsonSerializableModelsAsJson() { - $this->markTestIncomplete(); + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $this->markTestSkipped('Can only test JsonSerializable models in PHP 5.4.0 and up'); + } + $model = new TestAsset\JsonModel; + $model->value = array('foo' => 'bar'); + $expected = json_encode($model->value); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); } public function testRendersTraversableObjectsAsJsonObjects() { - $this->markTestIncomplete(); + $model = new ArrayObject(array( + 'foo' => 'bar', + 'bar' => 'baz', + )); + $expected = json_encode($model->getArrayCopy()); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); } public function testRendersNonTraversableNonJsonSerializableObjectsAsJsonObjects() { - $this->markTestIncomplete(); + $model = new stdClass; + $model->foo = 'bar'; + $model->bar = 'baz'; + $expected = json_encode(get_object_vars($model)); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); } public function testNonViewModelInitialArgumentWithValuesRaisesException() { - $this->markTestIncomplete(); + $this->setExpectedException('Zend\View\Exception\DomainException'); + $this->renderer->render('foo', array('bar' => 'baz')); } } diff --git a/test/Renderer/TestAsset/JsonModel.php b/test/Renderer/TestAsset/JsonModel.php new file mode 100644 index 00000000..4a1461af --- /dev/null +++ b/test/Renderer/TestAsset/JsonModel.php @@ -0,0 +1,41 @@ + Date: Thu, 16 Feb 2012 14:17:35 -0600 Subject: [PATCH 42/69] Removed erroneous file - Removed "FeedRender.php", a misspelling of "FeedRenderer.php" --- src/Renderer/FeedRender.php | 153 ------------------------------------ 1 file changed, 153 deletions(-) delete mode 100644 src/Renderer/FeedRender.php diff --git a/src/Renderer/FeedRender.php b/src/Renderer/FeedRender.php deleted file mode 100644 index e5c9d5d8..00000000 --- a/src/Renderer/FeedRender.php +++ /dev/null @@ -1,153 +0,0 @@ -resolver = $resolver; - } - - /** - * Renders values as JSON - * - * @todo Determine what use case exists for accepting only $nameOrModel - * @param string|Model $name The script/resource process, or a view model - * @param null|array|\ArrayAccess Values to use during rendering - * @return string The script output. - */ - public function render($nameOrModel, $values = null) - { - if ($nameOrModel instanceof Model) { - // Use case 1: View Model provided - // Non-FeedModel: cast to FeedModel - if (!$nameOrModel instanceof Model\FeedModel) { - $vars = $nameOrModel->getVariables(); - $options = $nameOrModel->getOptions(); - $type = $this->getFeedType(); - if (isset($options['feed_type'])) { - $type = $options['feed_type']; - } else { - $this->setFeedType($type); - } - $nameOrModel = new Model\FeedModel($vars, $type); - } - } elseif (is_string($nameOrModel)) { - // Use case 2: string $nameOrModel + array|Traversable|Feed $values - $nameOrModel = new Model\FeedModel($values, $nameOrModel); - } else { - // Use case 3: failure - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a ViewModel or a string feed type as the first argument; received "%s"', - (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) - )); - } - - // Get feed and type - $feed = $nameOrModel->getFeed(); - $type = $nameOrModel->getFeedType(); - if (!$type) { - $type = $this->getFeedType(); - } else { - $this->setFeedType($type); - } - - // Render feed - return $feed->export($type); - } - - /** - * Set feed type ('rss' or 'atom') - * - * @param string $feedType - * @return FeedRenderer - */ - public function setFeedType($feedType) - { - $feedType = strtolower($feedType); - if (!in_array($feedType, array('rss', 'atom'))) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a string of either "rss" or "atom"', - __METHOD__ - )); - } - - $this->feedType = $feedType; - return $this; - } - - /** - * Get feed type - * - * @return string - */ - public function getFeedType() - { - return $this->feedType; - } -} From 86dbba523307607761074a7d7eb7b13adb0b80ca Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 16 Feb 2012 15:10:02 -0600 Subject: [PATCH 43/69] [zendframework/zf2#1] tree rendering capabilities - Added TreeRendererInterface; adds method "canRenderTrees" for querying tree rendering capabilities of a renderer - JsonRenderer implements, and always returns true - PhpRenderer implements, and has mutable (false by default) - View now queries whether Renderer is a TreeRenderer, and value of canRenderTrees(), before calling renderChildren(). Also, moved renderChildren() call to after retrieving the Renderer. - Added renderChildModel() view helper; renders the named child model, if it exists. --- src/Helper/RenderChildModel.php | 133 ++++++++++++++++++ src/HelperLoader.php | 2 + src/Renderer/JsonRenderer.php | 16 ++- src/Renderer/PhpRenderer.php | 34 ++++- src/Renderer/TreeRendererInterface.php | 39 +++++ src/View.php | 18 ++- test/PhpRendererTest.php | 13 ++ test/Renderer/JsonRendererTest.php | 5 + test/ViewTest.php | 61 ++++++++ .../nested-view-model-content.phtml | 1 + .../_templates/nested-view-model-layout.phtml | 6 + 11 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 src/Helper/RenderChildModel.php create mode 100644 src/Renderer/TreeRendererInterface.php create mode 100644 test/_templates/nested-view-model-content.phtml create mode 100644 test/_templates/nested-view-model-layout.phtml diff --git a/src/Helper/RenderChildModel.php b/src/Helper/RenderChildModel.php new file mode 100644 index 00000000..461feefd --- /dev/null +++ b/src/Helper/RenderChildModel.php @@ -0,0 +1,133 @@ +render($child); + } + + /** + * Render a model + * + * If a matching child model is found, it is rendered. If not, an empty + * string is returned. + * + * @param string $child + * @return string + */ + public function render($child) + { + $model = $this->findChild($child); + if (!$model) { + return ''; + } + + $view = $this->getView(); + return $view->render($model); + } + + /** + * Find the named child model + * + * Iterates through the current view model, looking for a child model that + * has a captureTo value matching the requested $child. If found, that child + * model is returned; otherwise, a boolean false is returned. + * + * @param string $child + * @return false|Model + */ + protected function findChild($child) + { + $model = $this->getCurrent(); + foreach ($model->getChildren() as $childModel) { + if ($childModel->captureTo() == $child) { + return $childModel; + } + } + return false; + } + + /** + * Get the current view model + * + * @return null|Model + */ + protected function getCurrent() + { + $helper = $this->getViewModelHelper(); + if (!$helper->hasCurrent()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered in renderer; cannot query for children', + __METHOD__ + )); + } + return $helper->getCurrent(); + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if ($this->viewModelHelper) { + return $this->viewModelHelper; + } + $view = $this->getView(); + $this->viewModelHelper = $view->plugin('view_model'); + return $this->viewModelHelper; + } +} diff --git a/src/HelperLoader.php b/src/HelperLoader.php index 6fb08200..8b6c7f6b 100644 --- a/src/HelperLoader.php +++ b/src/HelperLoader.php @@ -82,6 +82,8 @@ class HelperLoader extends PluginClassLoader 'partialloop' => 'Zend\View\Helper\PartialLoop', 'partial' => 'Zend\View\Helper\Partial', 'placeholder' => 'Zend\View\Helper\Placeholder', + 'renderchildmodel' => 'Zend\View\Helper\RenderChildModel', + 'render_child_model' => 'Zend\View\Helper\RenderChildModel', 'rendertoplaceholder' => 'Zend\View\Helper\RenderToPlaceholder', 'serverurl' => 'Zend\View\Helper\ServerUrl', 'translator' => 'Zend\View\Helper\Translator', diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index 74366475..8f7c8dae 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -30,7 +30,7 @@ Zend\View\Resolver; /** - * Interface class for Zend_View compatible template engine implementations + * JSON renderer * * @todo Should this use Zend\Json? * @category Zend @@ -39,7 +39,7 @@ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class JsonRenderer implements Renderer +class JsonRenderer implements Renderer, TreeRendererInterface { /** * Whether or not to merge child models with no capture-to value set @@ -145,6 +145,18 @@ public function render($nameOrModel, $values = null) )); } + /** + * Can this renderer render trees of view models? + * + * Yes. + * + * @return true + */ + public function canRenderTrees() + { + return true; + } + /** * Retrieve values from a model and recurse its children to build a data structure * diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php index 8981493b..3ae6d216 100644 --- a/src/Renderer/PhpRenderer.php +++ b/src/Renderer/PhpRenderer.php @@ -42,13 +42,18 @@ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class PhpRenderer implements Renderer, Pluggable +class PhpRenderer implements Renderer, Pluggable, TreeRendererInterface { /** * @var string Rendered content */ private $content = ''; + /** + * @var bool Whether or not to render trees of view models + */ + private $renderTrees = false; + /** * Template being rendered * @@ -500,6 +505,33 @@ public function render($nameOrModel, $values = null) return $this->getFilterChain()->filter($this->content); // filter output } + /** + * Set flag indicating whether or not we should render trees of view models + * + * If set to true, the View instance will not attempt to render children + * separately, but instead pass the root view model directly to the PhpRenderer. + * It is then up to the developer to render the children from within the + * view script. + * + * @param bool $renderTrees + * @return PhpRenderer + */ + public function setCanRenderTrees($renderTrees) + { + $this->renderTrees = (bool) $renderTrees; + return $this; + } + + /** + * Can we render trees, or are we configured to do so? + * + * @return bool + */ + public function canRenderTrees() + { + return $this->renderTrees; + } + /** * Add a template to the stack * diff --git a/src/Renderer/TreeRendererInterface.php b/src/Renderer/TreeRendererInterface.php new file mode 100644 index 00000000..56d5214e --- /dev/null +++ b/src/Renderer/TreeRendererInterface.php @@ -0,0 +1,39 @@ +hasChildren()) { - $this->renderChildren($model); - } - $event = $this->getEvent(); $event->setModel($model); $events = $this->events(); @@ -196,6 +191,19 @@ public function render(Model $model) __METHOD__ )); } + + // If we have children, render them first, but only if: + // a) the renderer does not implement TreeRendererInterface, or + // b) it does, but canRenderTrees() returns false + if ($model->hasChildren() + && (!$renderer instanceof Renderer\TreeRendererInterface + || !$renderer->canRenderTrees()) + ) { + $this->renderChildren($model); + } + + // Reset the model, in case it has changed, and set the renderer + $event->setModel($model); $event->setRenderer($renderer); $rendered = $renderer->render($model); diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index e0d082a7..e44ded79 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -370,4 +370,17 @@ public function testRendererRaisesExceptionIfResolverCannotResolveTemplate() $this->setExpectedException('Zend\View\Exception\RuntimeException', 'could not resolve'); $test = $this->renderer->render('should-not-find-this'); } + + public function testDoesNotRenderTreesOfViewModelsByDefault() + { + $this->assertFalse($this->renderer->canRenderTrees()); + } + + public function testRenderTreesOfViewModelsCapabilityIsMutable() + { + $this->renderer->setCanRenderTrees(true); + $this->assertTrue($this->renderer->canRenderTrees()); + $this->renderer->setCanRenderTrees(false); + $this->assertFalse($this->renderer->canRenderTrees()); + } } diff --git a/test/Renderer/JsonRendererTest.php b/test/Renderer/JsonRendererTest.php index b6fa4fc7..0abf4061 100644 --- a/test/Renderer/JsonRendererTest.php +++ b/test/Renderer/JsonRendererTest.php @@ -172,4 +172,9 @@ public function testNonViewModelInitialArgumentWithValuesRaisesException() $this->setExpectedException('Zend\View\Exception\DomainException'); $this->renderer->render('foo', array('bar' => 'baz')); } + + public function testRendersTreesOfViewModelsByDefault() + { + $this->assertTrue($this->renderer->canRenderTrees()); + } } diff --git a/test/ViewTest.php b/test/ViewTest.php index 8616cf0b..502bc387 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -237,4 +237,65 @@ public function testResponseStrategyIsNotTriggeredForChildModel() $this->assertEquals(1, count($result)); } + + public function testUsesTreeRendererInterfaceToDetermineWhetherOrNotToPassOnlyRootViewModelToPhpRenderer() + { + $resolver = new Resolver\TemplateMapResolver(array( + 'layout' => __DIR__ . '/_templates/nested-view-model-layout.phtml', + 'content' => __DIR__ . '/_templates/nested-view-model-content.phtml', + )); + $phpRenderer = new PhpRenderer(); + $phpRenderer->setCanRenderTrees(true); + $phpRenderer->setResolver($resolver); + + $this->view->addRenderingStrategy(function ($e) use ($phpRenderer) { + return $phpRenderer; + }); + + $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $layout = new ViewModel(); + $layout->setTemplate('layout'); + $content = new ViewModel(); + $content->setTemplate('content'); + $content->setCaptureTo('content'); + $layout->addChild($content); + + $this->view->render($layout); + + $this->assertContains('Layout start', $result->content); + $this->assertContains('Content for layout', $result->content, $result->content); + $this->assertContains('Layout end', $result->content); + } + + public function testUsesTreeRendererInterfaceToDetermineWhetherOrNotToPassOnlyRootViewModelToJsonRenderer() + { + $jsonRenderer = new Renderer\JsonRenderer(); + + $this->view->addRenderingStrategy(function ($e) use ($jsonRenderer) { + return $jsonRenderer; + }); + + $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $layout = new ViewModel(array('status' => 200)); + $content = new ViewModel(array('foo' => 'bar')); + $content->setCaptureTo('response'); + $layout->addChild($content); + + $this->view->render($layout); + + $expected = json_encode(array( + 'status' => 200, + 'response' => array('foo' => 'bar'), + )); + + $this->assertEquals($expected, $result->content); + } } diff --git a/test/_templates/nested-view-model-content.phtml b/test/_templates/nested-view-model-content.phtml new file mode 100644 index 00000000..a54c478b --- /dev/null +++ b/test/_templates/nested-view-model-content.phtml @@ -0,0 +1 @@ +Content for layout diff --git a/test/_templates/nested-view-model-layout.phtml b/test/_templates/nested-view-model-layout.phtml new file mode 100644 index 00000000..a5319ee7 --- /dev/null +++ b/test/_templates/nested-view-model-layout.phtml @@ -0,0 +1,6 @@ +Layout start + +renderChildModel('content'); ?> + + +Layout end From 840655bcf03c886cc5b07fade1f890f483565fa1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 16 Feb 2012 16:30:57 -0600 Subject: [PATCH 44/69] [zendframework/zf2#1] ensure renderChildModel helper works with nested/sequential calls - Added tests and code to ensure renderChildModel helper will work with sequential calls to the helper, as well as with nested models. --- src/Helper/RenderChildModel.php | 14 +- test/Helper/RenderChildModelTest.php | 131 ++++++++++++++++++ .../_templates/nested-view-model-child2.phtml | 2 + 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/Helper/RenderChildModelTest.php create mode 100644 test/_templates/nested-view-model-child2.phtml diff --git a/src/Helper/RenderChildModel.php b/src/Helper/RenderChildModel.php index 461feefd..0944b1b6 100644 --- a/src/Helper/RenderChildModel.php +++ b/src/Helper/RenderChildModel.php @@ -40,6 +40,11 @@ */ class RenderChildModel extends AbstractHelper { + /** + * @var Model Current view model + */ + protected $current; + /** * @var ViewModel */ @@ -74,8 +79,11 @@ public function render($child) return ''; } - $view = $this->getView(); - return $view->render($model); + $view = $this->getView(); + $return = $view->render($model); + $helper = $this->getViewModelHelper(); + $helper->setCurrent($this->current); + return $return; } /** @@ -90,7 +98,7 @@ public function render($child) */ protected function findChild($child) { - $model = $this->getCurrent(); + $this->current = $model = $this->getCurrent(); foreach ($model->getChildren() as $childModel) { if ($childModel->captureTo() == $child) { return $childModel; diff --git a/test/Helper/RenderChildModelTest.php b/test/Helper/RenderChildModelTest.php new file mode 100644 index 00000000..6ada34a3 --- /dev/null +++ b/test/Helper/RenderChildModelTest.php @@ -0,0 +1,131 @@ +resolver = new TemplateMapResolver(array( + 'layout' => __DIR__ . '/../_templates/nested-view-model-layout.phtml', + 'child1' => __DIR__ . '/../_templates/nested-view-model-content.phtml', + 'child2' => __DIR__ . '/../_templates/nested-view-model-child2.phtml', + )); + $this->renderer = $renderer = new PhpRenderer(); + $renderer->setCanRenderTrees(true); + $renderer->setResolver($this->resolver); + + $this->viewModelHelper = $renderer->plugin('view_model'); + $this->helper = $renderer->plugin('render_child_model'); + + $this->parent = new ViewModel(); + $this->parent->setTemplate('layout'); + $this->viewModelHelper->setRoot($this->parent); + $this->viewModelHelper->setCurrent($this->parent); + } + + public function testRendersEmptyStringWhenUnableToResolveChildModel() + { + $result = $this->helper->render('child1'); + $this->assertSame('', $result); + } + + public function setupFirstChild() + { + $child1 = new ViewModel(); + $child1->setTemplate('child1'); + $child1->setCaptureTo('child1'); + $this->parent->addChild($child1); + return $child1; + } + + public function testRendersChildTemplateWhenAbleToResolveChildModelByCaptureToValue() + { + $this->setupFirstChild(); + $result = $this->helper->render('child1'); + $this->assertContains('Content for layout', $result, $result); + } + + public function setupSecondChild() + { + $child2 = new ViewModel(); + $child2->setTemplate('child2'); + $child2->setCaptureTo('child2'); + $this->parent->addChild($child2); + return $child2; + } + + + public function testRendersSiblingChildrenWhenCalledInSequence() + { + $this->setupFirstChild(); + $this->setupSecondChild(); + $result = $this->helper->render('child1'); + $this->assertContains('Content for layout', $result, $result); + $result = $this->helper->render('child2'); + $this->assertContains('Second child', $result, $result); + } + + public function testRendersNestedChildren() + { + $child1 = $this->setupFirstChild(); + $child1->setTemplate('layout'); + $child2 = new ViewModel(); + $child2->setTemplate('child1'); + $child2->setCaptureTo('content'); + $child1->addChild($child2); + + $result = $this->helper->render('child1'); + $this->assertContains('Layout start', $result, $result); + $this->assertContains('Content for layout', $result, $result); + $this->assertContains('Layout end', $result, $result); + } + + /** + * @outputBuffering enabled + */ + public function testAttemptingToRenderWithNoCurrentModelRaisesException() + { + $renderer = new PhpRenderer(); + $renderer->setResolver($this->resolver); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'no view model'); + $renderer->render('layout'); + } +} diff --git a/test/_templates/nested-view-model-child2.phtml b/test/_templates/nested-view-model-child2.phtml new file mode 100644 index 00000000..d800c35c --- /dev/null +++ b/test/_templates/nested-view-model-child2.phtml @@ -0,0 +1,2 @@ +Second child + From 0cac657aeb58a8a110037e272db3343c6a0070f5 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 16 Feb 2012 17:01:57 -0600 Subject: [PATCH 45/69] [zendframework/zf2#1] Ensured nested + sequential children render correctly - Scenario was: - child "content" with children, followed by: - child "sidebar" with no children --- src/Helper/RenderChildModel.php | 9 ++++--- test/Helper/RenderChildModelTest.php | 26 ++++++++++++++++--- .../nested-view-model-complexlayout.phtml | 5 ++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 test/_templates/nested-view-model-complexlayout.phtml diff --git a/src/Helper/RenderChildModel.php b/src/Helper/RenderChildModel.php index 0944b1b6..68612a84 100644 --- a/src/Helper/RenderChildModel.php +++ b/src/Helper/RenderChildModel.php @@ -79,10 +79,11 @@ public function render($child) return ''; } - $view = $this->getView(); - $return = $view->render($model); - $helper = $this->getViewModelHelper(); - $helper->setCurrent($this->current); + $current = $this->current; + $view = $this->getView(); + $return = $view->render($model); + $helper = $this->getViewModelHelper(); + $helper->setCurrent($current); return $return; } diff --git a/test/Helper/RenderChildModelTest.php b/test/Helper/RenderChildModelTest.php index 6ada34a3..710d7462 100644 --- a/test/Helper/RenderChildModelTest.php +++ b/test/Helper/RenderChildModelTest.php @@ -44,9 +44,10 @@ class RenderChildModelTest extends TestCase public function setUp() { $this->resolver = new TemplateMapResolver(array( - 'layout' => __DIR__ . '/../_templates/nested-view-model-layout.phtml', - 'child1' => __DIR__ . '/../_templates/nested-view-model-content.phtml', - 'child2' => __DIR__ . '/../_templates/nested-view-model-child2.phtml', + 'layout' => __DIR__ . '/../_templates/nested-view-model-layout.phtml', + 'child1' => __DIR__ . '/../_templates/nested-view-model-content.phtml', + 'child2' => __DIR__ . '/../_templates/nested-view-model-child2.phtml', + 'complex' => __DIR__ . '/../_templates/nested-view-model-complexlayout.phtml', )); $this->renderer = $renderer = new PhpRenderer(); $renderer->setCanRenderTrees(true); @@ -118,6 +119,25 @@ public function testRendersNestedChildren() $this->assertContains('Layout end', $result, $result); } + public function testRendersSequentialChildrenWithNestedChildren() + { + $this->parent->setTemplate('complex'); + $child1 = $this->setupFirstChild(); + $child1->setTemplate('layout'); + $child1->setCaptureTo('content'); + + $child2 = $this->setupSecondChild(); + $child2->setCaptureTo('sidebar'); + + $nested = new ViewModel(); + $nested->setTemplate('child1'); + $nested->setCaptureTo('content'); + $child1->addChild($nested); + + $result = $this->renderer->render($this->parent); + $this->assertRegExp('/Content:\s+Layout start\s+Content for layout\s+Layout end\s+Sidebar:\s+Second child/s', $result, $result); + } + /** * @outputBuffering enabled */ diff --git a/test/_templates/nested-view-model-complexlayout.phtml b/test/_templates/nested-view-model-complexlayout.phtml new file mode 100644 index 00000000..35c46851 --- /dev/null +++ b/test/_templates/nested-view-model-complexlayout.phtml @@ -0,0 +1,5 @@ +Content: +renderChildModel('content') ?> + +Sidebar: +renderChildModel('sidebar') ?> From a05ece7f412fea1db66a198be07c6bcf3dbbf23c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2012 11:46:02 -0600 Subject: [PATCH 46/69] [zendframework/zf2#1] tested FeedRenderer - Tested all intended behaviors of FeedRenderer --- src/Renderer/FeedRenderer.php | 5 +- test/Renderer/FeedRendererTest.php | 139 +++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 test/Renderer/FeedRendererTest.php diff --git a/src/Renderer/FeedRenderer.php b/src/Renderer/FeedRenderer.php index e5c9d5d8..c106e7dc 100644 --- a/src/Renderer/FeedRenderer.php +++ b/src/Renderer/FeedRenderer.php @@ -95,15 +95,16 @@ public function render($nameOrModel, $values = null) } else { $this->setFeedType($type); } - $nameOrModel = new Model\FeedModel($vars, $type); + $nameOrModel = new Model\FeedModel($vars, array('feed_type' => $type)); } } elseif (is_string($nameOrModel)) { // Use case 2: string $nameOrModel + array|Traversable|Feed $values - $nameOrModel = new Model\FeedModel($values, $nameOrModel); + $nameOrModel = new Model\FeedModel($values, (array) $nameOrModel); } else { // Use case 3: failure throw new Exception\InvalidArgumentException(sprintf( '%s expects a ViewModel or a string feed type as the first argument; received "%s"', + __METHOD__, (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) )); } diff --git a/test/Renderer/FeedRendererTest.php b/test/Renderer/FeedRendererTest.php new file mode 100644 index 00000000..03afcb61 --- /dev/null +++ b/test/Renderer/FeedRendererTest.php @@ -0,0 +1,139 @@ +renderer = new FeedRenderer(); + } + + protected function getFeedData($type) + { + return array( + 'copyright' => date('Y'), + 'date_created' => time(), + 'date_modified' => time(), + 'last_build_date' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/', + 'language' => 'en_US', + 'feed_link' => array( + 'link' => 'http://framework.zend.com/feed.xml', + 'type' => $type, + ), + 'link' => 'http://framework.zend.com/feed.xml', + 'title' => 'Testing', + 'encoding' => 'UTF-8', + 'base_url' => 'http://framework.zend.com/', + 'entries' => array( + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/1', + 'link' => 'http://framework.zend.com/1', + 'title' => 'Test 1', + ), + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/2', + 'link' => 'http://framework.zend.com/2', + 'title' => 'Test 2', + ), + ), + ); + } + + public function testRendersFeedModelAccordingToTypeProvidedInModel() + { + $model = new Model\FeedModel($this->getFeedData('atom')); + $model->setOption('feed_type', 'atom'); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testRendersFeedModelAccordingToRenderTypeIfNoTypeProvidedInModel() + { + $this->renderer->setFeedType('atom'); + $model = new Model\FeedModel($this->getFeedData('atom')); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testCastsViewModelToFeedModelUsingFeedTypeOptionProvided() + { + $model = new Model\ViewModel($this->getFeedData('atom')); + $model->setOption('feed_type', 'atom'); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testCastsViewModelToFeedModelUsingRendererFeedTypeIfNoFeedTypeOptionInModel() + { + $this->renderer->setFeedType('atom'); + $model = new Model\ViewModel($this->getFeedData('atom')); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testStringModelWithValuesProvidedCastsToFeed() + { + $this->renderer->setFeedType('atom'); + $xml = $this->renderer->render('layout', $this->getFeedData('atom')); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testNonStringNonModelArgumentRaisesException() + { + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects'); + $this->renderer->render(array('foo')); + } + + public function testSettingUnacceptableFeedTypeRaisesException() + { + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects a string of either "rss" or "atom"'); + $this->renderer->setFeedType('foobar'); + } +} From bc883ec7a0a07add623db1664b5f2a8a4f0c349f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2012 11:58:01 -0600 Subject: [PATCH 47/69] [zendframework/zf2#1] completed FeedStrategy tests - Tested non-selection of renderer - Fixed an issue found -- test for Attach header before introspecting it - Tested attach/detach --- src/Strategy/FeedStrategy.php | 24 ++++++++------- test/Strategy/FeedStrategyTest.php | 48 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/Strategy/FeedStrategy.php b/src/Strategy/FeedStrategy.php index fc1c3c3b..3ceb96b8 100644 --- a/src/Strategy/FeedStrategy.php +++ b/src/Strategy/FeedStrategy.php @@ -110,17 +110,19 @@ public function selectRenderer(ViewEvent $e) } $headers = $request->headers(); - $accept = $headers->get('Accept'); - foreach ($accept->getPrioritized() as $mediaType) { - if (0 === strpos($mediaType, 'application/rss+xml')) { - // application/rss+xml Accept header found - $this->renderer->setFeedType('rss'); - return $this->renderer; - } - if (0 === strpos($mediaType, 'application/atom+xml')) { - // application/atom+xml Accept header found - $this->renderer->setFeedType('atom'); - return $this->renderer; + if ($headers->has('accept')) { + $accept = $headers->get('accept'); + foreach ($accept->getPrioritized() as $mediaType) { + if (0 === strpos($mediaType, 'application/rss+xml')) { + // application/rss+xml Accept header found + $this->renderer->setFeedType('rss'); + return $this->renderer; + } + if (0 === strpos($mediaType, 'application/atom+xml')) { + // application/atom+xml Accept header found + $this->renderer->setFeedType('atom'); + return $this->renderer; + } } } diff --git a/test/Strategy/FeedStrategyTest.php b/test/Strategy/FeedStrategyTest.php index 51076771..1383fffe 100644 --- a/test/Strategy/FeedStrategyTest.php +++ b/test/Strategy/FeedStrategyTest.php @@ -22,6 +22,7 @@ namespace ZendTest\View\Strategy; use PHPUnit_Framework_TestCase as TestCase, + Zend\EventManager\EventManager, Zend\Feed\Writer\FeedFactory, Zend\Http\Request as HttpRequest, Zend\Http\Response as HttpResponse, @@ -200,4 +201,51 @@ public function testResponseContentTypeIsBasedOnFeedType() $this->assertTrue($headers->has('content-type')); $this->assertEquals('application/rss+xml', $headers->get('content-type')->getFieldValue()); } + + public function testReturnsNullWhenUnableToSelectRenderer() + { + $model = new Model\ViewModel(); + $request = new HttpRequest(); + $this->event->setModel($model); + $this->event->setRequest($request); + $this->assertNull($this->strategy->selectRenderer($this->event)); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } } From 3deb7ff775cde958798945222c14d702d9a365f5 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2012 12:02:43 -0600 Subject: [PATCH 48/69] [zendframework/zf2#1] completed JsonStrategy tests - Tested behavior when unable to select renderer - Fixed issue; do not introspect Accept header of not present - Added tests for attach/detach --- src/Strategy/JsonStrategy.php | 12 +++++--- test/Strategy/JsonStrategyTest.php | 49 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/Strategy/JsonStrategy.php b/src/Strategy/JsonStrategy.php index fa566952..aa10d596 100644 --- a/src/Strategy/JsonStrategy.php +++ b/src/Strategy/JsonStrategy.php @@ -109,11 +109,13 @@ public function selectRenderer(ViewEvent $e) } $headers = $request->headers(); - $accept = $headers->get('Accept'); - foreach ($accept->getPrioritized() as $mediaType) { - if (0 === strpos($mediaType, 'application/json')) { - // application/json Accept header found - return $this->renderer; + if ($headers->has('accept')) { + $accept = $headers->get('Accept'); + foreach ($accept->getPrioritized() as $mediaType) { + if (0 === strpos($mediaType, 'application/json')) { + // application/json Accept header found + return $this->renderer; + } } } diff --git a/test/Strategy/JsonStrategyTest.php b/test/Strategy/JsonStrategyTest.php index 1b1908fe..a4c890b8 100644 --- a/test/Strategy/JsonStrategyTest.php +++ b/test/Strategy/JsonStrategyTest.php @@ -22,6 +22,7 @@ namespace ZendTest\View\Strategy; use PHPUnit_Framework_TestCase as TestCase, + Zend\EventManager\EventManager, Zend\Http\Request as HttpRequest, Zend\Http\Response as HttpResponse, Zend\View\Model, @@ -116,4 +117,52 @@ public function testMatchingRendererAndStringResultInjectsResponse() $this->assertTrue($headers->has('content-type')); $this->assertEquals('application/json', $headers->get('content-type')->getFieldValue()); } + + public function testReturnsNullWhenCannotSelectRenderer() + { + $model = new Model\ViewModel(); + $request = new HttpRequest(); + $this->event->setModel($model); + $this->event->setRequest($request); + + $this->assertNull($this->strategy->selectRenderer($this->event)); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } } From 7ee174676da81a9cd1f5f3dbb961b4870de5515f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2012 12:22:01 -0600 Subject: [PATCH 49/69] [zendframework/zf2#1] completed testing PhpRendererStrategy - Added tests for content placeholder mutators - Added tests for attach/detach --- test/Strategy/PhpRendererStrategyTest.php | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/Strategy/PhpRendererStrategyTest.php b/test/Strategy/PhpRendererStrategyTest.php index ff2cd76c..54632a59 100644 --- a/test/Strategy/PhpRendererStrategyTest.php +++ b/test/Strategy/PhpRendererStrategyTest.php @@ -22,6 +22,7 @@ namespace ZendTest\View\Strategy; use PHPUnit_Framework_TestCase as TestCase, + Zend\EventManager\EventManager, Zend\Http\Request as HttpRequest, Zend\Http\Response as HttpResponse, Zend\Registry, @@ -120,4 +121,53 @@ public function testResponseContentSetToResultIfNotEmpty() $content = $this->response->getContent(); $this->assertEquals('Result Content', $content); } + + public function testContentPlaceholdersIncludeContentAndArticleByDefault() + { + $this->assertEquals(array('article', 'content'), $this->strategy->getContentPlaceholders()); + } + + public function testContentPlaceholdersListIsMutable() + { + $this->strategy->setContentPlaceholders(array('foo', 'bar')); + $this->assertEquals(array('foo', 'bar'), $this->strategy->getContentPlaceholders()); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } } From 0ce10bc4d582c45804876f28410414a4a7547f07 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 17 Feb 2012 12:35:39 -0600 Subject: [PATCH 50/69] [zendframework/zf2#1] altered Layout view helper - Now proxies to ViewModel to set layout on root view model --- src/Helper/Layout.php | 72 +++++++++++++++++++++++++++----------- test/Helper/LayoutTest.php | 43 ++++++++++++++--------- 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/Helper/Layout.php b/src/Helper/Layout.php index a242e59b..4a44d999 100644 --- a/src/Helper/Layout.php +++ b/src/Helper/Layout.php @@ -24,13 +24,12 @@ */ namespace Zend\View\Helper; -use Zend\Layout\Layout as BaseLayout; +use Zend\View\Exception, + Zend\View\Model; /** * View helper for retrieving layout object * - * @uses \Zend\Layout\Layout - * @uses \Zend\View\Helper\AbstractHelper * @package Zend_View * @subpackage Helper * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) @@ -38,42 +37,75 @@ */ class Layout extends AbstractHelper { - /** @var BaseLayout */ - protected $_layout; + /** + * @var ViewModel + */ + protected $viewModelHelper; /** - * Get layout object + * Get layout template * - * @return BaseLayout + * @return string */ public function getLayout() { - if (null === $this->_layout) { - $this->_layout = new BaseLayout(); - } - - return $this->_layout; + $model = $this->getRoot(); + return $model->getTemplate(); } /** - * Set layout object + * Set layout template * - * @param BaseLayout $layout + * @param string $template * @return Layout */ - public function setLayout(BaseLayout $layout) + public function setLayout($template) { - $this->_layout = $layout; + $model = $this->getRoot(); + $model->setTemplate((string) $template); return $this; } /** - * Return layout object + * Set layout template * - * @return BaseLayout + * @param string $template + * @return Layout */ - public function __invoke() + public function __invoke($template) { - return $this->getLayout(); + return $this->setLayout($template); + } + + /** + * Get the root view model + * + * @return null|Model + */ + protected function getRoot() + { + $helper = $this->getViewModelHelper(); + if (!$helper->hasRoot()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered as root in renderer', + __METHOD__ + )); + } + return $helper->getRoot(); + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if ($this->viewModelHelper) { + return $this->viewModelHelper; + } + $view = $this->getView(); + $this->viewModelHelper = $view->plugin('view_model'); + return $this->viewModelHelper; } } diff --git a/test/Helper/LayoutTest.php b/test/Helper/LayoutTest.php index 8394fe22..ed36907b 100644 --- a/test/Helper/LayoutTest.php +++ b/test/Helper/LayoutTest.php @@ -24,8 +24,9 @@ */ namespace ZendTest\View\Helper; -use Zend\View\Helper, - Zend\Layout\Layout; +use Zend\View\Helper\Layout, + Zend\View\Model\ViewModel, + Zend\View\Renderer\PhpRenderer; /** * Test class for Zend_View_Helper_Layout @@ -49,30 +50,40 @@ class LayoutTest extends \PHPUnit_Framework_TestCase */ public function setUp() { + $this->renderer = $renderer = new PhpRenderer(); + $this->viewModelHelper = $renderer->plugin('view_model'); + $this->helper = $renderer->plugin('layout'); + + $this->parent = new ViewModel(); + $this->parent->setTemplate('layout'); + $this->viewModelHelper->setRoot($this->parent); } - public function testGetLayoutCreatesLayoutObjectWhenNoPluginRegistered() + public function testCallingSetLayoutAltersRootModelTemplate() { - $helper = new Helper\Layout(); - $layout = $helper->getLayout(); - $this->assertTrue($layout instanceof Layout); + $this->helper->setLayout('alternate/layout'); + $this->assertEquals('alternate/layout', $this->parent->getTemplate()); } - public function testSetLayoutReplacesExistingLayoutObject() + public function testCallingGetLayoutReturnsRootModelTemplate() { - $layout = new Layout; - $helper = new Helper\Layout(); - $this->assertNotSame($layout, $helper->getLayout()); + $this->assertEquals('layout', $this->helper->getLayout()); + } - $helper->setLayout($layout); - $this->assertSame($layout, $helper->getLayout()); + public function testCallingInvokeProxiesToSetLayout() + { + $helper = $this->helper; + $helper('alternate/layout'); + $this->assertEquals('alternate/layout', $this->parent->getTemplate()); } - public function testHelperMethodFetchesLayoutObject() + public function testRaisesExceptionIfViewModelHelperHasNoRoot() { - $helper = new Helper\Layout(); + $renderer = new PhpRenderer(); + $viewModelHelper = $renderer->plugin('view_model'); + $helper = $renderer->plugin('layout'); - $received = $helper->__invoke(); - $this->assertTrue($received instanceof Layout); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'view model'); + $helper->setLayout('foo/bar'); } } From 29f7b7e9c9de42ac66bcede1cfb9b3c1b079f473 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 19 Feb 2012 16:10:17 -0600 Subject: [PATCH 51/69] [zendframework/zf2#1] Remove Zend\Layout-awareness from Json helper - With Zend\Layout removed, JSON view helper should not worry about layouts --- src/Helper/Json.php | 48 +++++----------------------------------- test/Helper/JsonTest.php | 20 +---------------- 2 files changed, 6 insertions(+), 62 deletions(-) diff --git a/src/Helper/Json.php b/src/Helper/Json.php index b0252bcc..cc9ca344 100644 --- a/src/Helper/Json.php +++ b/src/Helper/Json.php @@ -25,8 +25,7 @@ namespace Zend\View\Helper; use Zend\Http\Response, - Zend\Json\Json as JsonFormatter, - Zend\Layout\Layout as LayoutComponent; + Zend\Json\Json as JsonFormatter; /** * Helper for simplifying JSON responses @@ -38,28 +37,11 @@ */ class Json extends AbstractHelper { - /** - * @var LayoutComponent - */ - protected $layout; - /** * @var Response */ protected $response; - /** - * Set the layout object - * - * @param LayoutComponent $layout - * @return Json - */ - public function setLayout(LayoutComponent $layout) - { - $this->layout = $layout; - return $this; - } - /** * Set the response object * @@ -73,35 +55,15 @@ public function setResponse(Response $response) } /** - * Encode data as JSON, disable layouts, and set response header - * - * If $keepLayouts is true, does not disable layouts. + * Encode data as JSON and set response header * * @param mixed $data - * @param bool $keepLayouts - * NOTE: if boolean, establish $keepLayouts to true|false - * if array, admit params for Zend_Json::encode as enableJsonExprFinder=>true|false - * this array can contains a 'keepLayout'=>true|false - * that will not be passed to Zend_Json::encode method but will be used here + * @param array $jsonOptions Options to pass to JsonFormatter::encode() * @return string|void */ - public function __invoke($data, $keepLayouts = false) + public function __invoke($data, array $jsonOptions = array()) { - $options = array(); - if (is_array($keepLayouts)) - { - $options = $keepLayouts; - $keepLayouts = (array_key_exists('keepLayouts', $keepLayouts)) - ? $keepLayouts['keepLayouts'] - : false; - unset($options['keepLayouts']); - } - - $data = JsonFormatter::encode($data, null, $options); - - if (!$keepLayouts && ($this->layout instanceof LayoutComponent)) { - $this->layout->disableLayout(); - } + $data = JsonFormatter::encode($data, null, $jsonOptions); if ($this->response instanceof Response) { $headers = $this->response->headers(); diff --git a/test/Helper/JsonTest.php b/test/Helper/JsonTest.php index 865808b9..3c521cbf 100644 --- a/test/Helper/JsonTest.php +++ b/test/Helper/JsonTest.php @@ -27,7 +27,6 @@ use PHPUnit_Framework_TestCase as TestCase, Zend\Http\Response, Zend\Json\Json as JsonFormatter, - Zend\Layout\Layout, Zend\View\Helper\Json as JsonHelper; /** @@ -52,11 +51,9 @@ class JsonTest extends TestCase */ public function setUp() { - $this->layout = new Layout(); $this->response = new Response(); $this->helper = new JsonHelper(); - $this->helper->setLayout($this->layout) - ->setResponse($this->response); + $this->helper->setResponse($this->response); } public function verifyJsonHeader() @@ -79,19 +76,4 @@ public function testJsonHelperReturnsJsonEncodedString() $this->assertTrue(is_string($data)); $this->assertEquals('foobar', JsonFormatter::decode($data)); } - - public function testJsonHelperDisablesLayoutsByDefault() - { - $this->assertTrue($this->layout->isEnabled()); - $this->testJsonHelperReturnsJsonEncodedString(); - $this->assertFalse($this->layout->isEnabled()); - } - - public function testJsonHelperDoesNotDisableLayoutsWhenKeepLayoutFlagTrue() - { - $this->assertTrue($this->layout->isEnabled()); - - $data = $this->helper->__invoke(array('foobar'), true); - $this->assertTrue($this->layout->isEnabled()); - } } From 3578ee2bd619d32cc2127db5c3958b43eebad0fc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 19 Feb 2012 16:15:14 -0600 Subject: [PATCH 52/69] Fixed issue with PaginationControl test - Exception message had changed; updated --- test/Helper/PaginationControlTest.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/Helper/PaginationControlTest.php b/test/Helper/PaginationControlTest.php index b0e17ca8..ccbbeb4c 100644 --- a/test/Helper/PaginationControlTest.php +++ b/test/Helper/PaginationControlTest.php @@ -24,9 +24,10 @@ */ namespace ZendTest\View\Helper; -use Zend\View\Renderer\PhpRenderer as View, +use Zend\Paginator, Zend\View\Helper, - Zend\Paginator; + Zend\View\Renderer\PhpRenderer as View, + Zend\View\Resolver; /** * @category Zend @@ -54,8 +55,11 @@ class PaginationControlTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { + $resolver = new Resolver\TemplatePathStack(array('script_paths' => array( + __DIR__ . '/_files/scripts', + ))); $view = new View(); - $view->resolver()->addPath(__DIR__ . '/_files/scripts'); + $view->setResolver($resolver); Helper\PaginationControl::setDefaultViewPartial(null); $this->_viewHelper = new Helper\PaginationControl(); @@ -166,7 +170,7 @@ public function testAcceptsViewPartialInOtherModule() * make sure it gets to Zend_View_Helper_Partial and it's recognized * as a module. */ $this->assertInstanceOf('Zend\View\Exception\RuntimeException', $e); - $this->assertContains('Script "partial.phtml" not found in path', $e->getMessage()); + $this->assertContains('could not resolve', $e->getMessage()); } } From e0455c78b4c64ff5bb90d3d5b7cd747a1b0913f4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 20 Feb 2012 21:57:26 -0600 Subject: [PATCH 53/69] [zendframework/zf2#1] layout plugins should return root view model - When no arguments are provided to __invoke() the view and controller layout plugins should return the root/layout view model. --- src/Helper/Layout.php | 12 +++++++++--- test/Helper/LayoutTest.php | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Helper/Layout.php b/src/Helper/Layout.php index 4a44d999..559da40b 100644 --- a/src/Helper/Layout.php +++ b/src/Helper/Layout.php @@ -67,13 +67,19 @@ public function setLayout($template) } /** - * Set layout template + * Set layout template or retrieve "layout" view model * - * @param string $template + * If no arguments are given, grabs the "root" or "layout" view model. + * Otherwise, attempts to set the template for that view model. + * + * @param null|string $template * @return Layout */ - public function __invoke($template) + public function __invoke($template = null) { + if (null === $template) { + return $this->getRoot(); + } return $this->setLayout($template); } diff --git a/test/Helper/LayoutTest.php b/test/Helper/LayoutTest.php index ed36907b..4f99aa1c 100644 --- a/test/Helper/LayoutTest.php +++ b/test/Helper/LayoutTest.php @@ -77,6 +77,13 @@ public function testCallingInvokeProxiesToSetLayout() $this->assertEquals('alternate/layout', $this->parent->getTemplate()); } + public function testCallingInvokeWithNoArgumentReturnsViewModel() + { + $helper = $this->helper; + $result = $helper(); + $this->assertSame($this->parent, $result); + } + public function testRaisesExceptionIfViewModelHelperHasNoRoot() { $renderer = new PhpRenderer(); From 37769249f63c3b738450a5b26b8ffc96bd18d93f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 20 Feb 2012 22:10:34 -0600 Subject: [PATCH 54/69] [zendframework/zf2#1] addChild() should optionally allow specifying capture-to value - addChild() now accepts an optional second argument, the "capture to" value to set on the child. --- src/Model.php | 3 ++- src/Model/ViewModel.php | 6 +++++- test/Model/ViewModelTest.php | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Model.php b/src/Model.php index 9cc7a918..748c7d1c 100644 --- a/src/Model.php +++ b/src/Model.php @@ -107,9 +107,10 @@ public function getTemplate(); * Add a child model * * @param Model $child + * @param null|string $captureTo Optional; if specified, the "capture to" value to set on the child * @return Model */ - public function addChild(Model $child); + public function addChild(Model $child, $captureTo = null); /** * Return all children. diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index 9400407a..81ea52b1 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -219,11 +219,15 @@ public function getTemplate() * Add a child model * * @param Model $child + * @param null|string $captureTo Optional; if specified, the "capture to" value to set on the child * @return ViewModel */ - public function addChild(Model $child) + public function addChild(Model $child, $captureTo = null) { $this->children[] = $child; + if (null !== $captureTo) { + $child->setCaptureTo($captureTo); + } return $this; } diff --git a/test/Model/ViewModelTest.php b/test/Model/ViewModelTest.php index 772f2853..a55a0745 100644 --- a/test/Model/ViewModelTest.php +++ b/test/Model/ViewModelTest.php @@ -214,4 +214,13 @@ public function testTerminationFlagIsMutable() $model->setTerminal(true); $this->assertTrue($model->terminate()); } + + public function testAddChildAllowsSpecifyingCaptureToValue() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child, 'foo'); + $this->assertTrue($model->hasChildren()); + $this->assertEquals('foo', $child->captureTo()); + } } From 84ec7eb44495bc577f92f70ebfc8e4e002a9138a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 20 Feb 2012 22:27:50 -0600 Subject: [PATCH 55/69] [zendframework/zf2#1] ViewModel should use View\Variables internally by default - ViewModel now uses View\Variables internally by default - Added tests verifying PhpRenderer uses return of ViewModel::getVariables() verbatim when it is a Variables object --- src/Model/ViewModel.php | 11 ++++++--- test/Model/ViewModelTest.php | 13 +++++++++-- test/PhpRendererTest.php | 27 ++++++++++++++++++++++ test/_templates/view-model-variables.phtml | 1 + 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 test/_templates/view-model-variables.phtml diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index 81ea52b1..11a1ee32 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -26,7 +26,8 @@ Traversable, Zend\Stdlib\IteratorToArray, Zend\View\Exception, - Zend\View\Model; + Zend\View\Model, + Zend\View\Variables as ViewVariables; /** * @category Zend @@ -79,13 +80,17 @@ class ViewModel implements Model /** * Constructor * - * @param array|Traversable $variables + * @param null|array|Traversable $variables * @param array|Traversable $options * @return void */ - public function __construct($variables = array(), $options = array()) + public function __construct($variables = null, $options = array()) { + if (null === $variables) { + $variables = new ViewVariables(); + } $this->setVariables($variables); + $this->setOptions($options); } diff --git a/test/Model/ViewModelTest.php b/test/Model/ViewModelTest.php index a55a0745..8b076a4b 100644 --- a/test/Model/ViewModelTest.php +++ b/test/Model/ViewModelTest.php @@ -24,7 +24,8 @@ use ArrayObject, stdClass, PHPUnit_Framework_TestCase as TestCase, - Zend\View\Model\ViewModel; + Zend\View\Model\ViewModel, + Zend\View\Variables as ViewVariables; /** * @category Zend @@ -38,7 +39,7 @@ class ViewModelTest extends TestCase public function testAllowsEmptyConstructor() { $model = new ViewModel(); - $this->assertEquals(array(), $model->getVariables()); + $this->assertInstanceOf('Zend\View\Variables', $model->getVariables()); $this->assertEquals(array(), $model->getOptions()); } @@ -223,4 +224,12 @@ public function testAddChildAllowsSpecifyingCaptureToValue() $this->assertTrue($model->hasChildren()); $this->assertEquals('foo', $child->captureTo()); } + + public function testAllowsPassingViewVariablesContainerAsVariables() + { + $variables = new ViewVariables(); + $model = new ViewModel(); + $model->setVariables($variables); + $this->assertSame($variables, $model->getVariables()); + } } diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php index e44ded79..c9255d61 100644 --- a/test/PhpRendererTest.php +++ b/test/PhpRendererTest.php @@ -371,11 +371,17 @@ public function testRendererRaisesExceptionIfResolverCannotResolveTemplate() $test = $this->renderer->render('should-not-find-this'); } + /** + * @group view-model + */ public function testDoesNotRenderTreesOfViewModelsByDefault() { $this->assertFalse($this->renderer->canRenderTrees()); } + /** + * @group view-model + */ public function testRenderTreesOfViewModelsCapabilityIsMutable() { $this->renderer->setCanRenderTrees(true); @@ -383,4 +389,25 @@ public function testRenderTreesOfViewModelsCapabilityIsMutable() $this->renderer->setCanRenderTrees(false); $this->assertFalse($this->renderer->canRenderTrees()); } + + /** + * @group view-model + */ + public function testIfViewModelComposesVariablesInstanceThenRendererUsesIt() + { + $model = new ViewModel(); + $model->setTemplate('template'); + $vars = $model->getVariables(); + $vars->setEscapeCallback(function ($value) { + return strtolower($value); + }); + $vars['foo'] = 'BAR-BAZ-BAT'; + + $resolver = new TemplateMapResolver(array( + 'template' => __DIR__ . '/_templates/view-model-variables.phtml', + )); + $this->renderer->setResolver($resolver); + $test = $this->renderer->render($model); + $this->assertContains('bar-baz-bat', $test); + } } diff --git a/test/_templates/view-model-variables.phtml b/test/_templates/view-model-variables.phtml new file mode 100644 index 00000000..2659db44 --- /dev/null +++ b/test/_templates/view-model-variables.phtml @@ -0,0 +1 @@ +foo ?> From 7a8df9838bf9be3db1a99203d242e5dcc7d84dfc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 08:58:35 -0600 Subject: [PATCH 56/69] [zendframework/zf2#1] use Json::encode() instead of json_encode - Updated JsonRenderer to use Zend\Json\Json::encode() instead of json_encode() throughout --- src/Renderer/JsonRenderer.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index 8f7c8dae..fd43b543 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -23,6 +23,7 @@ use JsonSerializable, Traversable, + Zend\Json\Json, Zend\Stdlib\IteratorToArray, Zend\View\Exception, Zend\View\Model, @@ -32,7 +33,6 @@ /** * JSON renderer * - * @todo Should this use Zend\Json? * @category Zend * @package Zend_View * @subpackage Renderer @@ -117,7 +117,7 @@ public function render($nameOrModel, $values = null) $values = $nameOrModel->serialize(); } else { $values = $this->recurseModel($nameOrModel); - $values = json_encode($values); + $values = Json::encode($values); } return $values; @@ -127,15 +127,15 @@ public function render($nameOrModel, $values = null) // Serialize $nameOrModel if (null === $values) { if (!is_object($nameOrModel) || $nameOrModel instanceof JsonSerializable) { - return json_encode($nameOrModel); + return Json::encode($nameOrModel); } if ($nameOrModel instanceof Traversable) { $nameOrModel = IteratorToArray::convert($nameOrModel); - return json_encode($nameOrModel); + return Json::encode($nameOrModel); } - return json_encode(get_object_vars($nameOrModel)); + return Json::encode(get_object_vars($nameOrModel)); } // use case 3: Both $nameOrModel and $values are populated From e57736271ef6546cd9377b34bf2a1823f3432946 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 10:57:16 -0600 Subject: [PATCH 57/69] [zendframework/zf2#1] Address View test failures - Updated tests to work when ViewModel uses a Variables container internally --- test/ViewTest.php | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/ViewTest.php b/test/ViewTest.php index 502bc387..678d95bd 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -31,6 +31,7 @@ Zend\View\Renderer\PhpRenderer, Zend\View\Renderer, Zend\View\Resolver, + Zend\View\Variables as ViewVariables, Zend\View\View; class ViewTest extends TestCase @@ -75,22 +76,20 @@ public function testRendersViewModelWithChildren() $this->attachTestStrategies(); $child1 = new ViewModel(array('foo' => 'bar')); - $child1->setCaptureTo('child1'); $child2 = new ViewModel(array('bar' => 'baz')); - $child2->setCaptureTo('child2'); $this->model->setVariable('parent', 'node'); - $this->model->addChild($child1); - $this->model->addChild($child2); + $this->model->addChild($child1, 'child1'); + $this->model->addChild($child2, 'child2'); $this->view->render($this->model); - $expected = var_export(array( + $expected = var_export(new ViewVariables(array( 'parent' => 'node', 'child1' => var_export(array('foo' => 'bar'), true), 'child2' => var_export(array('bar' => 'baz'), true), - ), true); + )), true); $this->assertEquals($expected, $this->result->content); } @@ -110,13 +109,13 @@ public function testRendersTreeOfModels() $this->view->render($this->model); - $expected = var_export(array( + $expected = var_export(new ViewVariables(array( 'parent' => 'node', 'child1' => var_export(array( 'foo' => 'bar', 'child2' => var_export(array('bar' => 'baz'), true), ), true), - ), true); + )), true); $this->assertEquals($expected, $this->result->content); } @@ -153,11 +152,11 @@ public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() $this->view->render($this->model); - $expected = var_export(array( + $expected = var_export(new ViewVariables(array( 'parent' => 'node', 'child1' => var_export(array('foo' => 'bar'), true), 'child2' => json_encode(array('bar' => 'baz')), - ), true); + )), true); $this->assertEquals($expected, $this->result->content); } @@ -199,10 +198,10 @@ public function testOmittingCaptureToValueInChildLeadsToOmissionInParent() $this->view->render($this->model); - $expected = var_export(array( + $expected = var_export(new ViewVariables(array( 'parent' => 'node', 'child1' => var_export(array('foo' => 'bar'), true), - ), true); + )), true); $this->assertEquals($expected, $this->result->content); } From c0c03da3b8cf44250ab0aaed0cd71b0845e1171d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 12:19:13 -0600 Subject: [PATCH 58/69] [zendframework/zf2#1] allow using closures/functors as values - Invokes closures/functors when retrieving values - Escapes return value before returning - getRawValue() returns original closure/functor --- src/Variables.php | 11 ++++++- test/TestAsset/VariableFunctor.php | 44 ++++++++++++++++++++++++++ test/VariablesTest.php | 50 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/TestAsset/VariableFunctor.php diff --git a/src/Variables.php b/src/Variables.php index 4dee1da2..7b6effe7 100644 --- a/src/Variables.php +++ b/src/Variables.php @@ -288,7 +288,16 @@ public function offsetGet($key) return null; } - return parent::offsetGet($key); + $return = parent::offsetGet($key); + + // If we have a closure/functor, invoke it, and ensure the return value + // is escaped. + if (is_object($return) && is_callable($return)) { + $return = call_user_func($return); + return $this->escape($return); + } + + return $return; } /** diff --git a/test/TestAsset/VariableFunctor.php b/test/TestAsset/VariableFunctor.php new file mode 100644 index 00000000..c4b72ee8 --- /dev/null +++ b/test/TestAsset/VariableFunctor.php @@ -0,0 +1,44 @@ +value = $value; + } + + public function __invoke() + { + return $this->value; + } +} diff --git a/test/VariablesTest.php b/test/VariablesTest.php index 130d10aa..0f50b4f6 100644 --- a/test/VariablesTest.php +++ b/test/VariablesTest.php @@ -223,4 +223,54 @@ public function testCallingClearEmptiesObject() $rawValues = $this->vars->getRawValues(); $this->assertEquals(0, count($rawValues)); } + + public function testAllowsSpecifyingClosureValuesAndReturningTheValue() + { + $this->vars->foo = function () { + return 'bar'; + }; + + $this->assertEquals('bar', $this->vars->foo); + } + + public function testAllowsSpecifyingFunctorValuesAndReturningTheValue() + { + $this->vars->foo = new TestAsset\VariableFunctor('bar'); + $this->assertEquals('bar', $this->vars->foo); + } + + public function testClosureValuesAreEscaped() + { + $this->vars->foo = function () { + return 'bar'; + }; + + $expected = $this->vars->escape('bar'); + $this->assertEquals($expected, $this->vars->foo); + } + + public function testFunctorValuesAreEscaped() + { + $this->vars->foo = new TestAsset\VariableFunctor('bar'); + $expected = $this->vars->escape('bar'); + $this->assertEquals($expected, $this->vars->foo); + } + + public function testCallbackValuesAreReturnedAsIsWhenRetrievedAsRawValue() + { + $value = function () { + return 'bar'; + }; + $this->vars->foo = $value; + + $this->assertSame($value, $this->vars->getRawValue('foo')); + } + + public function testFunctorValuesAreReturnedAsIsWhenRetrievedAsRawValue() + { + $value = new TestAsset\VariableFunctor('bar'); + $this->vars->foo = $value; + + $this->assertSame($value, $this->vars->getRawValue('foo')); + } } From 97c8948829622cb1be5f4d84dda9c25a5f40cc98 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 12:31:36 -0600 Subject: [PATCH 59/69] [zendframework/zf2#1] added property overloading to ViewModel - ViewModel now allows property overloading that proxies to the variables property. --- src/Model/ViewModel.php | 57 ++++++++++++++++++++++++++++++++++++ test/Model/ViewModelTest.php | 14 +++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index 11a1ee32..1568f8a9 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -94,6 +94,63 @@ public function __construct($variables = null, $options = array()) $this->setOptions($options); } + /** + * Property overloading: set variable value + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $variables = $this->getVariables(); + $variables[$name] = $value; + } + + /** + * Property overloading: get variable value + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!$this->__isset($name)) { + return null; + } + + $variables = $this->getVariables(); + return $variables[$name]; + } + + /** + * Property overloading: do we have the requested variable value? + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $variables = $this->getVariables(); + return isset($variables[$name]); + } + + /** + * Property overloading: unset the requested variable + * + * @param string $name + * @return void + */ + public function __unset($name) + { + if (!$this->__isset($name)) { + return null; + } + + $variables = $this->getVariables(); + unset($variables[$name]); + } + /** * Set renderer option/hint * diff --git a/test/Model/ViewModelTest.php b/test/Model/ViewModelTest.php index 8b076a4b..fcd6434d 100644 --- a/test/Model/ViewModelTest.php +++ b/test/Model/ViewModelTest.php @@ -232,4 +232,18 @@ public function testAllowsPassingViewVariablesContainerAsVariables() $model->setVariables($variables); $this->assertSame($variables, $model->getVariables()); } + + public function testPropertyOverloadingGivesAccessToProperties() + { + $model = new ViewModel(); + $variables = $model->getVariables(); + $model->foo = 'bar'; + $this->assertTrue(isset($model->foo)); + $this->assertEquals('bar', $variables['foo']); + $this->assertEquals('bar', $model->foo); + + unset($model->foo); + $this->assertFalse(isset($model->foo)); + $this->assertFalse(isset($variables['foo'])); + } } From be371bf9eac753c2d1329ae18ee0e3947445b16b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 14:13:20 -0600 Subject: [PATCH 60/69] [zendframework/zf2#1] fix ob errors - HeadScript() and HeadStyle() each had tests with bad OB behavior under PHPUnit 3.6; fixed --- test/Helper/HeadScriptTest.php | 1 - test/Helper/HeadStyleTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/test/Helper/HeadScriptTest.php b/test/Helper/HeadScriptTest.php index 3727d6d5..280e6d91 100644 --- a/test/Helper/HeadScriptTest.php +++ b/test/Helper/HeadScriptTest.php @@ -385,7 +385,6 @@ public function testCannotNestCaptures() $this->helper->__invoke()->captureEnd(); $this->assertContains('Cannot nest', $e->getMessage()); } - $this->helper->__invoke()->captureEnd(); } /** diff --git a/test/Helper/HeadStyleTest.php b/test/Helper/HeadStyleTest.php index 12bc4b64..aba0c108 100644 --- a/test/Helper/HeadStyleTest.php +++ b/test/Helper/HeadStyleTest.php @@ -350,7 +350,6 @@ public function testNestedCapturingFails() $this->helper->__invoke()->captureEnd(); $this->assertContains('Cannot nest', $e->getMessage()); } - $this->helper->__invoke()->captureEnd(); } public function testMediaAttributeAsArray() From bfd562372927ee6b3fbf07e8f12d5e46dbd15326 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Feb 2012 15:06:03 -0600 Subject: [PATCH 61/69] [zendframework/zf2#1] fix errant string output in tests - added an expectOutputString() call to ensure output is trapped --- test/Helper/RenderChildModelTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Helper/RenderChildModelTest.php b/test/Helper/RenderChildModelTest.php index 710d7462..a778b809 100644 --- a/test/Helper/RenderChildModelTest.php +++ b/test/Helper/RenderChildModelTest.php @@ -138,14 +138,12 @@ public function testRendersSequentialChildrenWithNestedChildren() $this->assertRegExp('/Content:\s+Layout start\s+Content for layout\s+Layout end\s+Sidebar:\s+Second child/s', $result, $result); } - /** - * @outputBuffering enabled - */ public function testAttemptingToRenderWithNoCurrentModelRaisesException() { $renderer = new PhpRenderer(); $renderer->setResolver($this->resolver); $this->setExpectedException('Zend\View\Exception\RuntimeException', 'no view model'); + $this->expectOutputString("Layout start\n\n"); $renderer->render('layout'); } } From d993918c40e71fd066cc069e7f6738c362a73a29 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 22 Feb 2012 10:44:27 -0600 Subject: [PATCH 62/69] [zendframework/zf2#1] s/setLayout/setTemplate/ in plugins - per Akrabat --- src/Helper/Layout.php | 4 ++-- test/Helper/LayoutTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Helper/Layout.php b/src/Helper/Layout.php index 559da40b..4e03b3e6 100644 --- a/src/Helper/Layout.php +++ b/src/Helper/Layout.php @@ -59,7 +59,7 @@ public function getLayout() * @param string $template * @return Layout */ - public function setLayout($template) + public function setTemplate($template) { $model = $this->getRoot(); $model->setTemplate((string) $template); @@ -80,7 +80,7 @@ public function __invoke($template = null) if (null === $template) { return $this->getRoot(); } - return $this->setLayout($template); + return $this->setTemplate($template); } /** diff --git a/test/Helper/LayoutTest.php b/test/Helper/LayoutTest.php index 4f99aa1c..4c4b5fb7 100644 --- a/test/Helper/LayoutTest.php +++ b/test/Helper/LayoutTest.php @@ -59,9 +59,9 @@ public function setUp() $this->viewModelHelper->setRoot($this->parent); } - public function testCallingSetLayoutAltersRootModelTemplate() + public function testCallingSetTemplateAltersRootModelTemplate() { - $this->helper->setLayout('alternate/layout'); + $this->helper->setTemplate('alternate/layout'); $this->assertEquals('alternate/layout', $this->parent->getTemplate()); } @@ -70,7 +70,7 @@ public function testCallingGetLayoutReturnsRootModelTemplate() $this->assertEquals('layout', $this->helper->getLayout()); } - public function testCallingInvokeProxiesToSetLayout() + public function testCallingInvokeProxiesToSetTemplate() { $helper = $this->helper; $helper('alternate/layout'); @@ -91,6 +91,6 @@ public function testRaisesExceptionIfViewModelHelperHasNoRoot() $helper = $renderer->plugin('layout'); $this->setExpectedException('Zend\View\Exception\RuntimeException', 'view model'); - $helper->setLayout('foo/bar'); + $helper->setTemplate('foo/bar'); } } From 8cbb1a10c8b6b997e7acde675efe6a42280c9cc8 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 28 Feb 2012 11:19:09 -0600 Subject: [PATCH 63/69] [zendframework/zf2#1] simplify aggregate priority - Default to 1, allowing removal of a case statement and code duplication in each strategy - Requires that the EventManager::attachAggregate $priority argument also defaults to 1 --- src/Strategy/FeedStrategy.php | 9 ++------- src/Strategy/JsonStrategy.php | 9 ++------- src/Strategy/PhpRendererStrategy.php | 9 ++------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Strategy/FeedStrategy.php b/src/Strategy/FeedStrategy.php index c4c87237..04cae632 100644 --- a/src/Strategy/FeedStrategy.php +++ b/src/Strategy/FeedStrategy.php @@ -64,16 +64,11 @@ public function __construct(FeedRenderer $renderer) * Attach the aggregate to the specified event manager * * @param EventCollection $events + * @param int $priority * @return void */ - public function attach(EventCollection $events, $priority = null) + public function attach(EventCollection $events, $priority = 1) { - if (null === $priority) { - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); - $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); - return; - } - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer'), $priority); $this->listeners[] = $events->attach('response', array($this, 'injectResponse'), $priority); } diff --git a/src/Strategy/JsonStrategy.php b/src/Strategy/JsonStrategy.php index 4c17e7ef..15ecbb07 100644 --- a/src/Strategy/JsonStrategy.php +++ b/src/Strategy/JsonStrategy.php @@ -63,16 +63,11 @@ public function __construct(JsonRenderer $renderer) * Attach the aggregate to the specified event manager * * @param EventCollection $events + * @param int $priority * @return void */ - public function attach(EventCollection $events, $priority = null) + public function attach(EventCollection $events, $priority = 1) { - if (null === $priority) { - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); - $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); - return; - } - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer'), $priority); $this->listeners[] = $events->attach('response', array($this, 'injectResponse'), $priority); } diff --git a/src/Strategy/PhpRendererStrategy.php b/src/Strategy/PhpRendererStrategy.php index 539bfc44..0b2fdc3c 100644 --- a/src/Strategy/PhpRendererStrategy.php +++ b/src/Strategy/PhpRendererStrategy.php @@ -102,16 +102,11 @@ public function getContentPlaceholders() * Attach the aggregate to the specified event manager * * @param EventCollection $events + * @param int $priority * @return void */ - public function attach(EventCollection $events, $priority = null) + public function attach(EventCollection $events, $priority = 1) { - if (null === $priority) { - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); - $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); - return; - } - $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer'), $priority); $this->listeners[] = $events->attach('response', array($this, 'injectResponse'), $priority); } From edb5015ad59cd23f64375d985601be68d2d5e34c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 28 Feb 2012 13:51:44 -0600 Subject: [PATCH 64/69] [zendframework/zf2#1] change escaping to use ENT_QUOTES - s/ENT_COMPAT/ENT_QUOTES/ in the Escape view helper --- src/Helper/Escape.php | 2 +- test/Helper/HtmlListTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Helper/Escape.php b/src/Helper/Escape.php index e924bf4c..0bada9b1 100644 --- a/src/Helper/Escape.php +++ b/src/Helper/Escape.php @@ -106,7 +106,7 @@ public function getCallback() if (!is_callable($this->callback)) { $encoding = $this->getEncoding(); $callback = function($value) use ($encoding) { - return htmlspecialchars($value, ENT_COMPAT, $encoding, false); + return htmlspecialchars($value, ENT_QUOTES, $encoding, false); }; $this->setCallback($callback); } diff --git a/test/Helper/HtmlListTest.php b/test/Helper/HtmlListTest.php index e76b23fa..2903bc44 100644 --- a/test/Helper/HtmlListTest.php +++ b/test/Helper/HtmlListTest.php @@ -163,7 +163,7 @@ public function testListWithValuesToEscapeForZF2283() $this->assertContains('
  • one <small> test
  • ', $list); $this->assertContains('
  • second & third
  • ', $list); - $this->assertContains('
  • And \'some\' "final" test
  • ', $list); + $this->assertContains('
  • And 'some' "final" test
  • ', $list); } public function testListEscapeSwitchedOffForZF2283() From afb278bcf982ec269d8e5b0c3321dfe74327ab58 Mon Sep 17 00:00:00 2001 From: Matus Zeman Date: Wed, 7 Mar 2012 11:16:38 +0000 Subject: [PATCH 65/69] View Model construct fix - we do not expect options being array only, it can be traversable also --- src/Model/ViewModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index 87b563f5..ea02f47e 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -84,7 +84,7 @@ class ViewModel implements Model * @param array|Traversable $options * @return void */ - public function __construct($variables = null, array $options = null) + public function __construct($variables = null, $options = null) { if (null === $variables) { $variables = new ViewVariables(); From 710cfbd9fb295faedd230ff6f0309ffa1ae7197b Mon Sep 17 00:00:00 2001 From: Thinkscape Date: Thu, 8 Mar 2012 17:29:22 +0100 Subject: [PATCH 66/69] Refactor Zend\Stdlib\ArrayTools to ArrayUtils, add missing headers. - ArrayTools will now be called ArrayUtils after an anonymous vote (http://framework.zend.com/wiki/display/ZFDEV2/POLL+-+Array+class+name). - Add missing Zend Framework headers to ArrayUtils class file and tests. --- src/Model/JsonModel.php | 4 ++-- src/Model/ViewModel.php | 6 +++--- src/Renderer/JsonRenderer.php | 4 ++-- src/Resolver/TemplateMapResolver.php | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Model/JsonModel.php b/src/Model/JsonModel.php index 10269ef2..f1cfd63a 100644 --- a/src/Model/JsonModel.php +++ b/src/Model/JsonModel.php @@ -22,7 +22,7 @@ namespace Zend\View\Model; use Traversable, - Zend\Stdlib\ArrayTools; + Zend\Stdlib\ArrayUtils; /** * @category Zend @@ -42,7 +42,7 @@ public function serialize() { $variables = $this->getVariables(); if ($variables instanceof Traversable) { - $variables = ArrayTools::iteratorToArray($variables); + $variables = ArrayUtils::iteratorToArray($variables); } return json_encode($variables); } diff --git a/src/Model/ViewModel.php b/src/Model/ViewModel.php index c4a7d805..8f3d2cda 100644 --- a/src/Model/ViewModel.php +++ b/src/Model/ViewModel.php @@ -24,7 +24,7 @@ use ArrayAccess, ArrayIterator, Traversable, - Zend\Stdlib\ArrayTools, + Zend\Stdlib\ArrayUtils, Zend\View\Exception, Zend\View\Model, Zend\View\Variables as ViewVariables; @@ -175,7 +175,7 @@ public function setOptions($options) // Assumption is that lowest common denominator for renderer configuration // is an array if ($options instanceof Traversable) { - $options = ArrayTools::iteratorToArray($options); + $options = ArrayUtils::iteratorToArray($options); } if (!is_array($options)) { @@ -230,7 +230,7 @@ public function setVariables($variables) } if ($variables instanceof Traversable) { - $variables = ArrayTools::iteratorToArray($variables); + $variables = ArrayUtils::iteratorToArray($variables); } if (!is_array($variables)) { diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index 441d7d71..c83bcbb8 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -24,7 +24,7 @@ use JsonSerializable, Traversable, Zend\Json\Json, - Zend\Stdlib\ArrayTools, + Zend\Stdlib\ArrayUtils, Zend\View\Exception, Zend\View\Model, Zend\View\Renderer, @@ -131,7 +131,7 @@ public function render($nameOrModel, $values = null) } if ($nameOrModel instanceof Traversable) { - $nameOrModel = ArrayTools::iteratorToArray($nameOrModel); + $nameOrModel = ArrayUtils::iteratorToArray($nameOrModel); return Json::encode($nameOrModel); } diff --git a/src/Resolver/TemplateMapResolver.php b/src/Resolver/TemplateMapResolver.php index bb2e9bbd..711448cd 100644 --- a/src/Resolver/TemplateMapResolver.php +++ b/src/Resolver/TemplateMapResolver.php @@ -24,7 +24,7 @@ use ArrayIterator, IteratorAggregate, Traversable, - Zend\Stdlib\ArrayTools, + Zend\Stdlib\ArrayUtils, Zend\View\Exception, Zend\View\Renderer, Zend\View\Resolver; @@ -85,7 +85,7 @@ public function setMap($map) } if ($map instanceof Traversable) { - $map = ArrayTools::iteratorToArray($map); + $map = ArrayUtils::iteratorToArray($map); } $this->map = $map; @@ -142,7 +142,7 @@ public function merge($map) } if ($map instanceof Traversable) { - $map = ArrayTools::iteratorToArray($map); + $map = ArrayUtils::iteratorToArray($map); } $this->map = array_replace_recursive($this->map, $map); From d142dcbb6918d73da2f8e773f92bc5bc0a68e266 Mon Sep 17 00:00:00 2001 From: Matt Pinkston Date: Fri, 9 Mar 2012 12:15:31 -0600 Subject: [PATCH 67/69] Update to use ArrayUtils, JSON::encode --- src/Model/JsonModel.php | 10 +++++++++- src/Renderer/JsonRenderer.php | 14 +++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Model/JsonModel.php b/src/Model/JsonModel.php index e554080d..0f74a03e 100644 --- a/src/Model/JsonModel.php +++ b/src/Model/JsonModel.php @@ -33,6 +33,14 @@ */ class JsonModel extends ViewModel { + /** + * JSON probably won't need to be captured into a + * a parent container by default. + * + * @var string + */ + protected $captureTo = null; + /** * JSON is usually terminal * @@ -51,6 +59,6 @@ public function serialize() if ($variables instanceof Traversable) { $variables = ArrayUtils::iteratorToArray($variables); } - return json_encode($variables); + return Json::encode($variables); } } diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index e0e7dd36..4d9dba4c 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -165,7 +165,11 @@ public function canRenderTrees() */ protected function recurseModel(Model $model) { - $values = (array) $model->getVariables(); + $values = $model->getVariables(); + if ($variables instanceof Traversable) { + $variables = ArrayUtils::iteratorToArray($variables); + } + if (!$model->hasChildren()) { return $values; } @@ -179,12 +183,12 @@ protected function recurseModel(Model $model) } $childValues = $this->recurseModel($child); - if ($mergeChildren) { - // Merging values with parent - $values = array_replace_recursive($values, $childValues); - } else { + if ($captureTo) { // Capturing to a specific key $values[$captureTo] = $childValues; + } elseif ($mergeChildren) { + // Merging values with parent + $values = array_replace_recursive($values, $childValues); } } return $values; From 5fb25283b241df4a04d1b952b57b91a197625b41 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Thu, 1 Mar 2012 07:59:53 +0100 Subject: [PATCH 68/69] Make it possible to disable memory_limit of the memory cache adapter to not set it up to 1gb on tests --- test/Helper/CurrencyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Helper/CurrencyTest.php b/test/Helper/CurrencyTest.php index 8bf00256..86dcc76c 100644 --- a/test/Helper/CurrencyTest.php +++ b/test/Helper/CurrencyTest.php @@ -66,7 +66,7 @@ public function setUp() { $this->clearRegistry(); - $this->_cache = CacheFactory::adapterFactory('memory', array('memory_limit' => 1073741824)); + $this->_cache = CacheFactory::adapterFactory('memory', array('memory_limit' => 0)); Currency\Currency::setCache($this->_cache); $this->helper = new Helper\Currency('de_AT'); From 4fb7785341255feac93037ddcb780245e0b3650c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Mar 2012 15:33:59 -0500 Subject: [PATCH 69/69] Corrected test assumption in View test - JsonModel now sets "terminal" flag to true by default, which invalidated a test; test now explicity disables it. - JsonModel was missing an import for Zend\Json\Json, which was exposed when the test started working. --- src/Model/JsonModel.php | 1 + test/ViewTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Model/JsonModel.php b/src/Model/JsonModel.php index 0f74a03e..419817ee 100644 --- a/src/Model/JsonModel.php +++ b/src/Model/JsonModel.php @@ -22,6 +22,7 @@ namespace Zend\View\Model; use Traversable, + Zend\Json\Json, Zend\Stdlib\ArrayUtils; /** diff --git a/test/ViewTest.php b/test/ViewTest.php index 678d95bd..c67baa36 100644 --- a/test/ViewTest.php +++ b/test/ViewTest.php @@ -145,6 +145,7 @@ public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() $child2 = new Model\JsonModel(array('bar' => 'baz')); $child2->setCaptureTo('child2'); + $child2->setTerminal(false); $this->model->setVariable('parent', 'node'); $this->model->addChild($child1);