From 2faee884ee85ab4bcece54a38a5c414687afc209 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 25 Aug 2015 17:03:30 -0500 Subject: [PATCH 1/3] Abstract template layouts in zend-view implementation This patch abstracts layouts by doing the following: - Allowing passing a layout script name or ViewModel representing the layout as the second argument in the constructor. - Allowing passing a "layout" parameter during rendering, which may be a layout script name or a ViewModel representing the layout. In each case, rendering now munges the provided template and parameters into a view model, injecting it as a child of the layout view model if one is discovered (if a layout script name is passed, a layout view model is created to represent it). --- src/Exception/RenderingException.php | 16 ++ src/Template/ZendView.php | 140 ++++++++++++++++-- test/Template/TestAsset/zendview-layout.phtml | 8 + .../Template/TestAsset/zendview-layout2.phtml | 8 + test/Template/ZendViewTest.php | 93 +++++++++++- 5 files changed, 252 insertions(+), 13 deletions(-) create mode 100644 src/Exception/RenderingException.php create mode 100644 test/Template/TestAsset/zendview-layout.phtml create mode 100644 test/Template/TestAsset/zendview-layout2.phtml diff --git a/src/Exception/RenderingException.php b/src/Exception/RenderingException.php new file mode 100644 index 00000000..d4a36ced --- /dev/null +++ b/src/Exception/RenderingException.php @@ -0,0 +1,16 @@ +createRenderer(); + if (null === $renderer) { + $renderer = $this->createRenderer(); } - $this->template = $template; - $this->resolver = $template->resolver(); + $this->renderer = $renderer; + $this->resolver = $renderer->resolver(); + + if ($layout && is_string($layout)) { + $model = new ViewModel(); + $model->setTemplate($layout); + $layout = $model; + } + + if ($layout && ! $layout instanceof ModelInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Layout must be a string layout template name or a %s instance; received %s', + ModelInterface::class, + (is_object($layout) ? get_class($layout) : gettype($layout)) + )); + } + + $this->layout = $layout; } /** @@ -72,7 +95,17 @@ private function getDefaultResolver() } /** - * Render + * Render a template with the given parameters. + * + * If a layout was specified during construction, it will be used; + * alternately, you can specify a layout to use via the "layout" + * parameter, using either: + * + * - a string layout template name + * - a Zend\View\Model\ModelInterface instance + * + * Layouts specified with $params take precedence over layouts passed to + * the constructor. * * @param string $name * @param array|object $params @@ -80,12 +113,14 @@ private function getDefaultResolver() */ public function render($name, $params = []) { - $params = $this->normalizeParams($params); - return $this->template->render($name, $params); + return $this->renderModel( + $this->createModel($name, $this->normalizeParams($params)), + $this->renderer + ); } /** - * Add a path for template + * Add a path for templates. * * @param string $path * @param string $namespace @@ -112,4 +147,87 @@ public function getPaths() } return $paths; } + + /** + * Create a view model from the template and parameters. + * + * Injects the created model in the layout view model, if present. + * + * If the $params contains a non-empty 'layout' key, that value will + * be used to seed a layout view model, if: + * + * - it is a string layout template name + * - it is a ModelInterface instance + * + * If a layout is discovered in this way, it will override the one set in + * the constructor, if any. + * + * @param string $name + * @param array $params + * @return ModelInterface + */ + private function createModel($name, array $params) + { + $layout = $this->layout ? clone $this->layout : null; + if (array_key_exists('layout', $params) && $params['layout']) { + if (is_string($params['layout'])) { + $layout = new ViewModel(); + $layout->setTemplate($params['layout']); + unset($params['layout']); + } elseif ($params['layout'] instanceof ModelInterface) { + $layout = $params['layout']; + unset($params['layout']); + } + } + + if (array_key_exists('layout', $params) && is_string($params['layout']) && $params['layout']) { + $layout = new ViewModel(); + $layout->setTemplate($params['layout']); + unset($params['layout']); + } + + $model = new ViewModel($params); + $model->setTemplate($name); + + if ($layout) { + $layout->addChild($model); + $model = $layout; + } + + return $model; + } + + /** + * Do a recursive, depth-first rendering of a view model. + * + * @param ModelInterface $model + * @param PhpRenderer $renderer + * @return string + * @throws Exception\RenderingException if it encounters a terminal child. + */ + private function renderModel(ModelInterface $model, PhpRenderer $renderer) + { + foreach ($model as $child) { + if ($child->terminate()) { + throw new Exception\RenderingException('Cannot render; encountered a child marked terminal'); + } + + $capture = $child->captureTo(); + if (empty($capture)) { + continue; + } + + $result = $this->renderModel($child, $renderer); + + if ($child->isAppend()) { + $oldResult = $model->{$capture}; + $model->setVariable($capture, $oldResult . $result); + continue; + } + + $model->setVariable($capture, $result); + } + + return $renderer->render($model); + } } diff --git a/test/Template/TestAsset/zendview-layout.phtml b/test/Template/TestAsset/zendview-layout.phtml new file mode 100644 index 00000000..0db1c312 --- /dev/null +++ b/test/Template/TestAsset/zendview-layout.phtml @@ -0,0 +1,8 @@ + + + + Layout Page + + +content ?> + diff --git a/test/Template/TestAsset/zendview-layout2.phtml b/test/Template/TestAsset/zendview-layout2.phtml new file mode 100644 index 00000000..0b71aa4b --- /dev/null +++ b/test/Template/TestAsset/zendview-layout2.phtml @@ -0,0 +1,8 @@ + + + + ALTERNATE LAYOUT PAGE + + +content ?> + diff --git a/test/Template/ZendViewTest.php b/test/Template/ZendViewTest.php index 9e2ae92a..95498af5 100644 --- a/test/Template/ZendViewTest.php +++ b/test/Template/ZendViewTest.php @@ -13,6 +13,7 @@ use PHPUnit_Framework_TestCase as TestCase; use Zend\Expressive\Template\ZendView; use Zend\Expressive\Exception; +use Zend\View\Model\ViewModel; use Zend\View\Renderer\PhpRenderer; use Zend\View\Resolver\TemplatePathStack; @@ -31,14 +32,14 @@ public function testCanPassRendererToConstructor() { $template = new ZendView($this->render); $this->assertInstanceOf(ZendView::class, $template); - $this->assertAttributeSame($this->render, 'template', $template); + $this->assertAttributeSame($this->render, 'renderer', $template); } public function testInstantiatingWithoutEngineLazyLoadsOne() { $template = new ZendView(); $this->assertInstanceOf(ZendView::class, $template); - $this->assertAttributeInstanceOf(PhpRenderer::class, 'template', $template); + $this->assertAttributeInstanceOf(PhpRenderer::class, 'renderer', $template); } public function testCanAddPathWithEmptyNamespace() @@ -135,4 +136,92 @@ public function testCanRenderWithParameterObjects($params, $search) $content = str_replace('', $search, $content); $this->assertEquals($content, $result); } + + /** + * @group layout + */ + public function testWillRenderContentInLayoutPassedToConstructor() + { + $template = new ZendView(null, 'zendview-layout'); + $template->addPath(__DIR__ . '/TestAsset'); + $name = 'ZendView'; + $result = $template->render('zendview', [ 'name' => $name ]); + $this->assertContains($name, $result); + $content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml'); + $content = str_replace('', $name, $content); + $this->assertContains($content, $result); + $this->assertContains('Layout Page', $result, sprintf("Received %s", $result)); + } + + /** + * @group layout + */ + public function testWillRenderContentInLayoutPassedDuringRendering() + { + $template = new ZendView(null); + $template->addPath(__DIR__ . '/TestAsset'); + $name = 'ZendView'; + $result = $template->render('zendview', [ 'name' => $name, 'layout' => 'zendview-layout' ]); + $this->assertContains($name, $result); + $content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml'); + $content = str_replace('', $name, $content); + $this->assertContains($content, $result); + + $this->assertContains('Layout Page', $result); + } + + /** + * @group layout + */ + public function testLayoutPassedWhenRenderingOverridesLayoutPassedToConstructor() + { + $template = new ZendView(null, 'zendview-layout'); + $template->addPath(__DIR__ . '/TestAsset'); + $name = 'ZendView'; + $result = $template->render('zendview', [ 'name' => $name, 'layout' => 'zendview-layout2' ]); + $this->assertContains($name, $result); + $content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml'); + $content = str_replace('', $name, $content); + $this->assertContains($content, $result); + + $this->assertContains('ALTERNATE LAYOUT PAGE', $result); + } + + /** + * @group layout + */ + public function testCanPassViewModelForLayoutToConstructor() + { + $layout = new ViewModel(); + $layout->setTemplate('zendview-layout'); + + $template = new ZendView(null, $layout); + $template->addPath(__DIR__ . '/TestAsset'); + $name = 'ZendView'; + $result = $template->render('zendview', [ 'name' => $name ]); + $this->assertContains($name, $result); + $content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml'); + $content = str_replace('', $name, $content); + $this->assertContains($content, $result); + $this->assertContains('Layout Page', $result, sprintf("Received %s", $result)); + } + + /** + * @group layout + */ + public function testCanPassViewModelForLayoutParameterWhenRendering() + { + $layout = new ViewModel(); + $layout->setTemplate('zendview-layout2'); + + $template = new ZendView(null, 'zendview-layout'); + $template->addPath(__DIR__ . '/TestAsset'); + $name = 'ZendView'; + $result = $template->render('zendview', [ 'name' => $name, 'layout' => $layout ]); + $this->assertContains($name, $result); + $content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml'); + $content = str_replace('', $name, $content); + $this->assertContains($content, $result); + $this->assertContains('ALTERNATE LAYOUT PAGE', $result); + } } From fbe175984bc62bc961f3016b71767c3c17f84925 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 25 Aug 2015 17:14:46 -0500 Subject: [PATCH 2/3] Documented layout capabilities. --- doc/book/template/zend-view.md | 88 +++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/doc/book/template/zend-view.md b/doc/book/template/zend-view.md index a4666813..afd30065 100644 --- a/doc/book/template/zend-view.md +++ b/doc/book/template/zend-view.md @@ -59,24 +59,80 @@ $templates = new ZendView($zendView); ## Layouts Unlike the other supported template engines, zend-view does not support layouts -out-of-the-box. +out-of-the-box. Expressive abstracts this fact away, providing two facilities +for doing so: -Layouts are accomplished in one of two ways: +- You may pass a layout template name or `Zend\View\Model\ModelInterface` + instance representing the layout as the second argument to the constructor. +- You may pass a "layout" parameter during rendering, with a value of either a + layout template name or a `Zend\View\Model\ModelInterface` + instance representing the layout. Passing a layout this way will override any + layout provided to the constructor. -- Multiple rendering passes: +In each case, the zend-view implementation will do a depth-first, recursive +render in order to provide content within the selected layout. - ```php - $content = $templates->render('blog/entry', [ 'entry' => $entry ]); - $layout = $templates->render('layout/layout', [ 'content' => $content ]); - ``` +### Layout name passed to constructor -- View models. To accomplish this, you will compose a view model for the - content, and pass it as a value to the layout: +```php +use Zend\Expressive\Template\ZendView; + +// Create the engine instance with a layout name: +$zendView = new PhpRenderer(null, 'layout'); +``` + +### Layout view model passed to constructor + +```php +use Zend\Expressive\Template\ZendView; +use Zend\View\Model\ViewModel + +// Create the layout view model: +$layout = new ViewModel([ + 'encoding' => 'utf-8', + 'cssPath' => '/css/prod/', +]); +$layout->setTemplate('layout'); + +// Create the engine instance with the layout: +$zendView = new PhpRenderer(null, $layout); +``` + +### Provide a layout name when rendering + +```php +$content = $templates->render('blog/entry', [ + 'layout' => 'blog', + 'entry' => $entry, +]); +``` + +### Provide a layout view model when rendering + +```php +use Zend\View\Model\ViewModel + +// Create the layout view model: +$layout = new ViewModel([ + 'encoding' => 'utf-8', + 'cssPath' => '/css/blog/', +]); +$layout->setTemplate('layout'); + +$content = $templates->render('blog/entry', [ + 'layout' => $layout, + 'entry' => $entry, +]); +``` + +## Recommendations + +We recommend the following practices when using the zend-view adapter: - ```php - use Zend\View\Model\ViewModel; - - $viewModel = new ViewModel(['entry' => $entry]); - $viewModel->setTemplate('blog/entry'); - $layout = $templates->render('layout/layout', [ 'content' => $viewModel ]); - ``` +- If using a layout, create a factory to return the layout view model as a + service; this allows you to inject it into middleware and add variables to it. +- While we support passing the layout as a rendering parameter, be aware that if + you change engines, this may not be supported. +- While you can use alternate resolvers, not all of them will work with the + `addPath()` implementation. As such, we recommend setting up resolvers and + paths only during creation of the template adapter. From e16f23353081f186d1c92da32d1076412a4212c8 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 25 Aug 2015 17:20:41 -0500 Subject: [PATCH 3/3] Better docblock for ZendView constructor --- src/Template/ZendView.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Template/ZendView.php b/src/Template/ZendView.php index 8d0868f2..4e03e0bf 100644 --- a/src/Template/ZendView.php +++ b/src/Template/ZendView.php @@ -45,7 +45,23 @@ class ZendView implements TemplateInterface private $resolver; /** + * Constructor + * + * Allows specifying the renderer to use (any zend-view renderer is + * allowed), and optionally also the layout. + * + * The layout may be: + * + * - a string layout name + * - a ModelInterface instance representing the layout + * + * If no renderer is provided, a default PhpRenderer instance is created; + * omitting the layout indicates no layout should be used by default when + * rendering. + * * @param null|RendererInterface $renderer + * @param null|string|ModelInterface $layout + * @throws InvalidArgumentException for invalid $layout types */ public function __construct(RendererInterface $renderer = null, $layout = null) { @@ -201,11 +217,11 @@ private function createModel($name, array $params) * Do a recursive, depth-first rendering of a view model. * * @param ModelInterface $model - * @param PhpRenderer $renderer + * @param RendererInterface $renderer * @return string * @throws Exception\RenderingException if it encounters a terminal child. */ - private function renderModel(ModelInterface $model, PhpRenderer $renderer) + private function renderModel(ModelInterface $model, RendererInterface $renderer) { foreach ($model as $child) { if ($child->terminate()) {