注意:这篇文章是 ZF2 官方教程之外的一篇,如果想直接看教程,请看下一章节。
在结束上一章节(数据库和模式)之后,ZF2 官方教程还剩最后三个章节,分别是:
是不是觉得少了些什么?是的,少了MVC模式中的“V”,也就是少了有关视图(View)的介绍,在整个 ZF2 教程中对视图(View)的介绍很少,只是简单的要求我们在 view 目录下建立四个空白的 .phtml 的文件,然后复制代码,然后运行......而且对于 ZF2 中与视图(View)有关的另一个概念:布局(Layout)也说的很少,基本上没有提到。但是我认为先了解视图(View)和布局(Layout)是有必要的,所以有了下文。
下面这篇文章来自于 ZF2 参考(Zend Framework 2 Reference)原文在此。以下的文字来自于我对原文的意译不是非常准确和通顺,对于鸟语好的同学最好阅读原文。
Zend\View 为 ZF2 系统提供了“视图(View)”级别的支持,它是一个多层机制系统(multi-tiered system),允许多种多样的扩展机制,多种多样的替代以及更多。
视图(View)级别的组件如下:
此外,ZF2 提供在 Zend\Mvc\View 命名空间下的许多 MVC 事件监听器集成
手册的这部分向你展示当使用 ZF2 MVC 时视图(view)的经典使用模式。假设你正在使用依赖注入(Dependency Injection)和默认的 MVC 视图(view)策略。
默认的配置直接拿来就可以使用,然而你依然需要选择解析策略(Resolver Strategies)并且对选择的策略进行配置。就像可能表明交替模板名称的东西一样,象站点布局,404(没有找到)页面和错误页面。为了实现这个功能,以下代码片段可以添加到你的配置中。我们建议将代码添加到一个站点特定的模块(site-specific module)中,例如放入 Zend 应用程序骨架(ZendSkeletonApplication)中的“Application”模块中或者放入一个你在 config/autoload/ 目录中自动调用的配置文件中。
return array( 'view_manager' => array( // 模板映射解析器允许你直接地映射模板名称到一个特殊的模板。 // 以下映射将提供首页模板("application/index/index")位置 // 以及布局("layout/layout"),错误页面("error/index")和 // 404 page ("error/404"), 决定他们对应的视图(view)代码 'template_map' => array( 'application/index/index' => __DIR__ . '/../view/application/index/index.phtml', 'site/layout' => __DIR__ . '/../view/layout/layout.phtml', 'error/index' => __DIR__ . '/../view/error/index.phtml', 'error/404' => __DIR__ . '/../view/error/404.phtml', ), // 模板路径堆栈(TemplatePathStack)是一个目录数组。 // 这些目录根据请求的视图(view)代码以 LIFO 方式(它是一个堆栈)进行搜索 // 这是一个进行快速开发应用程序非常好的解决方案,但是由于在产品中有很多必要的静态调用而导致潜在的声明影响了性能。 // // 下面添加了一个当前模块的视图(view)目录的入口 // 确保你的关键字在各个模块中是不同的,确保关键字不会相互覆盖 -- 或者简单的忽略关键字! 'template_path_stack' => array( 'application' => __DIR__ . '/../view', ), // 这是定义默认模板文件的后缀,默认是"phtml"。 'default_template_suffix' => 'php', // 设定站点布局模板名称 // // 默认情况下,MVC 默认的渲染策略使用 "layout/layout" 来作为站点布局模板名称。 // 这里,我们使用 "site/layout"。 // 我们通过上面提到的模板映射解析器(TemplateMapResolver)来映射。 'layout' => 'site/layout', // 默认情况下,MVC 注册一个 "异常策略" // 当一个请求的 action 引发一个异常时触发 // 它创建一个自定义的视图模型(view model)来包裹住异常并且选择一个模板 // 我们将设定它到 "error/index" 目录下 // // 此外,我们告诉它我们要显示一个异常跟踪 // 你很有可能默认想要关闭这个工能 'display_exceptions' => true, 'exception_template' => 'error/index', // 另一个 MVC 默认的策略是"路由没有找到"(route not found) // 基本上要触发这个策略(a)没有路由能够匹配当前的请求,(b)控制器(controller)指定的路由在服务定位器(service locator)中无法找到,(c)控制器(controller)指定的路由在 DispatchableInterface 接口中无效,(d)从控制器(controller)返回的响应状态是 404 // // 在这种情况下,默认使用的是 "error" 模板,就像异常策略。 // 这里,我们将使用 "error/404" 模板(我们通过上面提到的模板映射解析器(TemplateMapResolver)来映射。) // // 你可以有选择性的注入 404 状况的原因;具体见各种各样的 `Application\:\:ERROR_*`_ 常量列表。 // 此外,许多 404 状况来自于路由和调度过程中的异常。 // 你可以有选择性的设定打开或关闭 'display_not_found_reason' => true, 'not_found_template' => 'error/404', ), );
Zend\View\View 消耗 ViewModel,传送它们给选择的渲染器。可是在哪里创建它们呢?
最直接的方式是在你的控制器(Controllers)中创建并返回它们。
namespace Foo\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class BazBatController extends AbstractActionController { public function doSomethingCrazyAction() { $view = new ViewModel(array( 'message' => 'Hello world', )); $view->setTemplate('foo/baz-bat/do-something-crazy'); return $view; } }
在许多项目中,你很可能有一个基于模块命名空间(Module Namespace),控制器(Controller)和 action 的模板名称。考虑一下,如果你简单的传送一些变量,是否可以变得简单一点?绝对。
MVC 为控制器(Controller)注册几个监听器(listeners)来自动化实现这个。首先注意查看你是否从你的控制器(Controller)中返回了一个关联数组,如果返回了,监听器(listener)将创建一个视图模型(View Model)并且使这个关联数组成为变量容器(Variables Container);这个视图模型(View Model)然后替换 MvcEvent 的结果。同样也会注意查看你是否没有任何返回值或者返回 null;如果返回了,监听器(listener)将创建一个没有任何关联数组的视图模型(View Model),这个视图模型(View Model)也会替换 MvcEvent 的结果。
第二个监听器(Listener)检查 MvcEvent 结果是否是一个视图模型(View Model),如果是一个视图模型(View Model)而且这个模型中含有关联的模板信息,如果没有,监听器(Listener)将检查在路由过程中匹配的控制器(Controller)来决定模块的命名空间(Module namespace)和控制器类名称。如果有效,它为了创建一个模块名字的“action”参数。这将是“module/controller/action”,标准化的小写字母和破折号分隔的字符。
举个例子,控制器 Foo\Controller\BazBatController 和 action “doSomethingCrazyAction”将被映射到 foo/baz-bat/do-something-crazy,正如你所看到的,“Controller” 和 “Action”被省略了。
在实践中,我们先前的例子可以重写为以下代码
namespace Foo\Controller; use Zend\Mvc\Controller\AbstractActionController; class BazBatController extends AbstractActionController { public function doSomethingCrazyAction() { return array( 'message' => 'Hello world', ); } }
以上的方法(Method)可能可以在我们多数项目中工作。当你需要指定一个不同的模板,明确的创建和返回一个视图模型(View Model)并且手动指定模板,就像第一个例子一样。
另一种使用情况是当你需要嵌套视图模型(view model)时你需要明确的设定视图模型(view model)。换句话说,你可以在你返回的主视图中包含需要渲染的模板。
例如,你可能想要一个来自于主要片段中的action的视图(View),包含所有的“章节”和几个侧边栏;同样其中一个侧边栏可以包含来自于多个视图(view)的内容。
namespace Content\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class ArticleController extends AbstractActionController { public function viewAction() { // 从数据持久层(persistence layer)获得文章 $view = new ViewModel(); // 这不是必须的,因为它与 "module/controller/action" 匹配 $view->setTemplate('content/article/view'); $articleView = new ViewModel(array('article' => $article)); $articleView->setTemplate('content/article'); $primarySidebarView = new ViewModel(); $primarySidebarView->setTemplate('content/main-sidebar'); $secondarySidebarView = new ViewModel(); $secondarySidebarView->setTemplate('content/secondary-sidebar'); $sidebarBlockView = new ViewModel(); $sidebarBlockView->setTemplate('content/block'); $secondarySidebarView->addChild($sidebarBlockView, 'block'); $view->addChild($articleView, 'article') ->addChild($primarySidebarView, 'sidebar_primary') ->addChild($secondarySidebarView, 'sidebar_secondary'); return $view; } }
以上代码将创建并返回一个指定的模板“content/article/view”的视图模型(view model)。当这个视图被渲染时,将同时渲染三个子视图(child views),它们是:$articleView,$primarySidebarView 和 $secondarySidebarView;它们将分别被捕获到 $view 的“article”, “sidebar_primary”和“sidebar_secondary” 变量中,这样,当它们渲染时,你可以包含它们的内容。此外,$secondarySidebarView 将包含一个额外的视图模型(view model)$sidebarBlockView,它将被“block”视图变量捕获。
为了更好的想象,让我们看看最终的内容可能会是什么样子,每个嵌套都有详细的注释。
这里是一个基于12列网格进行渲染的模板
<?php // "content/article/view" template ?> <!-- This is from the $view View Model, and the "content/article/view" template --> <div class="row content"> <?php echo $this->article ?> <?php echo $this->sidebar_primary ?> <?php echo $this->sidebar_secondary ?> </div>
<?php // "content/article" template ?> <!-- This is from the $articleView View Model, and the "content/article" template --> <article class="span8"> <?php echo $this->escapeHtml('article') ?> </article>
<?php // "content/main-sidebar" template ?> <!-- This is from the $primarySidebarView View Model, and the "content/main-sidebar" template --> <div class="span2 sidebar"> sidebar content... </div>
<?php // "content/secondary-sidebar template ?> <!-- This is from the $secondarySidebarView View Model, and the "content/secondary-sidebar" template --> <div class="span2 sidebar pull-right"> <?php echo $this->block ?> </div>
<?php // "content/block template ?> <!-- This is from the $sidebarBlockView View Model, and the "content/block" template --> <div class="block"> block content... </div>
<!-- This is from the $view View Model, and the "content/article/view" template --> <div class="row content"> <!-- This is from the $articleView View Model, and the "content/article" template --> <article class="span8"> Lorem ipsum .... </article> <!-- This is from the $primarySidebarView View Model, and the "content/main-sidebar" template --> <div class="span2 sidebar"> sidebar content... </div> <!-- This is from the $secondarySidebarView View Model, and the "content/secondary-sidebar" template --> <div class="span2 sidebar pull-right"> <!-- This is from the $sidebarBlockView View Model, and the "content/block" template --> <div class="block"> block content... </div> </div> </div>
正如你所看到的,你可以使用嵌套视图(nested Views)来实现非常复杂的结构,同时在控制器(Controller)的请求/响应生命周期期间对渲染的细节保持独立性。
大多数网站采用一个有凝聚力的外观和感觉,我们通常称之为“布局(Layout)”。它包括默认的层叠样式表(stylesheets)和必要的 javascript,如果有的话,就像其它基本结构一样站点的内容都需要包含它。
在 ZF2 里,布局(layouts)通过视图模型(view model)的嵌套来使用(参见上面的视图模型嵌套的例子)。Zend\Mvc\View\Http\ViewManager 构成一个视图模型来担当视图模型嵌套的“root”。同样的,它将包含站点的骨架(skeleton)或者布局(layout)模板。然后所有其它内容将被这个根视图模型中的视图变量所捕获和渲染。
ViewManager 设置“layout/layout”默认为布局的模板。要修改这个配置,你可以在你的配置文件中的“view_manager”区域里添加一些配置。
在控制器中的监听器 Zend\Mvc\View\Http\InjectViewModelListener,将一个从控制器中返回的视图模型(view model),并将它象一个子视图模式一样注入到根(布局)视图模型。默认情况下,视图模型将捕获名为“content”的根视图模型变量。意思是说你可以在你的布局视图代码中使用下面的代码
<html> <head> <title><?php echo $this->headTitle() ?></title> </head> <body> <?php echo $this->content; ?> </body> </html>
namespace Foo\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class BazBatController extends AbstractActionController { public function doSomethingCrazyAction() { $view = new ViewModel(array( 'message' => 'Hello world', )); // Capture to the layout view's "article" variable $view->setCaptureTo('article'); return $view; } }
namespace Foo\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class BazBatController extends AbstractActionController { public function doSomethingCrazyAction() { $view = new ViewModel(array( 'message' => 'Hello world', )); // 关闭布局;`MvcEvent` 将使用这个视图模型实例 $view->setTerminal(true); return $view; } }
namespace Content\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class ArticleController extends AbstractActionController { public function viewAction() { // get the article from the persistence layer, etc... // Get the "layout" view model and inject a sidebar $layout = $this->layout(); $sidebarView = new ViewModel(); $sidebarView->setTemplate('content/sidebar'); $layout->addChild($sidebarView, 'sidebar'); // Create and return a view model for the retrieved article $view = new ViewModel(array('article' => $article)); $view->setTemplate('content/article'); return $view; } }
//In a controller namespace Content\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class ArticleController extends AbstractActionController { public function viewAction() { // get the article from the persistence layer, etc... // Get the "layout" view model and set an alternate template $layout = $this->layout(); $layout->setTemplate('article/layout'); // Create and return a view model for the retrieved article $view = new ViewModel(array('article' => $article)); $view->setTemplate('content/article'); return $view; } }
通常来说,你可能需要在当前的模型上改变布局。这个需求(a)检查在路由中匹配到的控制器是否属于这个模块,(b)改变视图模型的模板。
在一个监听器里做这些操作。它应该在低(负)的优先级上监听“route”事件,或在任何优先级上监听“dispatch”事件。
namespace Content; class Module { /** * @param \Zend\Mvc\MvcEvent $e The MvcEvent instance * @return void */ public function onBootstrap($e) { // Register a dispatch event $app = $e->getParam('application'); $app->getEventManager()->attach('dispatch', array($this, 'setLayout')); } /** * @param \Zend\Mvc\MvcEvent $e The MvcEvent instance * @return void */ public function setLayout($e) { $matches = $e->getRouteMatch(); $controller = $matches->getParam('controller'); if (false === strpos($controller, __NAMESPACE__)) { // not a controller from this module return; } // Set the layout template $viewModel = $e->getViewModel(); $viewModel->setTemplate('content/layout'); } }
Zend\View\View 确实非常少,它的工作流程本质上来说就是 ViewEvent,它触发了两个事件“renderer”和 “response”。你可以分别地使用 addRenderingStrategy() 和 addResponseStrategy() 方法,在这些事件上附加“策略”。一个渲染策略考察请求对象(或者任何其它条件)为了选择一个渲染器(或者不选择)。一个响应策略确定如何填充基于渲染返回的响应。
ZF2 提供了三种渲染和响应策略,你可以在你的应用程序中使用他们。
默认情况下,只有 PhpRendererStrategy 是注册的,意思是说如果你需要使用其它的策略你需要自己注册。此外,这意味着你有可能会想在高优先级的情况下注册它们,以确保它们在 PhpRendererStrategy 之前得到匹配。以下是一个例子,让我们注册一个 JsonStrategy
namespace Application; class Module { /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return void */ public function onBootstrap($e) { // 在高优先级(在视图尝试渲染之前执行)注册一个渲染事件 $app = $e->getApplication(); $app->getEventManager()->attach('render', array($this, 'registerJsonStrategy'), 100); } /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return void */ public function registerJsonStrategy($e) { $app = $e->getTarget(); $locator = $app->getServiceManager(); $view = $locator->get('Zend\View\View'); $jsonStrategy = $locator->get('ViewJsonStrategy'); // 附加策略,在高优先级中哪个是监听器集合 $view->getEventManager()->attach($jsonStrategy, 100); } }
如果你要在指定的模块(Module)或者指定的控制器(Controller)中注册该怎么做呢?一种方法是类似于在布局(Layout)章节中最后一个例子,我们在指定的模块(Module)中详细的改变布局(Layout)代码:
namespace Content; class Module { /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return void */ public function onBootstrap($e) { // 注册一个渲染事件 $app = $e->getParam('application'); $app->getEventManager()->attach('render', array($this, 'registerJsonStrategy'), 100); } /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return void */ public function registerJsonStrategy($e) { $matches = $e->getRouteMatch(); $controller = $matches->getParam('controller'); if (false === strpos($controller, __NAMESPACE__)) { // 不是一个来自于模块的控制器 return; } // 可能地,你可以在这里有更多有选择性的测试指定的控制器类,指定的action或者请求方法 // 当来自于这个模块的控制器被选择时设定JSON策略 $app = $e->getTarget(); $locator = $app->getServiceManager(); $view = $locator->get('Zend\View\View'); $jsonStrategy = $locator->get('ViewJsonStrategy'); // 附加策略,在高优先级中哪个是监听器集合 $view->getEventManager()->attach($jsonStrategy, 100); } }
如果你想使用自定义的渲染器该怎么做呢?或者如果你的应用允许 JSON,Atom 供稿(feed)和 HTML 综合在一起该怎么做呢?在这里,你要创建你自己的自定义的策略。下面是一个通过 HTTP Accept 头进行适当的循环以及选择适当的基于第一个匹配的渲染器的例子
namespace Content\View; use Zend\EventManager\EventManagerInterface; use Zend\EventManager\ListenerAggregateInterface; use Zend\Feed\Writer\Feed; use Zend\View\Renderer\FeedRenderer; use Zend\View\Renderer\JsonRenderer; use Zend\View\Renderer\PhpRenderer; class AcceptStrategy implements ListenerAggregateInterface { protected $feedRenderer; protected $jsonRenderer; protected $listeners = array(); protected $phpRenderer; public function __construct( PhpRenderer $phpRenderer, JsonRenderer $jsonRenderer, FeedRenderer $feedRenderer ) { $this->phpRenderer = $phpRenderer; $this->jsonRenderer = $jsonRenderer; $this->feedRenderer = $feedRenderer; } public function attach(EventManagerInterface $events, $priority = null) { if (null === $priority) { $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer')); $this->listeners[] = $events->attach('response', array($this, 'injectResponse')); } else { $this->listeners[] = $events->attach('renderer', array($this, 'selectRenderer'), $priority); $this->listeners[] = $events->attach('response', array($this, 'injectResponse'), $priority); } } public function detach(EventManagerInterface $events) { foreach ($this->listeners as $index => $listener) { if ($events->detach($listener)) { unset($this->listeners[$index]); } } } /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return \Zend\View\Renderer\RendererInterface */ public function selectRenderer($e) { $request = $e->getRequest(); $headers = $request->getHeaders(); // 没有Accept header? 返回 PhpRenderer if (!$headers->has('accept')) { return $this->phpRenderer; } $accept = $headers->get('accept'); foreach ($accept->getPrioritized() as $mediaType) { if (0 === strpos($mediaType, 'application/json')) { return $this->jsonRenderer; } if (0 === strpos($mediaType, 'application/rss+xml')) { $this->feedRenderer->setFeedType('rss'); return $this->feedRenderer; } if (0 === strpos($mediaType, 'application/atom+xml')) { $this->feedRenderer->setFeedType('atom'); return $this->feedRenderer; } } // 没有匹配到任何东西;返回 PhpRenderer。严格来说,我可能要返回一个 HTTP 415 不支持响应 return $this->phpRenderer; } /** * @param \Zend\Mvc\MvcEvent $e MvcEvent 实例 * @return void */ public function injectResponse($e) { $renderer = $e->getRenderer(); $response = $e->getResponse(); $result = $e->getResult(); if ($renderer === $this->jsonRenderer) { // JSON 渲染器; 设定 content-type header $headers = $response->getHeaders(); $headers->addHeaderLine('content-type', 'application/json'); } elseif ($renderer === $this->feedRenderer) { // Feed 渲染器; 设定 content-type header,如果有必要刻意输出 feed $feedType = $this->feedRenderer->getFeedType(); $headers = $response->getHeaders(); $mediatype = 'application/' . (('rss' == $feedType) ? 'rss' : 'atom') . '+xml'; $headers->addHeaderLine('content-type', $mediatype); // 如果 $result 是一个 feed,就输出它 if ($result instanceof Feed) { $result = $result->export($feedType); } } elseif ($renderer !== $this->phpRenderer) { // 不是我们所支持的渲染器,因此没有我们的策略,返回 return; } // 注入内容 $response->setContent($result); } }