Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit 2faee88

Browse files
committed
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).
1 parent 418c3ae commit 2faee88

File tree

5 files changed

+252
-13
lines changed

5 files changed

+252
-13
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @see http://github.com/zendframework/zend-expressive for the canonical source repository
6+
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
8+
*/
9+
10+
namespace Zend\Expressive\Exception;
11+
12+
use DomainException;
13+
14+
class RenderingException extends DomainException implements ExceptionInterface
15+
{
16+
}

src/Template/ZendView.php

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace Zend\Expressive\Template;
1111

12+
use Zend\View\Model\ModelInterface;
13+
use Zend\View\Model\ViewModel;
1214
use Zend\View\Renderer\RendererInterface;
1315
use Zend\View\Renderer\PhpRenderer;
1416
use Zend\View\Resolver\ResolverInterface;
@@ -22,6 +24,11 @@ class ZendView implements TemplateInterface
2224
{
2325
use ArrayParametersTrait;
2426

27+
/**
28+
* @var ViewModel
29+
*/
30+
private $layout;
31+
2532
/**
2633
* Paths and namespaces data store.
2734
*/
@@ -30,23 +37,39 @@ class ZendView implements TemplateInterface
3037
/**
3138
* @var RendererInterface
3239
*/
33-
private $template;
40+
private $renderer;
3441

3542
/**
3643
* @var ResolverInterface
3744
*/
3845
private $resolver;
3946

4047
/**
41-
* @param null|RendererInterface $template
48+
* @param null|RendererInterface $renderer
4249
*/
43-
public function __construct(RendererInterface $template = null)
50+
public function __construct(RendererInterface $renderer = null, $layout = null)
4451
{
45-
if (null === $template) {
46-
$template = $this->createRenderer();
52+
if (null === $renderer) {
53+
$renderer = $this->createRenderer();
4754
}
48-
$this->template = $template;
49-
$this->resolver = $template->resolver();
55+
$this->renderer = $renderer;
56+
$this->resolver = $renderer->resolver();
57+
58+
if ($layout && is_string($layout)) {
59+
$model = new ViewModel();
60+
$model->setTemplate($layout);
61+
$layout = $model;
62+
}
63+
64+
if ($layout && ! $layout instanceof ModelInterface) {
65+
throw new Exception\InvalidArgumentException(sprintf(
66+
'Layout must be a string layout template name or a %s instance; received %s',
67+
ModelInterface::class,
68+
(is_object($layout) ? get_class($layout) : gettype($layout))
69+
));
70+
}
71+
72+
$this->layout = $layout;
5073
}
5174

5275
/**
@@ -72,20 +95,32 @@ private function getDefaultResolver()
7295
}
7396

7497
/**
75-
* Render
98+
* Render a template with the given parameters.
99+
*
100+
* If a layout was specified during construction, it will be used;
101+
* alternately, you can specify a layout to use via the "layout"
102+
* parameter, using either:
103+
*
104+
* - a string layout template name
105+
* - a Zend\View\Model\ModelInterface instance
106+
*
107+
* Layouts specified with $params take precedence over layouts passed to
108+
* the constructor.
76109
*
77110
* @param string $name
78111
* @param array|object $params
79112
* @return string
80113
*/
81114
public function render($name, $params = [])
82115
{
83-
$params = $this->normalizeParams($params);
84-
return $this->template->render($name, $params);
116+
return $this->renderModel(
117+
$this->createModel($name, $this->normalizeParams($params)),
118+
$this->renderer
119+
);
85120
}
86121

87122
/**
88-
* Add a path for template
123+
* Add a path for templates.
89124
*
90125
* @param string $path
91126
* @param string $namespace
@@ -112,4 +147,87 @@ public function getPaths()
112147
}
113148
return $paths;
114149
}
150+
151+
/**
152+
* Create a view model from the template and parameters.
153+
*
154+
* Injects the created model in the layout view model, if present.
155+
*
156+
* If the $params contains a non-empty 'layout' key, that value will
157+
* be used to seed a layout view model, if:
158+
*
159+
* - it is a string layout template name
160+
* - it is a ModelInterface instance
161+
*
162+
* If a layout is discovered in this way, it will override the one set in
163+
* the constructor, if any.
164+
*
165+
* @param string $name
166+
* @param array $params
167+
* @return ModelInterface
168+
*/
169+
private function createModel($name, array $params)
170+
{
171+
$layout = $this->layout ? clone $this->layout : null;
172+
if (array_key_exists('layout', $params) && $params['layout']) {
173+
if (is_string($params['layout'])) {
174+
$layout = new ViewModel();
175+
$layout->setTemplate($params['layout']);
176+
unset($params['layout']);
177+
} elseif ($params['layout'] instanceof ModelInterface) {
178+
$layout = $params['layout'];
179+
unset($params['layout']);
180+
}
181+
}
182+
183+
if (array_key_exists('layout', $params) && is_string($params['layout']) && $params['layout']) {
184+
$layout = new ViewModel();
185+
$layout->setTemplate($params['layout']);
186+
unset($params['layout']);
187+
}
188+
189+
$model = new ViewModel($params);
190+
$model->setTemplate($name);
191+
192+
if ($layout) {
193+
$layout->addChild($model);
194+
$model = $layout;
195+
}
196+
197+
return $model;
198+
}
199+
200+
/**
201+
* Do a recursive, depth-first rendering of a view model.
202+
*
203+
* @param ModelInterface $model
204+
* @param PhpRenderer $renderer
205+
* @return string
206+
* @throws Exception\RenderingException if it encounters a terminal child.
207+
*/
208+
private function renderModel(ModelInterface $model, PhpRenderer $renderer)
209+
{
210+
foreach ($model as $child) {
211+
if ($child->terminate()) {
212+
throw new Exception\RenderingException('Cannot render; encountered a child marked terminal');
213+
}
214+
215+
$capture = $child->captureTo();
216+
if (empty($capture)) {
217+
continue;
218+
}
219+
220+
$result = $this->renderModel($child, $renderer);
221+
222+
if ($child->isAppend()) {
223+
$oldResult = $model->{$capture};
224+
$model->setVariable($capture, $oldResult . $result);
225+
continue;
226+
}
227+
228+
$model->setVariable($capture, $result);
229+
}
230+
231+
return $renderer->render($model);
232+
}
115233
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Layout Page</title>
5+
</head>
6+
<body>
7+
<?= $this->content ?>
8+
</body>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>ALTERNATE LAYOUT PAGE</title>
5+
</head>
6+
<body>
7+
<?= $this->content ?>
8+
</body>

test/Template/ZendViewTest.php

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPUnit_Framework_TestCase as TestCase;
1414
use Zend\Expressive\Template\ZendView;
1515
use Zend\Expressive\Exception;
16+
use Zend\View\Model\ViewModel;
1617
use Zend\View\Renderer\PhpRenderer;
1718
use Zend\View\Resolver\TemplatePathStack;
1819

@@ -31,14 +32,14 @@ public function testCanPassRendererToConstructor()
3132
{
3233
$template = new ZendView($this->render);
3334
$this->assertInstanceOf(ZendView::class, $template);
34-
$this->assertAttributeSame($this->render, 'template', $template);
35+
$this->assertAttributeSame($this->render, 'renderer', $template);
3536
}
3637

3738
public function testInstantiatingWithoutEngineLazyLoadsOne()
3839
{
3940
$template = new ZendView();
4041
$this->assertInstanceOf(ZendView::class, $template);
41-
$this->assertAttributeInstanceOf(PhpRenderer::class, 'template', $template);
42+
$this->assertAttributeInstanceOf(PhpRenderer::class, 'renderer', $template);
4243
}
4344

4445
public function testCanAddPathWithEmptyNamespace()
@@ -135,4 +136,92 @@ public function testCanRenderWithParameterObjects($params, $search)
135136
$content = str_replace('<?php echo $name ?>', $search, $content);
136137
$this->assertEquals($content, $result);
137138
}
139+
140+
/**
141+
* @group layout
142+
*/
143+
public function testWillRenderContentInLayoutPassedToConstructor()
144+
{
145+
$template = new ZendView(null, 'zendview-layout');
146+
$template->addPath(__DIR__ . '/TestAsset');
147+
$name = 'ZendView';
148+
$result = $template->render('zendview', [ 'name' => $name ]);
149+
$this->assertContains($name, $result);
150+
$content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml');
151+
$content = str_replace('<?php echo $name ?>', $name, $content);
152+
$this->assertContains($content, $result);
153+
$this->assertContains('<title>Layout Page</title>', $result, sprintf("Received %s", $result));
154+
}
155+
156+
/**
157+
* @group layout
158+
*/
159+
public function testWillRenderContentInLayoutPassedDuringRendering()
160+
{
161+
$template = new ZendView(null);
162+
$template->addPath(__DIR__ . '/TestAsset');
163+
$name = 'ZendView';
164+
$result = $template->render('zendview', [ 'name' => $name, 'layout' => 'zendview-layout' ]);
165+
$this->assertContains($name, $result);
166+
$content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml');
167+
$content = str_replace('<?php echo $name ?>', $name, $content);
168+
$this->assertContains($content, $result);
169+
170+
$this->assertContains('<title>Layout Page</title>', $result);
171+
}
172+
173+
/**
174+
* @group layout
175+
*/
176+
public function testLayoutPassedWhenRenderingOverridesLayoutPassedToConstructor()
177+
{
178+
$template = new ZendView(null, 'zendview-layout');
179+
$template->addPath(__DIR__ . '/TestAsset');
180+
$name = 'ZendView';
181+
$result = $template->render('zendview', [ 'name' => $name, 'layout' => 'zendview-layout2' ]);
182+
$this->assertContains($name, $result);
183+
$content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml');
184+
$content = str_replace('<?php echo $name ?>', $name, $content);
185+
$this->assertContains($content, $result);
186+
187+
$this->assertContains('<title>ALTERNATE LAYOUT PAGE</title>', $result);
188+
}
189+
190+
/**
191+
* @group layout
192+
*/
193+
public function testCanPassViewModelForLayoutToConstructor()
194+
{
195+
$layout = new ViewModel();
196+
$layout->setTemplate('zendview-layout');
197+
198+
$template = new ZendView(null, $layout);
199+
$template->addPath(__DIR__ . '/TestAsset');
200+
$name = 'ZendView';
201+
$result = $template->render('zendview', [ 'name' => $name ]);
202+
$this->assertContains($name, $result);
203+
$content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml');
204+
$content = str_replace('<?php echo $name ?>', $name, $content);
205+
$this->assertContains($content, $result);
206+
$this->assertContains('<title>Layout Page</title>', $result, sprintf("Received %s", $result));
207+
}
208+
209+
/**
210+
* @group layout
211+
*/
212+
public function testCanPassViewModelForLayoutParameterWhenRendering()
213+
{
214+
$layout = new ViewModel();
215+
$layout->setTemplate('zendview-layout2');
216+
217+
$template = new ZendView(null, 'zendview-layout');
218+
$template->addPath(__DIR__ . '/TestAsset');
219+
$name = 'ZendView';
220+
$result = $template->render('zendview', [ 'name' => $name, 'layout' => $layout ]);
221+
$this->assertContains($name, $result);
222+
$content = file_get_contents(__DIR__ . '/TestAsset/zendview.phtml');
223+
$content = str_replace('<?php echo $name ?>', $name, $content);
224+
$this->assertContains($content, $result);
225+
$this->assertContains('<title>ALTERNATE LAYOUT PAGE</title>', $result);
226+
}
138227
}

0 commit comments

Comments
 (0)