ZendFramework2-秘籍-全-

ZendFramework2 秘籍(全)

原文:zh.annas-archive.org/md5/679d6eca2d8751050b9b962f44fe55f2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

几年前,我的一个朋友向我介绍了 Zend Framework 1,从那时起我就成了它的粉丝。尽管第一个框架是一个真正的庞大框架,文档也不多,但我感觉框架的第二版有了很大的改进。功能丰富的工具箱,使这个框架(就我个人而言)成为最好的工作框架之一。

但正如我们可能都知道的,由于其功能极其丰富,学习曲线可能看起来非常陡峭。这就是为什么我觉得有必要将框架的所有重要部分写成小块,这样就不会让那些想要学习它的人(在这种情况下是你)感到不知所措。

这本书涵盖的内容

第一章, Zend Framework 2 基础,讲述了我们如何设置一个小型应用程序并运行它。依赖注入和配置也得到了处理。

第二章, 翻译和邮件处理,解释了国际化和本地化的重要性,以及在我们应用程序中发送和接收邮件的整体处理。

第三章, 处理和装饰表单,展示了如何创建表单。之后,它讨论了表单的过滤、验证和装饰。

第四章, 使用视图,涵盖了框架中将要讨论的最重要的部分之一,即视图的设置和渲染。

第五章, 配置和使用数据库,提供了数据库的配置和解释,这将使我们深入了解如何在我们的应用程序中充分利用它们。

第六章, 模块、模型和服务,主要讨论了模块是如何构建的,模型可以被激活,以及服务是如何定义的。

第七章, 处理身份验证,深入探讨了不同的用户认证方式以及我们如何创建自己的认证方法。

第八章, 优化性能,讨论了缓存的使用以及可用于缓存输出、操作码的方法,以及如何使用插件。

第九章, 捕获错误,教我们如何调试应用程序,处理异常,以及记录信息。

附录, 设置基本要素,展示了我们可以在哪里找到文档,如何设置开发环境,以及展示了 composer 的工作原理。

你需要这本书的内容

为了最好地跟随本书,我建议使用基于 Linux 的 Web 服务器,因为大多数食谱都是面向 Linux 的,而不是 Windows。如果您是 Windows 用户,您可能最好安装一个带有 Linux 的虚拟机,或者安装一个 Zend Server 社区版,以确保您的机器兼容于 Zend Framework 2 开发(您也可以不使用 Zend Server 这样做,但这会更方便)。

本书面向对象

Zend Framework 2 烹饪秘籍》是为那些在 PHP 编程方面相当先进的 PHP 开发者准备的。对于那些对超出简单脚本页面组合的边界扩展知识有浓厚兴趣的开发者来说,本书也将非常有用。由于本书将讨论单元测试和 MVC,因此了解这些技术对读者来说是有益的,尽管开发应用程序的经验并非必不可少。

习惯用法

在本书中,您将找到多种文本样式,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"我们可以通过使用getFlags()方法,或者使用hasFlag()方法从消息中获取标志。"

代码块设置如下:

<?php
echo $this->dateFormat(
    // Format the current UNIX timestamp.
    time(),

    // Our date is to be a LONG date format.
    IntlDateFormatter::LONG,

    // We want to omit the time, defining this is 
    // optional as the default is NONE.
    IntlDateFormatter::NONE

);

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

SampleModule/
  config/
    module.config.php
 language/
  src/
    SampleModule/
      Controller/
        IndexController.php
  view/
    samplemodule/
      index/
        index.phtml
  Module.php

任何命令行输入或输出都如下所示:

php composer.phar update

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"在我们已经做了所有能做的事情之后,点击确定,您可以选择保存我们的文件的位置。"

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小技巧和技巧如下所示。

读者反馈

我们欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题领域有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com购买的所有 Packt 书籍的账户中下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的现有勘误列表中,在“勘误”部分下。您可以通过从www.packtpub.com/support选择您的标题来查看现有勘误。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和提供有价值内容方面的帮助。

问题

如果您在本书的任何方面遇到问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决。

第一章。Zend Framework 2 基础知识

本章我们将涵盖:

  • 设置 Zend Framework 2 项目

  • 处理程序

  • 理解依赖注入

  • 利用配置来获得好处

  • 事件管理器和 Bootstrap 类

简介

在本章中,我们将从头开始介绍一个基本的 Zend Framework 2 应用程序,从下载、设置到运行。如果您不熟悉 Zend Framework 2 的工作原理以及最佳安装方式,可以使用本章作为参考。在章节的后续部分,我们将通过查看依赖注入DI)以及它如何帮助我们更高效地编码来对框架进行更深入的了解。最后,我们将更详细地介绍配置选项、EventManagerModuleManager

设置 Zend Framework 2 项目

没有什么比在我们的最爱框架中设置一个新的项目更令人兴奋的了。每次我们开始一个新的项目,我们都是从一张白纸开始。

准备工作

在您设置新的 Zend Framework 2 应用程序之前,您需要确保您已准备好以下项目:

  • 一个运行 PHP 版本 5.3.3 或更高版本的 Apache 网络服务器,您可以通过网络浏览器访问它

  • Git

如果您没有准备好上述所有项目,您最好在继续阅读本章内容之前,阅读本食谱中提到的也见部分(我们本章中解释的每个主题都称为食谱)。

我们假设将在基于 Linux 的平台和 Apache 2 网络服务器上使用 Zend Framework 2;这意味着命令可能不会在 Windows 平台上直接工作。然而,Windows 用户可以通过在虚拟机上安装 Linux 来设置虚拟机,以充分利用本书。

要在 Windows 上安装虚拟机,我们可以使用一个名为 Oracle VM VirtualBox 的应用程序,它是免费提供的。我们可以访问www.virtualbox.org下载并安装 VirtualBox 的最新版本,我们还可以访问 VirtualBoxes (virtualboxes.org/images/ubuntu)并从那里下载一个预配置的虚拟机。

在 VirtualBoxes 网站上,我们只需点击列表中最新的 Ubuntu(Linux 的一个发行版)链接,请注意那里显示的用户名和密码,因为我们稍后会需要它们来登录。一旦下载了镜像,就可以按照 VirtualBoxes 网站上可找到的文档说明将其准备好(virtualboxes.org/doc/register-and-load-a-downloaded-image)。

假设图像已导入,我们可以轻松启动虚拟机并输入与下载的虚拟机一起提供的用户名和密码。

登录到虚拟机后,我们需要确保 Git 已安装,这可以通过输入以下命令轻松完成(请注意,美元符号是命令提示符,而不是我们需要输入的命令):

$ sudo apt-get install git

如果 Git 未安装,系统将提示您安装 Git,这可以通过按Y键,然后按Enter键来完成;另一方面,如果 Git 已经安装,则不会进行任何操作,并告诉您它已经安装。

如何做到这一点…

首先,我们需要 Zend Framework 2 骨架,这样我们就可以轻松地创建一个新项目。骨架是一个模板结构,可以用来开始使用应用程序进行开发,在这种情况下,它为我们创建了一个在 Zend Framework 中开发的模板。幸运的是,这样做相对简单,几乎从不引起任何问题,当它发生时,通常与 Git 无法检索代码有关。当 Git 无法检索骨架时,请确保命令中没有拼写错误,并且 Git 有外部访问权限(我们可以通过输入ping Github.com并查看是否收到响应来测试这一点)。

我们将要使用的方法称为克隆,通过名为 Git 的版本控制系统。克隆源代码将确保我们总是获取开发者(在这种情况下是 Zend 本身)已上传的最新版本。

克隆骨架

我们可以通过以下命令克隆骨架——实际上,在 GitHub 上几乎可以克隆任何东西:

$ git clone git://github.com/zendframework/
 ZendSkeletonApplication.git

移动骨架

完成后,我们可以进入新创建的名为ZendSkeletonApplication的文件夹,并将里面的所有内容复制粘贴到我们的 Web 服务器文档根目录。在 Linux 系统中,这通常是/var/www(当我们使用 Zend Server 时也是如此,如附录中所述,设置基本要素)。我们可以通过输入以下命令来完成此操作:

$ cd ZendSkeletonApplication
$ mv ./* /var/www –f
$ cd /var/www

初始化 Composer

在所有内容复制完毕后,我们将通过输入以下命令来初始化项目:

$ php composer.phar install

现在,PHP 的命令行界面CLI)执行composer.phar,在这个例子中,它将下载并安装 Zend Framework 2 库,并为我们设置一个简单项目,以便我们能够工作。

此命令可能需要很长时间才能成功执行,因为 Composer 需要在告诉您 Zend Framework 2 已准备好使用之前完成很多事情,我们在这里不会深入讨论 Composer 的工作原理,因为它已经在附录中讨论过,设置基本要素

一旦完成此命令,我们需要确保我们的 Web 服务器文档根已更改以匹配骨架布局。通常的做法是,Zend Framework 2 使用public文件夹作为应用程序的主要着陆点。Zend Framework 2 骨架的结构允许我们将用户绑定到public文件夹,同时所有逻辑都安全地位于公共区域之外。

从本质上讲,这意味着我们需要首先使用public文件夹rootjailWeb 服务器,然后我们才能真正看到我们刚刚安装的内容。我们想要rootjailWeb 服务器,因为我们不希望外部世界过度滥用我们的 Web 服务器,而rootjail确保 Web 服务器本身无法访问除其被jail的文件夹之外的其他任何文件夹,从而使我们的服务器更加安全。

在我个人的情况下,这意味着更改 Apache 2 配置。在大多数基于 Linux 的系统上,它将是 Apache Web 服务器为我们提供 Web 请求。

最简单的事情就是找到您的 Web 服务器配置(通常位于/etc/apache2),并将 DocumentRoot 追加为/public。对我来说,这将更改文档根从/var/www/var/www/public

小贴士

如果您使用 Apache,您需要检查AllowOverride设置是否设置正确,这可以在与文档根相同的部分找到,并应反映以下内容:

AllowOverride FileInfo

最后,我们需要重新启动 Apache Web 服务器,如果您以 root 用户登录,可以通过以下命令完成,或者通过在命令前添加sudo来调用它,这告诉服务器我们想要以超级用户身份执行它。

$ apache2ctl restart

现在,我们能够检查我们的浏览器并查看我们实际上做了什么。我们现在只需使用 Web 浏览器输入 URL 访问创建的项目,在我的情况下,这将如下所示:

http://localhost/

这将导致以下屏幕:

初始化 Composer

恭喜,您现在已设置了一个基本的 Zend Framework 2 应用程序。

它是如何工作的…

在基本 Zend Framework 2 骨架工作正常后,安装 ZFTool 是完美的时机。ZFTool 是一个实用模块,当我们需要列出项目中的当前模块、添加新模块或设置新项目时,它非常有用。它还包含一个极其有用的类映射生成器,我们可以在 Zend Framework 2 的某些更高级区域中使用它。

我们可以通过以下命令安装此实用程序:

$ cd /var/www
$ mkdir -p vendor/zftool
$ cd vendor/zftool
$ wget https://packages.zendframework.com/zftool.phar

虽然我们已通过 composer 设置了 Zend Framework 2 骨架,但向您展示如何轻松通过 ZFTool 设置新项目可能很有趣。

$ cd /var/www
$ php vendor/zftool/zftool.phar create project new-project

上述命令将在/var/www/new-project文件夹中创建一个新的 Zend Framework 2 骨架项目。这反过来意味着我们新项目的文档根应设置为/var/www/new-project/public

要完成新项目中 Zend Framework 2 应用程序,我们只需进入新项目目录并执行以下命令:

$ cd new-project
$ php composer.phar install

ZFTool 的另一个实用命令是在我们的项目中创建和显示模块。ZFTool 可以轻松显示我们当前使用的模块列表(对于大型应用程序,我们往往容易失去对模块的视线),以及为我们应用程序创建新骨架模块的能力。要查看我们应用程序中当前使用的模块列表,我们可以使用以下命令:

$ php ../vendor/zftool/zftool.phar modules

要在基于/var/www/new-project目录的项目中创建一个名为wow-module的新模块,我们可以使用以下命令:

$ php ../vendor/zftool/zftool.phar create module wow-module
 /var/www/new-project

提供应用程序的路径是可选的,但如果我们在同一台机器上使用多个项目,最好确保我们有正确的项目路径。

现在是最后一条,也可能是 ZFTool 中最有用的一条命令,即类映射生成器。类映射文件是一个包含项目所有类及其相应路径的文件,这使得 PHP 自动加载器加载类文件变得更容易。通常类文件位于我们知道的路径中,这会导致自动加载器实际上需要搜索文件,从而产生小的延迟。然而,使用类映射文件,情况并非如此,因为自动加载器可以立即找到所需的文件。

类映射在 Zend Framework 2 中是一个大问题,因为不良的类映射可以使一个良好的应用程序运行得非常慢,而且为了完全公平,Zend Framework 2 可以使用它所能获得的所有速度。

类映射生成器的作用是创建一个包含所有可自动加载的类和路径的文件。这样我们就不必担心类的位置。

要生成新的类映射文件,我们可以使用以下命令:

$ php zftool.phar classmap generate <directory> <file> -w

命令要求我们提供两个不同的参数:

  • <directory>:需要索引类的目录。例如,这可以是添加到vendor目录的新库。

  • <file>:这是 ZFTool 需要生成的类映射文件。我们的 Zend Framework 2 自动加载器需要拾取此文件,因此我们需要确保 ZFTool 可以找到该文件。如果您没有指定文件,它将在当前工作目录中创建一个名为autoload_classmap.php的文件。

大多数情况下,如果您想追加而不是覆盖类映射文件,这是必要的,如果您想追加,只需将-w改为-a

类映射文件的示例是vendor/composer目录中的autoload_namespaces.php文件,它看起来有点像这样:

<?php
return array(
  // Every class beginning with namespace Zend\ will be 
  // searched in this specific directory
  'Zend\\' => array(
    __DIR__ . '/../zendframework/zendframework/library'
  ),
  'ZendTest\\' => array(
    __DIR__ . '/../zendframework/zendframework/tests'
  ),
);

小贴士

下载示例代码

您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

还有更多...

安装 ZFTool 也有其他方法,其中一些与使用 composer 一样简单,所以我们将介绍两种其他安装 ZFTool 的方法。这样我们就能获得最广泛的可选方案。

安装 ZFTool 的另一种方法是利用 git,从而从仓库本身克隆源代码。然而,这会获取当前的 master 版本,可能会有些 bug。

$ cd vendor
$ git clone https://github.com/zendframework/ZFTool.git
$ cd ZFTool
$ php ./zf.php

现在我们有了zf.php文件,可以像使用zftool.phar一样使用它。现在我们已经涵盖了安装 ZFTool 的所有不同选项。

参见

  • 附录中的确保你拥有所有需要的东西配方,设置基本要素

  • 附录中的下载 Zend Framework 2 及其文档配方,设置基本要素

  • 附录中的作曲家及其在 Zend Framework 2 中的用途配方,设置基本要素

  • Apache 网络服务器 apache.org/

  • PHP 网站 php.net

处理例程

一个重要方面(如果不是最重要的方面)是 Zend Framework 2 中的路由。在其最基本的形式中,路由告诉框架用户应该如何从页面 A 到达页面 B,以及到达那个目的地需要做什么。这就是为什么我们通常认为这是如果你是初学者的话,理解的最重要部分。

如何做到这一点...

要定义一个路由,我们只需进入一个配置文件,并将路由配置添加到那里。

设置路由

让我们看看我们的简单(Segment)配置如下(文件:/module/Application/config/module.config.php):

return array(
  // Here we define our route configuration
  'routes' => array( 

    // We give this route the name 'website'
    'website' => array( 

      // The route type is of the class:
      // Zend\Mvc\Router\Http\Segment
      'type' => 'segment', 

        // Lets set the options for this route
        'options' => array( 

          /*
            The route that we want to match is /website
            where we can optionally add a controller name
            and an action name. For example:
              /website/index/index
          */ 
          'route' => '/website[/:controller[/:action]]',

          /*
            We don't want to accept everything, but this
            regex makes sure we only accept alpha-
            numeric characters and a dash and underscore.

            In our instance we want to check this for the
            action and the controller.
          */
          'constraints' => array( 
            'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
            'action' => '[a-zA-Z][a-zA-Z0-9_-]*'
          ),

          /*
           We want to make sure that if the user only 
           types /website in the URL bar it will actually
           go somewhere. We defined that here.
          */
          'defaults' => array( 
            'controller' => 'Website\Controller\Index', 
            'action' => 'index'
          ),
        ),
      ),
    ),
  ),
);

使用这个基本配置,我们可以轻松地在我们的应用程序中定义路由,在这个例子中,我们已经配置了一个响应/website URL 的路由。当我们访问/website URL 时,我们默认会被路由到Website\Controller\Index::indexAction。如果我们使用路由/website/another/route,我们会被路由到Website\Controller\Another::routeAction,因为我们已经定义了控制器和动作可以解析在后面。如果我们省略路由路径并输入/website/another,我们会重定向到Website\Controller\Another::indexAction,因为这是框架默认使用的。

上述示例只有一个真正的主要缺点,那就是当我们决定在配置中使用匿名函数来创建更多动态路由时,我们无法缓存路由,因为闭包不能被缓存序列化。

然而,还有一种声明路由的方法,那就是在代码中。在代码中创建路由功能的需求(显然每个人都有自己的原因和需求)可能是因为我们想在稍后的阶段缓存配置(例如,因为我们不能缓存匿名函数)或者当我们想从数据库中动态加载路由时。

让我们看看/module/Application/Module.php的例子:

<?php

// We are working in the Application module
namespace Application;

// Our main imports that we want to use
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

// Define our module class, this is always 'Module', but 
// needs to be specifically created by the developer.
class Module
{
  public function onBootstrap(MvcEvent $e)
  {
    // First we want to get the ServiceManager
    $sm = $e->getApplication()->getServiceManager();

    /*
      Say our logged in user is 'gdog' and we want
      him to be able to go to /gdog to see his profile.
    */
    $user = 'gdog';

    // Now get the router
    $router = $sm->get('router');

    // Lets add a route called 'member' to our router
    $router->addRoute('member', array(

      /*
        We want to make /$user the main end point, with 
        an optional controller and action.
      */
     'route' => '/'. $user. '[/:controller[/:action]]',

      /*
        We want a default end point (if no controller
        and action is given) to go to the index action
        of the index controller.
      */
      'defaults' => array( 
        'controller' => 'Member\Controller\Index', 
        'action' => 'index' 
      ), 

      /*
        We only want to allow alphanumeric characters
        with an exception to the dash and underscore.
      */
      'constraints' => array( 
          'controller' => '[a-zA-Z][a-zA-Z0-9_-]*', 
          'action' => '[a-zA-Z][a-zA-Z0-9_-]*' 
      ), 
    ));
  }
}

自然地,有更多添加路由的方法,但前述代码中提到的添加路由的方法展示了动态添加路由的巧妙方式。我们在那里创建的是,每当 Gdog 去他的个人资料时,他可以简单地输入http://example.ext/gdog并最终到达他的个人资料页面。

更令人惊奇的是,如果我们的朋友 Gdog 想看看他的朋友,他只需输入例如http://example.ext/gdog/my/friends,这将解析到Member模块,然后转到My控制器,最后执行Friends操作。

使用 SimpleRouteStack

这个路由栈——正如其名所示——是最简单的路由器之一,基本上是一个包含路由的列表,这些路由被解析以查看哪个路由匹配。默认情况下,这种类型的路由器在 Zend Framework 2 中不被使用。一般规则是,如果我们想添加一个具有高优先级的路由,我们给它一个高的索引号,例如 100 或 200。如果我们想给路由一个非常低的优先级,我们会给它一个索引号,例如 5 或 10。

当我们有非常具体的路由(通常具有高优先级)和不太具体的路由(低优先级)时,给路由分配优先级很有用。如果我们,例如,想让/website/url重定向到一个完全不同的模块、控制器和操作,但不影响其他网站路由,我们需要给/website/url路由一个更高的优先级,这样当它被找到时,它就不会搜索低优先级的路由。

如果我们不小心颠倒了优先级,我们会发现我们的/website/url总是重定向到包含所有/website路由的路由。

SimpleRouteStack使用Zend\Mvc\Router\PriorityList类来管理其路由优先级。

在我们想要开始创建应用程序之前,我们需要考虑路由,因为当应用程序增长时,如果我们没有事先考虑“如何路由”,我们可能会遇到路由问题。因此,在我们编码路由之前“绘制”应用程序的“网站地图”是明智的,以确保我们有正确的路由列表,并且没有创建任何冲突的路由。

SimpleRouteStack类定义了一些对我们非常有用的方法:

  • getRoute($name) / getRoutes($name): 这将检索当前路由——如果提供了名称——或者检索我们定义在SimpleRouteStack中的路由。如果我们不确定我们定义了哪些路由,这是一个首先检查的好地方。

addRoute($name, $route, $priority) / addRoutes($routes): 我们可以通过简单地将它添加到这个方法中来使用它来添加一个新的路由或一组路由到我们的路由类型。一个路由需要一个 nameroute(可以是字符串或 RouteInterface 的实例)以及如果我们想要一个优先级,我们可以将其作为第三个参数提供。

hasRoute($name): 如果我们想要检查是否存在特定的路由,我们可以通过其 name 来搜索,并找出它是否存在。

  • removeRoute($name): 当我们厌倦了一个路由时,我们可以简单地给出它的名字,并将其从列表中删除。如果我们想要例如在用户登录后重定向到 /user 而不是 /login,这可能会特别有用。

  • SimpleRouteStack: 没有多个具有相同优先级的路由的功能。如果已经定义了一个具有优先级的路由,它将优先考虑最后添加的路由作为具有最高优先级的路由。

使用 TreeRouteStack

路由器不仅限于使用 URI 路径来确定如何路由请求。它们还可以使用其他信息,如查询参数、头部、方法或主机名来找到匹配项。

它是如何工作的...

在 Zend Framework 2 中,我们通常会使用基于请求 URI 的路由,其中包含应查询的路径段。路由是通过路由器匹配的,路由器利用 RouteStack 来找到与路由器查询相匹配的路由。我们使用 RouteStack 是因为我们想要一种管理不同路由的合理方式。在 Zend Framework 2 中提供了大量的路由类型,但只有两种无味的路由器,即 SimpleRouteStackTreeRouteStack

当我们定义一个路由器时,我们需要确保我们理解它是如何工作的。虽然创建不同路径的列表足够简单,但记住,Zend Framework 2 的路由器通常使用 后进先出LIFO)的概念,这意味着经常使用的路由会被最后注册,而较少使用的路由会在路由器堆栈中较早注册。

更多内容...

除了两种标准路由类型之外,Zend Framework 2 还提供了一系列更专门针对互联网导航或甚至通过控制台的路由类型。

命名空间 – Zend\Mvc\Router\Http

一套出色的 HTTP 路由器可以在 Zend\Mvc\Router\Http 命名空间中找到,我们将快速浏览一下这个命名空间中存在的不同类。

主机名类解释

Zend\Mvc\Router\Http\Hostname 命名空间将尝试将其路由与配置中定义的主机名匹配。例如,如果我们定义的路由是 something.example.ext,我们的路由器将基于完整的 URL 做出路由决策。但是,如果我们在这个相同路由的开始处添加一个单冒号,例如::something.example.ext,那么路由器将基于 something 变量进行路由,这个变量可以是 aardvark.example.extzyxt.example.ext 之间的任何内容。

Literal 类的解释

Zend\Mvc\Router\Http\Literal 类将直接匹配我们给出的路径。例如,如果我们把一个路由放在那里,比如 /grouphug,那么这个路由将只解析到那个 URL,而不会是其他任何东西。

解释的方法

当我们想要匹配 HTTP 方法而不是段或路径时,使用 Zend\Mvc\Router\Http\Method 类。例如,可以是 POSTDELETE 等等。在 Zend Framework 2 中,这个方法也被称为 verb,这意味着在添加路由时,它请求一个 verb 参数而不是 route 参数,这是一种创建 RESTful API 的绝佳方式。

Part 类的解释

Zend\Mvc\Router\Http\Part 类用于描述我们的路由配置中的 child_routes。这意味着——尽管它从未被直接使用——我们可以定义 /user/profile 被重定向到使用 UserController,并执行 profile 动作。

让我们考虑以下配置:

return array(
  // We begin our router configuration
  'router' => array(

    // Define our routes 
    'routes' => array(

      // We are defining a route named 'Example'
      'Example' => array(
        'type' => 'Literal',
        'options' => array(

           /*
            This route will resolve to /recipe 
            which will resolve to the Example 
            module's IndexController and execute 
            the IndexAction.
           */
           'route' => 'recipe',
           'defaults' => array(
             '__NAMESPACE__' => 'Example\Controller',
             'controller' => 'Index',
           ),
         ),

         'may_terminate' => true,

          /*
            Here we begin to define our Part route, 
            which always begins with the 
            'child_routes' configuration.
          */
          'child_routes' => array(
            'client' => array(
              'type' => 'Literal',
              'options' => array(

              /*
                This child route (or Part) 
                will resolve to /recipe/foo       
                and will call the fooAction in  
                the IndexController.
              */
              'route' => '/foo',
              'defaults' => array(
               'action' => 'fooAction'
              ),
            ),
          ),
        ),
      ),
    ),
  ),
);

正则表达式解释

当我们需要匹配 HTTP 方法而不是段或路径时,使用 Zend\Mvc\Router\Http\Regex 类。例如,可以是 POSTDELETE 等等。在 Zend Framework 2 中,这个方法也被称为 verb,这意味着在添加路由时,它请求一个 verb 参数而不是 route 参数,这是一种创建 RESTful API 的绝佳方式。

让我们考虑以下示例:

// We begin our router configuration
'router' => array(

  // Define our routes 
  'routes' => array(

    // We are defining a route named 'Archive'
    'Archive' => array(
      'type' => 'Literal',
      'options' => array(

        /*
          This route will resolve to /archive 
          which will resolve to the Archive 
          module's IndexController and execute 
          the IndexAction.
        */
        'regex' => '/archive/(?<id>[a-zA-Z0-9_-
  ]+)(\.(?<format>(html|xml)))?',
        'defaults' => array(
            '__NAMESPACE__' => 'Archive\Controller',
            'controller' => 'Index',
            'action' => 'indexAction',
            'format' => 'html',
        ), 
        'spec' => '/archive/%id%.%format%',
      ),
    ),
  ),
),

在前面的示例中,重要的是要注意 /archive/%id%.%format% 告诉我们,在我们的 indexAction 方法中我们将接收两个参数,即 idformat

Scheme 类的解释

Zend\Mvc\Router\Http\Scheme 类始终使用 defaults 参数,并且只接受另一个参数,该参数称为 scheme,并且只能包含以下选项之一,即 httphttpsmailto

Segment 类的解释

Zend\Mvc\Router\Http\Segment 类可能是我们最常用的路由器之一,因为我们可以通过使用例如 /:controller/:action 这样的方式动态定义任何模块的路由和控制器,这种路由方式通过冒号分隔很容易识别。我们可以通过仅配置使用字母数字字符或其他我们希望使用的定义来为段定义任何 constraints

如何做... 部分的第一个示例中给出了 Segment 的一个示例。

理解依赖注入

当我们谈论依赖注入,简称 DI 时,我们谈论的是在需要时,例如在初始化时向对象或方法中注入数据,这些对象或方法在用后要么修改要么销毁对象。DI 可能是 Zend Framework 2 中最复杂的功能之一。不幸的是,由于 DI 在调试和性能上的过度复杂性以及服务定位器(在第六章中解释,模块、模型和服务),DI 可能不是最好的工具。然而,尽管它不是最好的工具,我们仍然必须努力去学习它,因为一旦掌握,它可能证明是一个非常强大的工具,可以创建非常易于维护的代码。

如果我们遇到需要我们在类中输入大量参数的情况,因为代码中更深层的对象依赖于它们,这可能是我们在甚至最专业环境中能找到的最令人烦恼且难以维护的代码片段。我们需要主要考虑在应用程序中多次使用且总是需要重新实例化的对象。

如何实现...

让我们看看以下示例,并假设FirstClass是我们将在代码中进一步需要的唯一类:

namespace OneNamespace
{
  class FirstClass 
  {
    private $secondClass;
    public function __construct(SecondClass $secondClass)
    {
      $this->secondClass = $secondClass;  
    } 
  }

  class SecondClass 
  {
    private $thirdClass;
    private $vehicle;
    public function __construct(ThirdClass $thirdClass, $vehicle)
    {
      $this->thirdClass = $thirdClass;
      $this->vehicle = $vehicle;
    }
  }
}

namespace AnotherNamespace 
{
  class ThirdClass 
  {
    private $first_name;
    private $last_name;

    public function __construct($first_name, $last_name)
    {
      $this->first_name = $first_name;
      $this->last_name = $last_name;
    }
  }
}

// Let us now create the example through the classic 
// method.
$thirdClass = new AnotherNamespace\ThirdClass("John", "Doe");
$secondClass = new OneNamespace\SecondClass($thirdClass, 
  'Motorcycle');
$firstClass = new OneNamespace\FirstClass($secondClass);

上述两个例子都给出了仅用于实例化另一个类或/和在阅读代码时增加复杂性的变量。尽管它们都是正确的,但在这个情况下,使用 DI 可以使这两个类的配置变得更加容易。

在调用时间初始化 DI

让我们看看这个 DI 示例,考虑到我们拥有与前面示例相同的类:

namespace OneNamespace
{
  class FirstClass 
  {
    [..] 
  }

  class SecondClass 
  {
    [..]  
  }
}

namespace AnotherNamespace 
{
  class ThirdClass 
  {
    [..]
  }
}

// Instead of configuring all the classes, we will now 
// simply configure the Di, and only instantiate the 
// class that we want to use.
$di = new \Zend\Di\Di();
$lister = $di->get(
    'OneNamespace\FirstClass',
    array(
        'first_name' => 'Jane',
        'last_name' => 'Doe',
        'vehicle' => 'Car',
    )
);

在前面的例子中,我们只是告诉 DI,AnotherNamespace\ThirdClass在其__construct方法中有两个参数。然后 DI 将利用Reflection来找出那里有哪些参数,然后将为任何在其构造函数中具有first_namevehiclelast_name参数的类提供该参数。

当然,我们在这里会看到一个潜在的缺陷,因为你可能需要利用多个实例化,可以假设在某个时刻会使用到相同的参数名称。在我们的例子中,如果另一个类也有一个$first_name参数但需要不同的输入,那么这将会引起问题,因为 DI 将简单地给出列表中的那个。

小贴士

如果我们使用依赖注入(DI)来实例化我们的类,并且我们只需要构造函数来设置变量,那么我们可以很容易地完全移除构造函数,因为 DI 不会使用构造函数来初始化变量。相反,DI 将只设置值的属性。

关于这一点的一个好处是,这种错误只会在我们使用 DI 在调用时间级别时发生,而不会在全局配置级别上,正如我们现在将要看到的。这就是为什么不建议在调用时间级别上使用 DI。

通过配置对象初始化 DI

我们还可以通过使用配置对象来创建更具体(或更准确)的对象初始化,并确保具有相同属性名的类不会发生冲突。

这个想法是,我们首先创建一个配置对象(或数组),它定义了哪些类需要设置哪些属性,然后使用它来初始化 DI,DI 反过来找出何时需要启动什么。

看看下面的例子,它展示了我们刚才解释的 exactly 东西:

<?php
// We are assuming that we are using the same classes as 
// in the previously shown examples.
namespace OneNamespace 
{
  class FirstClass 
  {
    [..] 
  }

  class SecondClass 
  {
    [..]  
  }
}

namespace AnotherNamespace 
{
  class ThirdClass 
  {
    [..]
  }
}

// After defining our classes we now begin to create our 
// configuration array which we will use to initialize 
// the DI.
$configuration = array(

  // We want to use this specific configuration at 
  // initialization of our class.
  'instance' => array(

    // We specify the class name to use here
    'SecondClass' => array(

      // We want to use this as a parameter
      'parameters' => array(

        // The property name to fill is vehicle.
        'vehicle' => 'Airplane'
      ),
    ),

    'FirstClass' => array(
      // Again we want to use this as a parameter
      'parameters' => array(

        // The property name to fill is first name and 
        //last name.
        'first_name' => 'Neil',
        'last_name' => 'deGrasse Tyson',
      ),
    ),
  ),
);

// We want to instantiate the Di\Configuration now.
use \Zend\Di\Configuration; 

$diConfiguration = new Configuration($configuration);

// Now instantiate the Di itself, with the configuration 
// attached.
$di = new \Zend\Di\Di($configuration);

// And to get the object we want to use, we just do the 
//same as before.
$firstClass = $di->get('OneNamespace\FirstClass');

为了让一切更加完美,我们只需将 DI 的Zend\Di\Configuration放在我们模块的引导程序中,这样我们就可以在整个命名空间中轻松使用它。这样我们就可以简单地将 DI 的配置放在我们的module.config.php中,让框架来处理它。

它是如何工作的…

DI 或依赖注入器是 Zend Framework 2 的一个重要特性,但大多数时候都被忽视了。DI 通过自动查找我们应用中需要的类,使我们的生活变得更加容易。

然而,尽管它很复杂,但也有一些特性我们应该小心。

DI 只提供一个对象的实例

这意味着每次get()调用都会导致相同的实例化重复进行。如果我们想要一个新的实例,我们需要调用newInstance(),因为 DI 实现了单例模式,这意味着每次我们调用get()方法时,所有数据都会持续存在,除非我们强制创建 DI 的新实例。

定义所有属性或使用完全合格(FQ)设置器参数

当我们的类具有比我们定义的更多的属性时,我们会发现 DI 将使用类中每个其他属性的最后一个值。当然,这是我们不希望的,如果我们自己编写了类,我们应该考虑重构配置和/或类。

然而,当没有其他方法可以定义正确的属性时,我们只能通过使用完全合格FQ)设置器参数来定义。

在我们的配置中,我们可以定义一个非常具体的属性名,例如,class::method:paramPos。如果我们以之前提到的ThirdClass为例,那么这将分别是ThirdClass::setFirstName:0ThirdClass::setLastName:0

还有更多…

在 Zend Framework 2 中,我们还可以学习更多关于 DI 的知识。以下列表提供了一个非常简短且紧凑的描述,介绍了其他有趣的 DI 组件:

  • RuntimeDefinition(默认)、CompilerDefinitionClassDefinition:这些定义用于确定如何配置我们的对象。尽管默认的一个通常可以完成工作,但看看其他两个定义做了什么是有好处的,因为它们都有其优缺点。

  • InstanceManager:用于定义配置,特别是AliasesParametersPreferences

利用配置的优势

配置在 Zend Framework 2 的工作中起着至关重要的作用,因此了解它是如何工作的至关重要。

如何做到这一点...

遍历以下部分,以利用配置:

创建全局配置

当开始使用 Zend Framework 2 编码时,对于不同的配置文件的作用存在一些误解。默认情况下,我们有多个配置文件,而且可能并不总是简单易懂地知道东西应该放在哪里。这就是为什么我们喜欢应用一个简单的规则:

注意

配置是否需要在所有模块中都是必要的?如果是,请将您的配置放在config/application.config.php文件中。如果不是,请将配置放在属于该模块的config/global.php文件中。

我们通常放在global.php文件中的配置,例如,可以是缓存方法和配置、数据库配置。通常我们希望放置与环境相关的项目,但没有任何安全敏感的信息。

让我们看看global.php的一个糟糕的例子:

<?php
return array(

  // We want to create a new database connection
  'db' => array(

    // The driver we want to use is the Pdo, our  
    // favorite
    'driver' => 'Pdo',

    // This is our connection url, defining a MySQL 
    // connection, with database 'somename' which is 
    // available on the localhost server.
    'dsn' => 'mysql:dbname=somename;host=localhost',

    // This is exactly what we should NOT do in this  
    // file, shame on you developer!
    'username' => 'terribleuser',
    'password' => 'evenworsepassword',
  ),

  // We need a database adapter defined as well, 
  // otherwise we can't use it at all.
  'service_manager' => array(
    'factories' => array(
      'Zend\Db\Adapter\Adapter' => 
  'Zend\Db\Adapter\AdapterServiceFactory',
    ),
  ),
);

global.php文件中放置用户名和密码是非常糟糕的做法。global.php文件应该放入我们的版本控制中,因此应该只包含全局运行应用程序所需的配置项,而不是与特定环境相关的特定信息,例如数据库用户名和密码。

创建仅适用于本地机器的配置

在 Zend Framework 2 中,超多配置文件的好处之一是您可以使用本地配置覆盖全局配置。这在开发过程中非常有用,当您发现自己处于配置与生产环境略有不同的情况时。

假设我们有一个如下的/config/autoload/global.php配置文件:

<?php
return array(

  // We want to create a new database connection
  'db' => array(

    // The driver we want to use is the Pdo, our  
    // favorite
    'driver' => 'Pdo',

    // This is our connection url, defining a MySQL 
    // connection, with database 'somename' which is 
    // available on the localhost server.
    'dsn' => 'mysql:dbname=somename;host=localhost',
  ),

  // We need a database adapter defined as well, 
  // otherwise we can't use it at all.
  'service_manager' => array(
    'factories' => array(
      'Zend\Db\Adapter\Adapter' => 
  'Zend\Db\Adapter\AdapterServiceFactory',
    ),
  ),
);

正如前一个例子所示,我们创建了一个很好的简单的 MySQL 数据库连接到我们位于本地的somename数据库。但作为优秀的开发者,我们并没有在这里定义用户名和密码。这就是/config/autoload/local.php文件的作用所在。

让我们看看我们的local.php可能的样子:

<?php
return array(
  'db' => array(
    'username' => 'awesomeuser',
    'password' => 'terriblepassword',
  ),
);

如果我们使用版本控制系统(请说“是”),我们不应该提交此文件,这不仅出于安全原因,而且因为这是一个本地配置文件,在实时系统中可能并不必要,因为我们会在该环境中创建一个新的具有正确细节的文件。

编辑您的 application.config.php 文件

如果我们查看我们的默认config/application.config.php文件,我们只设置了少量属性,但有很多内联注释,当我们不再记得某个属性的准确名称或描述时,这些注释非常有用。

在我们开发应用程序时,我们将更改最多的主要配置是modules属性。这个特定的属性是一个简单的数组,包含我们在应用程序中(并希望使用)的不同模块命名空间。默认情况下,它看起来有点像这样:

<?php 

return array(
  // This should be an array of module namespaces used 
  // in the application.
  'modules' => array(
    'Application',
  ),
[..]

当我们添加或删除模块时,此行也需要修改,甚至可以在开始新模块或删除模块之前修改此文件。原因很简单,当我们忘记在删除模块时修改此文件,访问应用程序时会在浏览器中生成500 – 应用程序错误。而且因为此配置文件在实例化早期就被读取,有时对于开发者来说很难确定为什么应用程序突然无法加载。

它是如何工作的…

如果我们查看public文件夹中的index.php文件,我们可以看到我们将初始配置文件通过以下行解析到 Zend Framework MVC 应用程序中:require 'config/application.config.php'。然后加载主配置文件,它反过来定义了所有我们的属性。

application.config.php文件中的一个巧妙属性是config_glob_paths属性。任何额外的配置文件默认情况下都会通过在config/autoload文件夹中查找文件来读取,使用一个非常具体的文件模式,即*global.php*local.php。定义的顺序也非常重要。

当我们说*global.php时,我们可以从somemodule.global.phpmenu.global.php,甚至只是global.php来定义任何内容,因为文件模式(也称为GLOB_BRACE)会搜索任何匹配的内容。对于*local.php也是如此。

如前所述,定义的顺序非常重要,因为我们希望全局配置在本地配置之前加载,否则覆盖全局配置就没有意义了,对吧?

还有更多…

总结配置文件:

  • config/application.config.php: 可以在此处添加和删除模块,并且非常底层的配置也在这里进行。

  • config/autoload/some-module.global.php: 用于覆盖模块配置的默认值。请确保不要在此处放置敏感信息,但主机名和数据库名称应该放在这里。

  • config/autoload/some-module.local.php: 您可以在此处放置您的用户名和密码以及其他非常特定于您本地环境的配置项。

  • module/SomeModule/config/module.config.php: 模块特定配置在此处进行,请仅使用默认值,并确保不要在此处输入过于具体的内容。

事件管理器和引导类

我们将展示 Zend Framework 2 最漂亮的功能之一:事件管理器。

如何操作…

EventManagerBootstrap类是我们应用程序的重要组成部分,这个配方主要介绍如何使用这两个工具:

使用引导

在我们的情况下,引导是模块的开始,每当请求一个模块时,它将使用位于 Module.php 文件中的 onBootstrap() 方法。虽然这个方法不是必需的,但我们通常希望在模块中包含这个方法,因为它是一个确保在进一步处理客户端请求之前某些实例已经存在或配置好的简单方法。

开始一个会话

会话是保存用户临时信息的绝佳方式。想想保存已登录用户的信息,或者他们在页面上浏览的历史记录。一旦我们开始创建应用程序,我们会发现自己会在会话中保存很多东西。

我们需要做的第一件事是修改 /module/Application/config/module.config.php 文件,并向其中添加一个名为 session 的新部分。假设我们有一个完全空的模块配置:

<?php
return array(
  'service_manager' => array(
     // These are the factories needed by the Service 
     // Locator to load in the session manager
    'factories' => array(
      'Zend\Session\Config\ConfigInterface' => 
  'Zend\Session\Service\SessionConfigFactory',
      'Zend\Session\Storage\StorageInterface' => 
  'Zend\Session\Service\SessionStorageFactory',
      'Zend\Session\ManagerInterface' => 
  'Zend\Session\Service\SessionManagerFactory',
    ),
    'abstract_factories' => array(
  'Zend\Session\Service\ContainerAbstractFactory',
    ),
  ),
  'session_config' => array(
    // How long can the session be idle for in seconds 
    // before it is being invalidated
    'remember_me_seconds' => 3600,

    // What is the name of the session (can be anything)
    'name' => 'some_name',
  ),
  // What kind of session storage do we want to use, 
  // only SessionArrayStorage is available at the minute
  'session_storage' => array(
    'type' => 'SessionArrayStorage',
    'options' => array(), 
  ),
  // These are session containers we can use to store 
  // our information in
  'session_containers' => array(
    'ContainerOne',
    'ContainerTwo',
  ),
);

就这样。会话现在可以在我们的控制器和模型中使用。我们现在创建了两个会话容器,我们可以使用它们来存储我们的信息。我们可以通过以下方式在任何有服务定位器可用的控制器或模型中访问这些容器(文件:/module/Application/src/Application/Controller/IndexController.php):

<?php

namespace Application;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractController
{
  public function indexAction()
  {
    // Every session container we define receives a 
    // SessionContainer\ prefix before the name
    $containerOne = $this->getServiceLocator()
  ->get('SessionContainer\ContainerOne');
  }
}

使用 EventManager 类

EventManager 类可能是框架中最好的特性之一。当正确使用时,它可以使我们的代码更加动态和易于维护,而不会产生乱麻般的代码。

它所做的事情相对简单,例如;一个类可能有一个名为 MethodA 的方法。这个 MethodA 有一个监听器列表,它们对这个类的结果感兴趣。当 MethodA 执行时,它只是运行其正常程序,完成后就通知 EventManager 发生了一个特定的事件。现在 EventManager 将触发所有感兴趣的相关方,而这些相关方将依次执行它们的代码。

明白了?如果你不明白,不要担心,因为这个示例代码可能会澄清一些问题(文件:/module/Application/src/Application/Model/SwagMachine.php):

<?php
// Don't forget to add the namespace
namespace Application\Model;

// We shouldn't forget to add these!
use Zend\EventManager\EventManager;

class SwagMachine
{
  // This will hold our EventManager
  private $em;

  public function getEventManager() 
  {
    // If there is no EventManager, make one!
    if (!$this->em) {
      $this->em = new EventManager(__CLASS__);
    }

    // Return the EventManager.
    return $this->em;
  }

  public function findSwag($id)
  {
    // Trigger our findSwag.begin event 
    // and push our $id variable with it.
    $response = $this->getEventManager()
                     ->trigger(
           'findSwag.begin', 
           $this,
           array(
             'id' => $id
           )
    );

    // Make our last response, the final 
    // ID if there is a response.
    if ($response->count() > 0)
      $id = $response->last();

    // ********************************
    // In the meantime important code 
    // is happening...
    // ********************************

    // ...And that ends up with the 
    // folowing return value:
    $returnValue = 'Original Value ('. $id. ')';

    // Now let's trigger our last 
    // event called findSwag.end and 
    // give the returnValue as a 
    // parameter.
    $this->getEventManager()
         ->trigger(
           'findSwag.end', 
           $this, 
           array(
             'returnValue' => $returnValue
           )
    );

    // Now return our value.
    return $returnValue;
  }
}

正如我们所看到的,我们创建了一个带有两个事件触发器的小类,分别是 findSwag.beginfindSwag.end,分别位于方法的开始和结束处。findSwag.begin 事件可能会修改 $id,而 findSwag.end 事件仅解析 returnValue 对象,无法修改其值。

现在我们来看看实现触发器的代码(文件:/module/Application/src/Application/Controller/IndexController.php):

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction() 
  {
    // Get our SwagMachine
    $machine = new SwagMachine();

    // Let's attach our first callback, 
    // which potentially will increase 
    // the $id with 10, which would 
    // make it result in 30!
    $machine->getEventManager()
            ->attach(
        'findSwag.begin',
        function(Event $e) 
        {
          // Get the ID from our findSwag() 
          // method, and add it up with 12.
          return $e->getParam('id') + 10;
        },
        200
    );

    // Now attach our second callback, 
    // which potentially will increase 
    // the value of $id to 60! We give 
    // this a *higher* priority then 
    // the previous attached event 
    // trigger.
    $machine->getEventManager()
            ->attach(
        'findSwag.begin',
        function(Event $e) 
        {
        // Get the ID from our findSwag() 
        // method, and add it up with 15.
        return $e->getParam('id') + 40;
      },
      100
    );

    // Now create a trigger callback 
    // for the end event called findSwag.end, 
    // which has no specific priority, 
    // and will just output to the screen.
    $machine->getEventManager()
            ->attach(
        'findSwag.end',
        function(Event $e) 
        {
          echo 'We are returning: '
             . $e->getParam('returnValue');
        }
    );

    // Now after defining the triggers, 
    // simply try and find our 'Swag'.      
    echo $machine->findSwag(20);
  }
}

正如我们所看到的,将触发器附加到事件上非常简单。如果事件得到适当的文档说明,当我们需要修改传入方法(如我们用 findSwag.begin 所做的那样)的参数或只是将结果输出到日志(如 findSwag.end)时,它们会很有用。

当我们查看屏幕上的内容时,它应该是这样的:

We are returning: Original Value (60)
Original Value (60)

结果是,最上面一行是来自findSwag.end触发器的输出,而值60来自最高优先级的触发器,即优先级为100的触发器(因为那被认为比200的优先级更高)。

更改视图输出

有时候,我们需要不同的视图输出,例如当我们需要构建自己的 REST 服务或 SOAP 服务时。虽然这可以通过控制器插件更简单地安排,但它是一个如何连接到dispatch事件并查看那里的示例。

不再赘述,让我们看一下以下代码片段:

Module.php:
namespace Application;

// We are going to use events, and because we use a MVC, 
// we need to use the MvcEvent.
use Zend\Mvc\MvcEvent;

class Module
{
  public function onBootstrap(MvcEvent $e)
  {
    // Get our SharedEventManager from the MvcEvent $e 
    // that we got from the method
    $sharedEvents = $e->getApplication()
                      ->getEventManager()
                      ->getSharedManager();

    // Also retrieve the ServiceManager of the 
    // application.
    $sm = $e->getApplication()->getServiceManager();

    // Let's propose a new ViewStrategy to our 
    // EventManager.
    $sharedEvents->attach(

        // We are attaching the event to this namespace 
        // only.
        __NAMESPACE__, 

        // We want to attach to this very specific 
        // event, the Dispatch event of our controller.
        MvcEvent::EVENT_DISPATCH, 

        // The callback function of the event, used when 
        // the event we attached to happens. In our 
        // callback we also want our local variable $sm 
        // to be available for use.
        function($e) use ($sm) 
        {
          // Get our alternate view strategy from the 
          // ServiceManager and attach the EventManager     
          // to the strategy.
          $strategy = $sm->get('ViewJsonStrategy');
          $view = $sm->get('ViewManager')->getView();
          $strategy->attach($view->getEventManager());
        }, 

        // We want to give this a priority, so this will 
        // get more priority.
        100
    );
}

如我们所见,将回调函数附加到EventManager对象相对简单。在这个例子中,我们使用McvEvent::EVENT_DISPATCH作为我们想要连接的事件。所以基本上发生的事情是,每当控制器执行onDispatch()方法时,这个事件也会被触发。这意味着通过事件,我们可以修改方法的输出结果,而实际上并不需要修改代码。

它是如何工作的...

EventManager类通过几种不同的方法工作,即观察者模式、面向方面编程技术(或 AOP)和事件驱动架构。

观察者模式解释

简单来说,观察者模式意味着存在几个感兴趣的参与者,称为监听器,它们希望知道当应用程序触发某个事件时。当特定事件被触发时,监听器将收到通知,以便它们可以采取必要的行动。

面向方面编程(AOP)解释

如果我们要解释 AOP 是什么,我们可以简单地说,它代表编写只包含功能且尽可能与代码其他部分隔离的干净代码。

事件驱动架构解释

事件驱动架构的好处是,我们不必创建大量需要检查每个条件的代码,我们可以轻松地将自己连接到不同的事件,本质上这将创建一个更响应的应用程序。

还有更多...

EventManager对象通过PriorityQueue进行查询,这告诉我们,重要的事件通常会得到更低的值,而不重要的事件会得到更高的值。例如,最高优先级可能得到优先级-1000,而相当低的优先级可能得到 40。然后EventManager类通过FIFO先进先出)的概念获取队列,这意味着优先级越高,数字越低。

第二章 翻译和邮件处理

在本章中,我们将涵盖:

  • 翻译您的应用程序

  • 本地化您的应用程序

  • 发送邮件

  • 接收邮件

简介

一个应用程序如果不能响应用户,那就不是一个真正的应用程序。显然,一个简单而有效的方法是显示文本和发送电子邮件。在过去的几年里,国际化(i18n)和本地化(l10n)变得越来越重要。如今,用户期望以他们的语言被问候,甚至在日常工作中从应用程序中收到自动化的电子邮件。

翻译您的应用程序

在这个菜谱中,我们将使用 Zend Framework 2 框架作为基础,但我们将创建一个新的模块来展示它是如何工作的。

准备工作

对于这个菜谱,我们假设您已经有一个运行的 Zend Framework 2 应用程序/框架。为了确保我们可以实际运行在菜谱中产生的代码,我们需要确保 PHP 中的 intlgettext 扩展已被启用。

在翻译字符串时,我们将使用 Poedit,这是一个跨平台的开源应用程序,用于翻译 gettext 目录。当前版本是 1.5.5,可以在 www.poedit.net/ 网站找到。我们使用 gettext,因为这是一个广泛使用的国际化本地化系统,用于编写多语言应用程序。由 Poedit 生成的文件扩展名为 .po.mo.po 文件用于编辑;让我们假设这是一个未编译的翻译文件。.mo 文件是编译后的翻译文件,用于我们的应用程序。

如何操作…

在这个菜谱中,我们将讨论如何将我们的应用程序翻译成其他语言,这在当今的应用程序中非常有用。

设置和检查基本要素

我们将假设我们至少设置了一个基本的模块,其中包含一个简单的 IndexController,它输出一个简单的视图。

我们首先要做的是确保在我们的模块结构中有一个语言目录,如下面的代码所示:

SampleModule/
  config/
    module.config.php
  language/
  src/
    SampleModule/
      Controller/
        IndexController.php
  view/
    samplemodule/
      index/
        index.phtml
  Module.php

在这个目录中,所有 gettext 文件都将被存储,这将使我们更容易控制它们。现在,我们已经设置了一个简单的文件夹结构,我们需要确保模块配置也知道我们在做什么。现在,我们打开 module.config.php 并向数组中添加以下行:

// We want to have our translator available through the 
// ServiceManager.
'service_manager' => array(
  'factories' => array(
    // Make our translator available in the 
    // ServiceManager so we can retrieve it under the 
    // 'translator' key.
    'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
  ),
),

// Now to configure the Translator
'translator' => array(
  'locale' => 'en_US',

  // We would like using file patterns when matching 
  // i18n files, as that makes our lives so much easier, 
  // this is default in the skeleton.
  'translation_file_patterns' =>array(
    array(
      // The type of i18n we want to use is gettext.
      'type'     => 'gettext',

      // Here we define our i18n file directory, this is 
      // the directory we just made.
      'base_dir' => __DIR__ . '/../language',

      // We want to match our i18n files through this 
      // pattern, what will be for example 'nl_NL.mo'.
      'pattern'  => '%s.mo',
    ),
  ),
),

通过上述配置,我们已经将我们的模块设置成我们需要的样子。就这样;我们的模块现在已设置好以使用国际化。

在控制器中翻译字符串

一旦我们设置了翻译器,翻译字符串将变得非常简单。在以下示例(文件:/module/Application/src/Application/Controller/IndexController.php)中,我们将翻译控制器中的字符串,但在实际应用中这样做并不好,这里仅作为示例展示:

<?php

// Set our namespace
namespace Application\Controller;

// We need to use the following abstract on our 
// controller
use Zend\Mvc\Controller\AbstractActionController;

// Begin our index controller class

class IndexController extends AbstractActionController
{
  // We can use this property to translate the strings, 
  // or do some other translator related stuff.
  public $i18n;

  // Lets attach the setLocale to the dispatch event, so 
  // it will be run before the action logic is executed
  public function setEventManager(EventManagerInterface $events) 
  {
    // Instantiate the i18n through our ServiceLocator.
    parent::setEventManager($events);

    // We want to use this controller in our event
    $c = $this;

    // Attach our locale setting to the dispatch event
    $events->attach(
      'dispatch', 

      // Variable $e is a Zend\Mvc\MvcEvent
      function ($e) use ($c) 
      {
        // Put our translator in a local property
        $c->i18n = $this->getServiceLocator()
                        ->get('translator'); 

        // while we are here, let's change the locale 
        // to Dutch.
        $c->i18n->setLocale('nl_NL');
      }, 

      // Make sure this event is triggered before the 
      // action execution
      100
    ); 

    // Return our selves
    return $this;
  }

  public function indexAction()
  {
    // Now simply translate this string with our i18n.
    $myTranslatedString = $this->i18n
                               ->translate("And how about me?");
  }
}

在视图中翻译字符串

视图中的翻译甚至比控制器(已经很简单了)还要简单。我们只需要我们想要翻译的字符串,就是这样。我们对index.phtml文件进行以下修改:

<?php
  // Translate and display this text.
  echo $this->translate(
      "Hello, I am a translated text!"
  );

使用 Poedit 翻译字符串

在我们安装 Poedit 后,在开始翻译字符串之前,我们需要设置一些设置。Gettext 与称为目录的文件一起工作。目录是表示特定语言的源文本和翻译文本的文件。

首先,我们应该创建一个新的目录。在输入项目名称和要翻译的语言(例如,nl-NL)的第一个标签后,我们应该转到名为“源路径”的第二个标签。该路径应包含我们想要翻译的源路径,并且很可能是按模块划分的,这意味着基本路径应该是模块目录。

在第三个标签中,应该有一些标识符,Poedit 可以通过这些标识符识别哪些字符串应该翻译或不应翻译。因为我们将会使用translate()方法,我们需要确保至少单词“translate”在列表中,我们可以保留其余的,因为它们不会造成任何伤害。

在我们完成所有能做的事情后,点击确定并选择一个保存文件的位置。此文件需要保存在模块的语言目录中,并且应该有一个命名模式,例如,nl_NL.poen_GB.poen_US.po。文件的命名约定是[language]_[COUNTRY];一些国家(例如,比利时和加拿大)有多个州语言,也需要定义。

保存后,点击更新按钮,这将导致代码被扫描以查找可翻译的字符串。现在出现了一个新列表,列出了所有可翻译的字符串。我们可以轻松地将我们的翻译放入翻译框中并保存。

如果我们完成了所有这些,我们的屏幕可能看起来类似于以下截图:

使用 Poedit 翻译字符串

恭喜,我们现在已经成功创建了一个国际化应用程序!

它是如何工作的...

在 ZF2 中翻译字符串有多种方式,而且它们都相对容易实现。

在你的模块中基本设置翻译

虽然应用程序模块已经设置了翻译功能,但这可能不是我们想要在整个应用程序中使用的。例如,如果我们(我们将这样做)使用不同的模块,我们不想使用应用程序模块中的翻译文件,因为这会使它变得不那么动态。

如果我们在所有模块中使用相同的gettext文件,并将其存储在应用程序模块中,这意味着如果我们没有使用特定的模块,翻译仍然会被加载。当然,这将意味着更多的内存使用,而这本不应该发生。

因此,为每个模块单独设置翻译是一个好主意。

在 ZF2 中,翻译工作显然是因为有Zend\I18n\Translator\Translator类。这个类然后查看配置并加载我们需要的相关Zend\I18n\Translator\Loader。如果找到,它将查看当前选定的区域设置(例如,通过setLocale()设置的nl_NLen_GBen_US等)然后解析相关的翻译文件——对于 gettext 是.mo,对于 INI 是.ini,对于 PHP 数组是.php等——并让加载器解析。

一旦我们调用translate()translatePlural()方法,翻译器将在会话中搜索相关的未翻译字符串。如果找到,它可以轻松返回翻译后的字符串,但如果是未翻译的字符串,它将只返回未翻译的字符串。

还有更多...

除了使用 gettext,还有其他几种方法可以作为翻译文件使用。默认情况下,ZF2 有选项使用以下格式之一:

PHP 数组

虽然这是一种可行且易于翻译的方法,但就我个人而言,我不会推荐它。我的个人经验是,使用这种方法限制了翻译文件的使用范围仅限于 PHP。例如,gettext 是一个行业标准,可以被许多平台和应用使用。

在语言目录中,我们将 PHP 文件命名为[language]_[COUNTRY].php的格式,例如nl_NL.php。我们的module.config.php需要如以下代码所示的条目:

'translator' => array(
  'locale' => 'en_US',
  'translation_file_patterns' =>array(
    array(
      // This is the method we want to use.
      'type' => 'phparray',

      // We tell the config that our translations can be 
      // found in the language directory.
      'base_dir' => __DIR__ . '/../language',

      // It will now search for files like en_US.php and 
      // nl_NL.php.
      'pattern' => '%s.php',
    ),
  ),
),

当这在module.config.php文件中定义时,翻译本身将完全相同,翻译文件(例如,nl_NL.php)将类似于以下代码:

<?php

// We need to return an array with the translated 
// strings.
return array(

  // The key is the untranslated string, while the value 
  // is the translated text.
  'And how about me?'=> 'En hoe zit het met mij?',

  // More translations here [..]
);

Gettext

我们在先前的例子中使用了这种格式,正如我们所见,它们可以通过像 Poedit 这样的应用程序轻松编辑。根据维基百科,gettext 最常用的实现是 GNU gettext。编辑 gettext 文件是在所谓的.po文件中完成的,其中 po 代表可移植对象,一旦文件编译用于使用,它们将被放置在.mo文件中,其中 mo 代表机器对象

我们可以在www.poedit.net/网站上找到翻译工具 Poedit。

Ini

这种ini文件的工作方式基本上与之前描述的任何其他方法相同。语言目录中的文件可以命名为[locale].ini(例如,nl_NL.ini),在module.config.php中,我们会有一个类似于以下代码的条目:

'translator' => array(
  'locale' =>array('en_US', 'nl_NL'),
  'translation_file_patterns' =>array(
    array(
      'type' => 'ini',
      'base_dir' => __DIR__ . '/../language',
      'pattern' => '%s.ini',
    ),
  ),
),

正如我们所见,我们在配置中定义了两个区域设置,这意味着这两个是我们的可用 i18n,但我们的en_US是我们的后备区域设置。后备区域设置在找不到合适的区域设置时使用。我们的翻译文件(nl_NL.ini)将类似于以下示例:

translation.0.message = "And how about me?"
translation.0.translation = "En hoe zit het met mij?"

translation.1.message = "Hello, I am a translated text!"
translation.1.translation = "Hallo, ik ben een vertaaldetekst!"

我们总是从一个翻译开始,使用translation.X,其中X是一个之前未使用的数字。我们应该将其视为一个 INI 数组,类似于它在 PHP 中的工作方式。

本地化您的应用程序

在这个菜谱中,我们将解释本地化及其用途。本地化与国际化的区别在于,本地化指的是例如数字、日期和时间格式,以及货币的使用。

如何操作...

在这个菜谱中,我们将讨论我们应用程序本地化的重要过程。

因此,故事开始了

当用户访问我们的网站时,我们很可能希望用户自动跳转到正确的语言。尽管有几种实现方法,但我们将使用手动检查来查看用户偏好的语言是否也在我们的语言列表中。

我们通过几个简单的技巧来实现这一点:

  • 首先,我们从 HTTP 请求中获取Accept-Language

  • 然后我们遍历它们,看看标题中提到的语言中是否有与我们所拥有的语言相匹配的

  • 最后,我们将语言设置为找到的语言,或者如果没有找到,则设置为回退语言

让我们看看它在我们的Module.php代码中的样子:

// We will be using a modified version of the default 
// Module.php which comes with the Application module on 
// the ZF2 Skeleton.
namespace Application;

// onBootStrap requires a McvEvent.
use Zend\Mvc\MvcEvent;

首先,我们需要声明命名空间(在我们的情况下是Application),因为我们希望框架知道我们的代码在哪里。然后我们想要确保我们总是将所有必需的类放在使用声明中,这样我们就可以在代码进一步之前预加载这些类。

// Start of our Module class
class Module
{
  // Private storage of all our local languages
  // available.
  private $locales;

  /**
   * Retrieves any locale that is available in the 
   * language directory. This
   * assumes that our language directory contains files 
   * in the format of en_GB.ext, nl_NL.ext.
   */
  private function retrieveLocales()
  {
    // If we haven't already got all the locales,
    // please do it now.
    if ($this->locales === null) {
      $handle = opendir(__DIR__. '/language');
      $locales = array();

     if ($handle !== false) {
       // Loop through the directory
       while (false !== ($entry = readdir($handle))) {
         if ($entry === '..' || $entry === '.') {
            continue;
          }

         // We only want the front part of the filename
          $split = explode('.', $entry);

          // Split[0] should be en_GB if the file is 
          // en_GB.ext.
          if (in_array($split[0], $locales) === false) {
            $locales[] = $split[0];
          }

          unset($split);
        }

        // We are done, now close the directory again
        closedir($handle);
      }

      // Make sure the locale is available for next time
      $this->locales = $locales;

      unset($handle, $locales);
    }

    // Return our available locales
    return $this->locales;
  }

retrieveLocales()方法中,我们正在解析语言目录,并假设我们的文件名为en_GB.ext。这样我们就可以轻松地将所有语言解析到一个数组中:

public function onBootstrap(MvcEvent $e)
{
  // Retrieve the HTTP headers of the user's request
  $headers = $e->getApplication()
               ->getRequest()
               ->getHeaders();

  // Get the translator
  $translator = $e->getApplication()
                  ->getServiceManager()
                  ->get('translator');

  // Check if we have a user that accepts specific 
  // languages.
  if ($headers->has('Accept-Language')) {
    // Retrieve our locales that our user accepts
    $headerLocales = $headers->get('Accept-Language')
                             ->getPrioritized();

    // Retrieve the locales that we have in our system
    $locales = $this->retrieveLocales();

    // Make sure that our fallback has been set in 
    // case we couldn't find a locale
    $translator->setFallbackLocale('en_US');

    // Go through all accepted languages, most of the 
    // time this will be only 1 or 2 languages.
    foreach ($headerLocales as $locale) {
      // getLanguage retrieves languages in a en-GB 
      // manner, but ZF2 only supports the underscore, 
      // like en_GB.
      $language = str_replace(
        '-', 
        '_', 
        $locale->getLanguage()
      );

      // See if this is a language we support in our application.
      if (in_array($language, $locales) === true) {
       // We have found our *exact* match
        break;
      }
    }

    // Now set our locale 
    $translator->setLocale($language);
  }
}

// We can just use the methods that are already in the 
// module.php, let's not repeat that code here.  
public function getConfig() {}

public function getAutoloaderConfig() {}
}

如前所述的代码所示,我们试图实现的是查看我们是否与支持的语言(en_GBnl_NL)中的任何一个完全匹配。如果没有找到完全匹配,我们已经确保正在使用回退语言(en_US)。

小贴士

请确保在配置中启用了 PHP 的intl扩展,否则此示例将无法正确工作。

本地化货币

在 Zend Framework 2 中,可以通过 i18n 视图助手在视图中本地化货币,该助手是 ZF2 的标准功能。可以通过以下方法调用轻松在视图中使用名为CurrencyFormat的视图助手。我们对sometemplate.phtml文件进行了以下修改:

<?php

// We always use $this for accessing a view helper.
echo $this->currencyFormat(45312.56, "EUR", "nl_NL");

这段代码将输出45.312,56 €,因为我们指定了使用荷兰本地化格式本地化欧元货币符号,在这种情况下是千位点的逗号和十位点的分隔符。我们也可以省略nl_NL,然后CurrencyFormat视图助手将自动选择应用程序的默认区域设置。

本地化日期/时间

要在我们的应用程序中格式化任何日期和时间,我们可以使用 DateFormat 视图助手,它就像货币视图助手一样容易使用,但有一些额外的选项可以使用。我们对 sometemplate.phtml 文件进行了以下修改。

<?php
echo $this->dateFormat(
    // Format the current UNIX timestamp.
    time(),

    // Our date is to be a LONG date format.
    IntlDateFormatter::LONG,

    // We want to omit the time, defining this is 
    // optional as the default is NONE.
    IntlDateFormatter::NONE
);

之前的代码只会显示日期,它将被格式化为 Monday, May 14, 2012 AD。我们可以省略提供任何参数,但这样将不会显示任何内容,因为默认选项是 IntlDateFormatter::NONE

它是如何工作的…

本地化(l10n)就像国际化(i18n)一样,是公共应用程序的一个重要方面。我们在上一个食谱中讨论了如何确保你的应用程序可以翻译,但现在我们需要确保我们能够找到使用任何 l10n 的方法。

Zend Framework 2 与 PHP 内置的 i18n/l10n 功能紧密协作。尽管我们可以单独使用 PHP 的 Locale 类而不依赖于 ZF2 类,但并不推荐这样做,因为 ZF2 已经使用了 PHP 自身的 Locale,但它提供了一个更优雅、更快捷的接口。

然而,在幕后,ZF2 直接与 PHP 的 Locale 进行通信,但如果我们想使用更强大的功能,我们应该使用 ZF2 库(这在创建多语言 Web 应用程序时非常有用)。

识别客户端语言

之前的示例代码依赖于客户端浏览器发送 Accept-Language 头部。尽管大多数现代浏览器都会这样做,但这仍然可能不是总是可行的。总的来说,这是一个相当好的工具,可以预先选择任何语言。

与之前展示的自行构建一切不同,还有一个非常巧妙的模块叫做 SlmLocale,由 Jurian Sluiman 创建(github.com/juriansluiman/SlmLocale),我们推荐用于检测和选择默认区域设置。

本地化货币和日期

本地化货币和日期通常在视图中完成,因为这基本上只是格式化信息的一部分。你可以在其他地方做,但我们应该始终小心,确保我们不会在例如模型中本地化任何内容,因为它们只应包含逻辑。在大多数情况下,语言不是逻辑的一部分,而是一种使视图更用户友好的简单方式。

发送邮件

通过 sendmail 发送电子邮件通常是一种相当标准的操作方式,因为它可能是 Linux 系统上传输电子邮件(或代理电子邮件到 SMTP 服务器)最常用的方式之一。在大多数 Linux 服务器上,sendmail 已经安装好了,因此使用它来发送电子邮件非常简单。

因此,我们将首先讨论这种发送电子邮件的方法,这样我们可以从简单开始。

如何操作…

在本食谱中,我们将讨论在应用程序内部发送邮件的方法。

Transport\Sendmail

让我们看看以下通过sendmail发送电子邮件的示例,尽管这个功能被放置在控制器中,但在现实生活中,这需要远离控制器,安全地放置在模型中:

<?php

namespace Application\Controller;

// We need the following libraries at a minimum to 
// send an e-mail.
use Zend\Mail\Message;
use Zend\Mail\Transport\Sendmail;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // We start off by creating a new Message, which 
    // will contain our message body, subject, to, 
    // etcetera.
    $message = new Message();

    // Add the options we would like to give the 
    // message, in this case we will be creating a text 
    // message.
    $message->addFrom('awesome.coder@example.com')
            ->addTo('rookie.coder@example.com')
            ->setSubject('Watch and learn.')
            ->setBody('My wisdom in a message.');

    // Now we have set up our message, let's initialize 
    // the transport.
    $sendmail = new Sendmail();

    // Although checking isValid is optional, it is a 
    // great way of checking if our message would send 
    //if we are getting input from outside.
    if ($message->isValid() === true) {
      // Send the message through sendmail.
      $sendmail->send($message);
    }
  }
}

通常情况下,设置通过sendmail发送的电子邮件不需要配置,因为它仅是本地主机上的邮件传输应用程序。

Transport\Smtp

如果我们想通过 SMTP 发送电子邮件(如果我们知道 SMTP 服务器的详细信息的话):

<?php
// Usually this sort of code is defined in the Model, 
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate a SMTP sending.
use Zend\Mail\Message;
use Zend\Mail\Transport\Smtp;
use Zend\Mail\Transport\SmtpOptions;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // First we built up a small message that we want to 
    // send off.
    $message = new Message();

    // We need at least one recipient and a message body 
    // to send off a message.
    $message->addTo('someone@example.com')
            ->addFrom('developer@example.com')
            ->setSubject('An example message!')
            ->setBody('This is a test message!');

    // Now we created our message we need to set up our 
    // SMTP transportation.
    $smtp = new Smtp();

    // Set our authentication and host details of our 
    // SMTP server.
    $smtp->setOptions(new SmtpOptions(array(
      // Name represents our domain name.
      'name' => 'ourdomain.com',

      // Host represents the SMTP server that will 
      // handle the sending of our mail. This could also 
      // be 'localhost' if the sending happens on our 
      // local server.
      'host' => 'smtp.somewhere.com',

      // Port is default 25, which in most cases is 
      // fine, but this is just to show how we can 
      // change it.
      'port' => '1234',

      // Connection class is the class used for 
      // authenticating with the SMTP server. Normally 
      // login will suffice, but sometimes the SMTP 
      // server requires a PLAIN (plain) or CRAM-MD5 
      // (crammd5) authentication method.
      'connection_class' => 'login',

      // This tells the connection_class which 
      // properties to set. The default three connection
      // classes only require username and password.
      'connection_config' =>array(
        'username' => 'someuser',
        'password' => 'someplainpassword',
      ),
    ))); 

    // We have set the options, now let's send the 
    // message.
    $smtp->send($message);
  }
}

Transport\File

让我们看看如何将我们的电子邮件发送到文件的示例…

<?php
// Usually this sort of code is defined in the Model, 
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate a SMTP sending.
use Zend\Mail\Message;
use Zend\Mail\Transport\File;
use Zend\Mail\Transport\FileOptions;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // First we create our simple message. 
    $message = new Message();

    // Set the essential fields send it off.
    $message->addTo('someone@example.com')
            ->addFrom('developer@example.com')
            ->setSubject('An example message!')
            ->setBody('This is a test message!');

    // Now we will initialize our File transport.
     $file = new File();

    // Set the options for the File transport.
    $file->setOptions(new FileOptions(array(
      // We want to save our e-mail in the /tmp path,   
      // this can be anything where we have write  
      // permission on.
      'path' => '/tmp',

      // Define our callback, which will be ran when 
      // the e-mail is being saved to our system. This 
      // also called an anonymous function, as it 
      // isn't defined as a normal method.
      'callback' = function(File $file) {

      // We want to return a name in which the file 
      // should be saved, which should be a unique.
      return 'mail_'. time(). '.txt';
    }
   )));

   // Now send off the message.
   $file->send($message);
  }
}

发送电子邮件后,文件传输器将创建一个可能看起来像mail_453421020.txt的文件。我们指定了/tmp作为文件应该保存的目录,我们应该在那里查看我们的文件是否存在。

当然,我们可以在回调函数中做任何事情,例如,我们可以检查某个文件是否存在,或者从数据库中拉取一个名称。选项是无限的。

工作原理…

Zend Framework 2 需要至少两个对象来使电子邮件发送工作。首先是Zend\Mail\Message对象,用于完全定义需要发送的消息。我们可以在该对象中定义toccbccfrom地址。该对象还用于设置消息正文;这可以是 HTML 或纯文本,完全取决于我们的需求。

作为第二个对象,我们需要一个类来实现Zend\Mail\Transport\TransportInterface类,该类负责处理电子邮件的实际发送。这个类目前(至少现在)只定义了一个send(Mail\Message $message)方法,在我们实现传输时需要添加。

定义两个对象之后,我们将Message对象交给Transport对象,并告诉它发送。发送是如何处理的,显然是由Transport对象决定的。

通过 SMTP 发送邮件

通过 SMTP 传输邮件可能对我们来说不熟悉,但它是通过另一个系统发送电子邮件的常用方法。想想看,桌面电子邮件客户端从另一个服务器检索我们的电子邮件。当我们从这个相同的电子邮件客户端发送电子邮件时,我们可能会使用 SMTP 来发送它。简而言之,SMTP 是将电子邮件发送到另一个邮件服务器,然后由该服务器为我们处理邮件传输。

通过文件发送邮件

虽然不常用,但有一些电子邮件发送者会简单地从特定目录中提取需要发送的完整消息的纯文本文件,并将它们发送出去。显然,如果我们没有测试实际电子邮件发送的方法,这也是测试系统是否工作的一种很好的方式。

接收邮件

现在,让我们处理接收邮件的部分。

准备工作

在这个菜谱中,我们将给出通过 ZF2 连接到邮箱的不同方法的示例,因此如果我们能访问我们连接到的邮箱那就太好了。当然,这不是必需的,但有一个实际工作的邮箱确实增加了乐趣。

如何做到这一点...

我们现在将讨论在应用程序内接收电子邮件,这在某些场合可能很有用。

连接到 IMAP 邮件服务器

连接到邮件服务器的第一种方法是使用 IMAP。该协议基本上允许我们连接到邮件服务器,并在服务器上的不同文件夹中查找是否有未读电子邮件。

让我们来看看我们的示例:

<?php
// Usually this sort of code is defined in the Model,
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate an IMAP connection
use Zend\Mail\Storage\Imap;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {

    // We will create a new IMAP connection here:
    // host: user/password: The username and password 
    // to use. 
    $mail = new Imap(array(
   // Refers to the host where we want to connect to.
   'host' => 'imap.example.com',

   // The username/password to connect to the server 
   // with.
   'user' => 'some_user',
   'password' => 'some_password',

   // Do we want to explicitly use a secure 
   // connection.
   'ssl' => true,

   // If we want to use a port that is different to 
   // the default port, we can do that here.
   'port' => 1234,

   // Specify the folder we want to use, if none 
   // given it will always use INBOX. This will also 
   // work with the Mbox and Maildir protocol.
   'folder' => 'Some_Folder',
 ));

 // We want to parse through all our e-mails.
 foreach ($mail as $message) {
   // Display the from and subject line.
      echo $message->from. ': '. $message->subject;
    }
  }
}

连接到 POP3 邮件服务器

让我们来看看我们的简单连接示例:

<?php
// Usually this sort of code is defined in the Model, 
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate a POP3 connection
use Zend\Mail\Storage\Pop3;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {        
    // We will create a new POP3 connection here
    $mail = new Pop3(array(
      // Refers to the host where we want to connect 
      // to
      'host' => 'pop3.example.com',

      // The username/password to connect to the 
      // server with.
      'user' => 'some_user',
      'password' => 'some_password',

      // Do we want to explicitly use a secure 
      // connection.
      'ssl' => true,

      // If we want to use a port that is different to 
      // the default port, we can do that here.
      'port' => 4321
    ));

    // We want to parse through all our e-mails.
    foreach ($mail as $message) {
      // Display the from and subject line.
      echo $message->from. ': '. $message->subject;
    }
  }
}

在 IMAP 或 Maildir 连接上处理标志

标志是附加到消息上的属性,我们可以从中看到消息的特定属性。简单来说,它可以告诉我们,例如,消息是否已阅读或回复。我们可以通过使用 getFlags() 方法或 hasFlag() 方法从消息中获取标志。可用的标志可以在 Zend\Mail\Storage 类中找到。

Maildir++ 配额系统

让我们来看看以下示例:

<?php
// Usually this sort of code is defined in the Model,
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate a Maildir storage 
// connection
use Zend\Mail\Storage\Maildir;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {	
    // Open up a new Maildir connection
    $mail = new Maildir(array(
      // Our mail folder on the server.
      'dirname' => '/home/user/.mymail/'
    ));

    if ($mail->checkQuota() === true) {
      // We are over quote, let's check what we are 
      // using!

      // Give us extended information about the quota.
      $quota = $mail->checkQuota(true);

      // Normalise the string if we are over the
      // quota.
      $overQuota = $quota['over_quota'] ? 'Yes' : 'No';

      // Display the information.
      echo "
        -- QUOTA --
        Total quota size: {$quota['quota']['size']}
        Total quota objects: {$quota['quota']['count']}
        -- USE --
        Total used size: {$quota['size']}
        Total used objects: {$quota['count']}
        Are we over quota: {$overQuota}
     ";
    }
  }
}

保持连接活跃

以下是一个使用 NOOP 的示例:

<?php
// Usually this sort of code is defined in the Model,
// but to test it out we can place it in the 
// controller as well.
namespace Application\Controller;

// We need these classes to initiate a IMAP connection
use Zend\Mail\Storage\IMAP;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Open up a new connection to the mail server.
    $mail = new Imap(array(
     'host' => 'imap.example.com',
     'user' => 'some_user',
     'password' => 'some_password'
  ));

 // Loop through the messages.
 foreach ($mail as $message) {
   /** Do stuff which takes a lot of time.. **/

   // Now let the server know we are still alive..
   $mail->noop(); 

   /** Do some more stuff..  **/

   // Let the server know again we are still here..
   $mail->noop();
    }
  }
}

这里棘手的部分是何时使用 noop(),因为有时很难预测哪个进程耗时最长。这就是为什么我们创建了一个特殊的示例来向您展示确保 noop() 定期执行有多容易,直到我们完成我们的进程。

我们可以通过利用 register_tick_function 来实现这一点,它使我们能够在每个 tick 上调用一个特定的进程。我们将创建一个处理 noop() 的类,并每 5 分钟执行一次,直到我们说它应该停止:

<?php

// We can make this anything we want, we just decided on 
// this though
namespace Application\System;

// Lets call our class this
class NoopTick
{
  // This is our Zend\Mail\Storage\Imap which is a 
  // static so we can call from outside the context of 
  // this class without instantiating the class
  private static $imap;

  // This is the time value in seconds of the next time 
  // we want to execute our noop functionality
  private static $newTime;

  // This is the amount of seconds between noop
  // executions, which in this case is 5 minutes
  private static $timeInBetween = 300;

  // This is our main method, which will only call the 
  // noop method
  public static function tickTock()
  {
    if (time() >= self::$newTime) {
      // We can execute our noop now
      self::$imap->noop();

      // Now set the new time
      self::$newTime = (time() + self::$timeInBetween);
    }
  }

  // Now we want to have a method that starts up the 
  // noop triggering
  public static function start($imap)
  {
    // Set our imap storage to use
    self::$imap = $imap;

    // Now we register the tick function, which executes 
    // every tick of the process, we will use the class 
    // NoopTicks (this class) and method 'tickTock'
    register_tick_function(array(
      'Application\System\NoopTick','tickTock'
    ));
  }

  // And we now unregister our tick function again when 
  // we are done with our operation
  public static function stop()
  {
    // Unregister our tick function again, mind that we 
    // don't have to provide our class name here
    unregister_tick_function('tickTock');
  }
}

如果我们现在遇到一段需要长时间执行的代码,我们可以轻松地调用 start() 方法来为我们执行 NOOP,如下面的代码行所示:

Application\System\NoopTick::start($imapStorage);

当我们完成时,我们只需再次 stop() NOOP'ing,如下面的代码行所示:

Application\System\NoopTick::stop();

它是如何工作的...

邮箱正在连接;然而,在 ZF2 中我们将它们视为存储对象。正因为如此,我们可以轻松地遍历所有消息,在某些情况下,我们甚至能够操纵存储中的消息,例如复制或移动它们。我们需要记住,消息始终是只读的,而存储是可以被操纵的。例如,我们可以创建和删除文件夹,但永远不能编辑现有的消息。

我们用于操作消息的唯一可写功能是 appendMessage(),它将消息追加到存储中。但是,当它被存储后,我们就无法再次编辑它。

连接到 POP3 服务器

使用 POP3 服务器连接与使用 IMAP 服务器非常相似(方便,不是吗?)。唯一的重大区别通常在于,使用 POP3 服务器,在检索消息后,消息会从邮件服务器上消失,除非我们明确告诉服务器否则。

关于 Maildir++配额系统

Maildir++是 Maildir 的扩展版本,但仍然与正常的 Maildir 程序兼容,并支持配额系统。这是一个非常有用的系统,因为它有配额以及如何存储消息(在文件系统上)。这个系统在许多公司中被使用,但显然它也伴随着自己的问题。例如,当尝试在 Maildir++服务器上写入/复制消息时,可能会抛出异常,因为我们已经超过了系统配额。

正因如此,除非你确定 Maildir++没有被使用,否则在尝试执行任何基于写入的功能之前,实施一个配额检查是明智的。

保持连接活跃

一旦建立了连接并且解析消息被实例化,如果时间过长,连接有很大可能会关闭。在那个时刻,实施一个“无操作”命令,或称 NOOP,总是明智的。这将告诉邮件服务器我们还在那里,但此刻正在做其他事情。

还有更多…

显然,关于从邮件服务器检索电子邮件还有很多可以说的,找到所有这些内容将是一次伟大的冒险。不幸的是,详细讲解所有高级细节几乎可以写成一本书,所以我们只列出了一些值得探索的主题:

  • 缓存实例(参见第八章优化性能,优化性能

  • 读取 HTML 消息,或带有附件的多部分消息

  • 在 IMAP/Maildir/Mbox 上的文件夹的高级使用

  • 协议类扩展

  • 通过配置设置电子邮件箱设置

第三章. 处理和装饰表单

在本章中,我们将涵盖:

  • 创建表单

  • 使用表单视图辅助工具

  • 创建自定义表单元素和表单视图辅助工具

简介

在本章中,我们将讨论表单,特别是它们的生成和处理。表单在与用户通信中起着非常重要的作用,因为它是从用户那里接收信息的一种方式。同时,结合 JavaScript 和 PHP,使用表单对元素进行大量验证也是一个很好的方法。如果我们还能让它看起来很棒,我们为什么不这样做呢?

创建表单

这个配方涉及创建表单的不同方法,然后我们将讨论如何将元素添加到表单中。在这个配方的最后部分,我们将讨论如何验证表单,以及完成此任务的最佳方式。

准备工作...

创建和输出表单所需的至少有一个模块的基本 ZF2 骨架应用程序是必要的。

如果我们想使用表单注解,我们也需要在骨架中初始化 Doctrine\Common,因为它有解析注解的解析引擎。如果我们使用 composer(它包含在 Zend Framework 2 骨架中),我们可以简单地更新我们的 composer.json 文件,在所需部分添加以下行:

"doctrine/common": ">=2.1",

小贴士

确保行尾的逗号只有在还有行在其下方时才存在。如果除了一个闭合括号之外没有其他行跟在此行之后,请勿添加逗号,因为它将导致过程失败。

接下来是运行 composer update 命令以确保其被安装,可以使用以下类似命令:

php composer.phar update

如果我们不使用 composer,我们最好查看 Doctrine 项目网站 (www.doctrine-project.org/projects/common.html) 以获取有关如何安装此项目的更多信息。

如何做到这一点...

我们首先将讨论创建表单和元素,然后我们将讨论添加过滤器和验证。

创建基本表单

表单始终需要是以下之一:

  • Zend\Form 类扩展的类

  • 使用 Zend\Form\Annotation 定义方法的类

定义一个从 Zend\Form 扩展的表单

我们将从定义第一个方法开始,通过从 Zend\Form 类扩展它。如果我们是 Zend Framework 2 (ZF2) 的新手,这可能是开始的最简单方法。

基本思想是我们的表单类应该从 Zend\Form 类扩展,并且至少有一个 __construct 方法来定义我们的元素。

让我们看看 /module/Application/src/Application/Form/NormalForm.php 文件中的以下示例:

<?php

// We define our namespace here
namespace Application\Form; 

// We need to use this to create an extend 
use Zend\Form\Form; 

// Starting class definition, extending from Zend\Form
class NormalForm extends Form 
{
  // Define our constructor that sets up our elements 
  public function __construct($name = null) 
  {
    // Create the form with the following name/id
    parent::__construct($name);
  }
}

如果我们现在去我们的控制器,比如 Application 模块的 IndexController,我们可以通过在文件 /module/Application/src/Application/Controller/IndexController.php 中执行以下操作将表单输出到视图:

<?php

// Namespace of the controller
namespace Application\Controller;

// Use the following classes at a minimum
use Zend\Mvc\Controller\AbstractActionController;
use Application\Form\NormalForm;
use Zend\View\Model\ViewModel;

// Begin our class definition
class IndexController extends AbstractActionController
{
  // Set up our indexAction,  in which we want to 
  // display our form.
  public function indexAction()
  {
    // Initialize our form
    $form = new NormalForm();

    // Return the view model to the user, with the 
    // attached form
    return new ViewModel(array(
        'form' =>  $form
    ));
  }
}

如果我们现在查看我们的视图脚本,我们可以看到我们有一个可用的变量。现在,我们将通过以下示例将表单实际输出到屏幕(文件/module/Application/view/application/index/index.phtml):

<?php
  // Output the opening FORM tag: <form>
  echo $this->form()->openTag($this->form); 

  // Output the formatted elements of the form
  echo $this->formCollection($this->form);

  // Output the closing FROM tag </form>
  echo $this->form()->closeTag();

这个代码示例的输出将类似于以下内容:

<form action="" method="POST" name="normalform" id="normalform"></form>

这告诉我们实例化进行得很顺利,并且它是完全功能的。正如我们也可以看到,我们定义的名称("normalform")正在作为表单的nameid返回。

定义一个使用 Zend\Form\Annotation 的表单

让我们看看一个空表单(/module/Application/src/Application/Form/AnnotationForm.php)在注解表单中的样子:

<?php

// We first define our namespace as usual
namespace Application\Form;

// We need to use this otherwise it will not parse the 
// elements correctly.
use Zend\Form\Annotation;

/**
 * We want to name this form annotationform, which is 
 * why we use the tag below, defining the name. 
 *
 * @Annotation\Name("annotationform")
 * 
 * A hydrator makes sure our framework can 'read' the 
 * properties in our object, in this case we tell our 
 * annotation engine that we have an object that needs 
 * its properties read. There is probably a more 
 * technical, accurate way of explaining it, but let's 
 * just keep it to this for now. 
 *
* @Annotation\Hydrator(
 *     "Zend\Stdlib\Hydrator\ObjectProperty
 * ")
*/
class AnnotationForm
{
  /**
   * If we want to exclude properties in our form just 
   * use the Exclude annotation.
   * 
   * @Annotation\Exclude()
   */
  public $id;
}

如果我们现在想开始将表单输出给用户,我们可以用类似正常表单的方式来做(幸运的是)。为此,我们首先需要做的是实际上再次将表单分配给视图(/module/Application/src/Application/Controller/IndexController.php),这是与正常表单创建略有不同的一点。

<?php

// Namespace of the controller
namespace Application\Controller;

// Use the following classes at a minimum
use Zend\Mvc\Controller\AbstractActionController;
use Application\Form\AnnotationForm;
use Zend\Form\Annotation\AnnotationBuilder;
use Zend\View\Model\ViewModel;

// Begin our class definition
class IndexController extends AbstractActionController
{

  // Set up our indexAction,  in which we want to 
  // display our form.
  public function indexAction()
  {
    // Set up the output model
    $viewModel = new ViewModel;

    // Instantiate the AnnotationBuilder which will 
    // create the actual form object
    $builder = new AnnotationBuilder();

    // Instantiate our annotated form
    $annotationForm = new AnnotationForm();

    // Now let the annotation builder create the form 
    // from scratch
    $form = $builder->createForm($annotationForm);

    // Set our form to be the form variable in the view
    $viewModel->setVariable('form', $form);

    // Return the view model to the user
    return $viewModel;
  }
}

如果我们现在想将表单输出到我们的视图(文件/module/Application/view/application/index/index.phtml),我们可以简单地像处理其他表单一样做:

<?php
  // Output the opening FORM tag: <form>
  echo $this->form()->openTag($this->form); 

  // Output the formatted elements of the form
  echo $this->formCollection($this->form);

  // Output the closing FROM tag </form>
  echo $this->form()->closeTag();

这个示例的 HTML 输出将产生以下内容:

<form action="" method="POST" name="annotationform" id="annotationform"></form>

向 Zend\Form 扩展表单添加元素

在这种表单中创建元素相当简单,让我们通过一个简短的例子来看看它是什么样子(文件/module/Application/src/Form/NormalForm.php):

// Adding a simple input text field
public function __construct($name = null) 
{
  // Create the form with the following name/id
  parent::__construct($name); 

  $this->add(array( 	
    // Specifying the name of the field
    'name' => 'name', 

    // The type of field we want to show
    'type' => 'Zend\Form\Element\Text', 

    // Any extra attributes we can give the element
    'attributes' => array( 
      // If there is no text we will display the 
      // placeholder
      'placeholder' => 'Your name here...', 

      // Tell the validator if the element is required 
      // or not
      'required' => 'required', 
    ), 

    // Any extra options we can define
    'options' => array(  
      // What is the label we want to give this element
      'label' => 'What is your name?', 
    ), 
  )); 
}

向注解表单添加元素

让我们以一个注解元素创建的例子为例:

class AnnotationForm
{
 /**
   * Add two filters to this element.
   *
   * @Annotation\Filter({"name": "StringTrim"})
   * @Annotation\Filter({"name": "StripTags"})

   * Add a validator to make sure the string length 
   * isn't going to be longer than 50, but also not 
   * smaller than 5.
   *
   * @Annotation\Validator({
   *    "name": "StringLength", 
   *    "options":{
   *        "min": 5, 
   *        "max": 50, 
   *        "encoding": "UTF-8"
   * }})
   *

   * Set this element to be required.
   * 
   * @Annotation\Required(true)

   * Set the attributes for the element
   *
   * @Annotation\Attributes({
   *     "type": "text", 
   *     "placeholder": "Your name here...", 
   * })

   * Set the options of this element.
   *
   * @Annotation\Options({
   *    "label": "What is your name?"
   * })
   */
  public $name;

验证表单输入

拥有表单最重要的一个方面是使用我们应用程序中的数据,因为如果不是为了使用这些数据,我们最初为什么要创建表单呢?

让我们去创建一个简单的模型(/module/Application/src/Application/Model/SampleModel.php),稍后我们可以用它作为例子,但这个模型对这道菜谱来说完全没有其他用途。

<?php

namespace Application\Model;

class SampleModel
{
  public function doStuff($array) { 
    return true; 
  }
}

如我们所见,这个模型根本什么都没做,但我们稍后还需要它。

我们现在已经创建了自己的表单扩展,所以现在是时候创建我们的InputFilter类了,这个类将过滤和验证我们将要放入表单中的值,稍后通过setInputFilter将其附加到表单上(我们将编辑文件/module/Application/src/Application/Form/NormalFormValidator.php):

<?php

// Of course our namespace first
namespace Application\Form; 

// As this will be an input filter, we need the 
// following imports to make it work
use Zend\InputFilter\Factory as InputFilterFactory; 
use Zend\InputFilter\InputFilter; 
use Zend\InputFilter\InputFilterAwareInterface; 
use Zend\InputFilter\InputFilterInterface; 

// Create our class, which should be implementing the 
// InputFilterAwareInterface if we want to attach it to 
// the form later on
class NormalFormValidator implements 
InputFilterAwareInterface
{ 
  // This is the input filter that we will create
  protected $inputFilter; 

  // This method is required by the implementation, but 
  // we will just throw an exception instead of setting 
  //the input filter as we don't want anyone to override 
  // us
  public function setInputFilter(InputFilterInterface $inputFilter) 
  { 
    // We want to make sure that we cannot set an input 
    // filter, as we already do that ourselves
    throw new \Exception("Cannot set input filter."); 
  } 

我们现在已经开始创建我们的输入过滤器类,并且已经创建了InputFilterAwareInterface的两个必需方法之一。现在,让我们继续到实现第二个方法,并构建实际的过滤器:

// This is the second method that is required by the 
// interface
public function getInputFilter() 
{ 
  // If our input filter doesn't exist yet, create one
  if ($this->inputFilter === null) {
    // Create the input filter which we will put in our 
    // property later
    $inputFilter = new InputFilter(); 

    // Also instantiate our factory so we can get more 
    // filters at ease
    $factory = new InputFilterFactory(); 

    // Let's add a filter for our name Element in our 
    // form
    $inputFilter->add($factory->createInput(array(
      // This is the element is applies to
      'name' => 'name', 

      // We want no one to skip this field, we need it
      'required' => true, 

      // Now we are defining the filters, which make 
      // sure that no malicious or invalid characters 
      // are supplied
      'filters' => array( 
        // Make sure no tags are in our value, which 
        // could make our system vulnerable for hacks
        array('name' => 'StripTags'), 

        // We want to make sure our string doesn't 
        // have any leading or trailing spaced	
        array('name' => 'StringTrim'), 
      ), 

      // Validators make the form generate errors when 
      // the data is invalid, filters only filter	
      'validators' => array( 
        array ( 
          // We want to add a validator that checks the 
          // length of the string received
          'name' => 'StringLength', 
          'options' => array( 
            // Check if the string is in UTF-8 encoding  
            // and between the 5 and 50 characters long
            'encoding' => 'UTF-8', 
            'min' => '5', 
            'max' => 50', 
          ), 
        ), 
      ), 
    )));

我们刚刚添加了一个简单的验证器,确保字符串的长度不小于 5 个字符,也不大于 50 个字符,当然,在我们的案例中,我们还想使用UTF-8字符,但显然,如果我们需要的话,我们可以取消选择这个选项或更改字符集。

现在,我们将添加一个简单的密码字段验证器和过滤器,但下一个验证器将检查repeat_password字段是否与密码字段值相同。我个人非常喜欢这个验证器,因为它简单而且足够强大,可以减少一些手动劳动。

    // We are doing the same trick again for the 
    // password, so we can just skip over this, as this 
    // was just necessary for the one after this one.
    $inputFilter->add($factory->createInput(array(
      'name' => 'password', 
      'filters' => array( 
        array('name' => 'StripTags'), 
        array('name' => 'StringTrim'), 
      ), 
      'validators' => array( 
        array ( 
          'name' => 'StringLength', 
          'options' => array( 
            'encoding' => 'UTF-8', 
            'min' => '5', 
          ), 
        ), 
      ), 
    )));

    // And here is the great piece of validation we 
    // wanted to show off. This validator checks if the 
    // value of the given element is identical to 
    // another fields value. This way we don't have to 
    // manually check if the password is the same as the 
    // repeat password field.
    $inputFilter->add($factory->createInput(array(
      'name' => 'password_verify', 
      'filters' => array( 
        // The usual filters, as we almost always want 
        // to be sure it contains no tags or 
        //trailing/leading spaces
        array('name' => 'StripTags'), 
        array('name' => 'StringTrim'), 
      ), 
      'validators' => array( 
        array( 
          'name' => 'identical', 
          'options' => array( 
            'token' => 'password', 
          ), 
        ), 
      ), 
    )));

在那个巧妙的验证器之后,我们现在将添加一个简单的电子邮件验证器,它也将有一个非空验证器,用于检查字段是否为空。我们将使用以下代码进行电子邮件验证:

// Email validator works perfectly, especially if we 
// don't want to trust any client side validation 
// (which we shouldn't)
$inputFilter->add($factory->createInput(array(
  'name' => 'email', 
  'filters' => array( 
     array('name' => 'StripTags'), 
     array('name' => 'StringTrim'), 
  ), 
  'validators' => array( 
    array ( 
       'name' => 'StringLength', 
       'options' => array( 
       'encoding' => 'UTF-8', 
       'min' => '5', 
       'max' => '250', 
        ), 
      ), 
      array( 
        // Don't you hate it when you get email 
        // addresses that are not valid? Well, no 
        // more as we can simply validate on that 
        // as well.
        'name' => 'EmailAddress', 
        'options' => array( 
        'messages' => array( 
            // We can even leave a neat little error 
            // message to display
            'emailAddressInvalidFormat' => 'Your email seems to be invalid', 
          ) 
        ), 
      ), 
      array( 
        // This validator makes sure the email 
        // address is not left empty. And although we 
        // can simply say this field is required, 
        // this will give us the opportunity to leave 
        // a nice error message that is relevant to 
        // the user as well
        'name' => 'NotEmpty', 
        'options' => array( 
        'messages' => array( 
            // This message is displayed when the 
            // field is empty, instead of a 'field 
            // required' message as we didn't make 
            // the field required
            'isEmpty' => 'I am sorry, your email is required', 
          ) 
        ), 
      ), 
    ), 
  )));

即使是日期验证也不是问题,我们甚至可以做得更好,只允许我们选择日期范围,这在某些情况下(例如 18+网站)是非常有用的。

      $inputFilter->add($factory->createInput(array(
        'name' => 'birthdate', 
        'required' => true, 
        'filters' => array( 
          array('name' => 'StripTags'), 
          array('name' => 'StringTrim'), 
        ), 
        'validators' => array( 
          array(
            'name' => 'Between',
            'options' => array(
              // We can define the ranges of dates 
              // here, min and max are both optional, 
              // as long as one of them at least exists
              'min' => '1900-01-01', 
              'max' => '2013-01-01', 
            ),
          ),
        ), 
      )));

      // Set the property
      $this->inputFilter = $inputFilter;
    }

    // End of our method, just return our created input 
    // filter now
    return $this->inputFilter;
  } 
}

让我们立即开始,看看一个简单的例子,它使用我们的normalform,就像之前一样(/module/Application/src/Application/Controller/IndexController.php):

<?php

// Define the namespace of our controller
namespace Application\Controller;

// We need to use the following classes
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\NormalForm;
use Application\Form\NormalFormValidator;
use Application\Model\SampleModel;

// Set up our class definition
class IndexController extends AbstractActionController
{
  // We want to parse/display our form on the index
  public function indexAction()
  {
    // Initialize our form
    $form = new NormalForm(); 

    // Set our request in a local variable for easier
    // access
    $request = $this->getRequest();

    if ($request->isPost() === true) {
      // Create a new form validator
      $formValidator = new NormalFormValidator();

      // Set the input filter of the form to the form
      // validator
      $form->setInputFilter(
          $formValidator->getInputFilter()
      );

      // Set the data from the post to the form
      $form->setData($request->getPost());

      // Check with the form validator if the form is 
      // valid or not
      if ($form->isValid() === true) { 
        // Do some Model stuff, like saving, this is 
        // just an empty model we created to show what 
        // probably would happen after a validation 
        // success.
        $user = new SampleModel();

        // Get *only* the filtered data from the form
        $user->doStuff($form->getData());

        // Done with this, unset it
        unset($user);
      }
    }

    // Return the view model to the user
    return newViewModel(array(
        'form' => $form
    ));
  }
}

它是如何工作的…

让我们了解我们是如何实现我们所实现的。

设置基本表单

之前的第一例,创建一个从Zend\Form扩展的表单类,这是设置表单的最基本要求。正如我们所看到的,这个表单目前没有任何元素或属性设置,它唯一定义的是表单对象的 DOM 元素的name/id。在那之后我们所做的是首先初始化表单,然后将ViewModel分配给它,因为这将是要输出到屏幕的视图。

在例子中我们所做的唯一一件事是首先输出<form>标签——包括它的所有属性,如methodaction等。第二件事是我们确实输出了表单中的所有元素(在这个例子中是没有的),最后我们输出表单结束标签</form>,这现在结束了表单声明。

如果我们打开浏览器查看我们的代码,我们将看到与之前没有太大的不同,可能是一个空页面。然而,当我们打开该页面的源代码(在 Firefox 中,这是在页面上右键单击并点击查看页面源代码)时,我们看到我们实际上已经正确地实例化了表单,在 HTML 中。

我们的基本表单实例化现在已经完成,如果我们想要一个更高级但同时也更吸引人的定义表单的方式,我们应该继续阅读下一部分。

设置带注释的表单

定义带注释的表单与普通表单略有不同,主要区别在于带注释的表单只是一个具有属性的类,它没有从任何其他类扩展,而另一种方法要求我们扩展Zend\Form类。在先前的例子中,我们首先使用注释方法创建了一个非常简单且为空的表单。我们还可以看到,我们需要一个 Hydrator 来让注释引擎理解我们在说什么,但我们不需要扩展类,所以我们可以在那里自由地做我们想做的事情。

我们唯一需要小心的是,我们表单中需要的每个元素,都应该将属性访问设置为 public,否则从技术上讲,注解引擎无法识别它。我们不需要为属性创建 getter/setter(除非我们想为自己使用它),因为注解引擎直接使用公共属性。

在控制器中使用表单与正常表单略有不同,因为如果我们只是实例化类并使用它作为表单,最终会出错。类需要首先通过AnnotationBuilder来实际构建表单。这就是为什么我们需要执行createForm(),然后输出一个表单。

这将不会输出任何可见的内容,但如果我们查看页面源代码(在 Firefox 中,这是通过右键单击页面然后点击查看页面源代码来实现的),我们会看到我们有一个新的表单开启标签<form>和一个表单结束标签</form>。在这些标签之间,你可以看到我们的表单,名为annotationform,现在被设置为表单的nameid

一些开发者认为这种定义表单的方式有点过度,因为最终可能看起来我们没有增加很多可用性,这在所有公平的讨论中确实有点道理。这完全取决于具体情况,某种方法是否比其他方法更好,但公平地说,这是一种相当流畅的定义表单的方式!

向表单添加元素

如果我们以与之前相同的方式设置了表单,那么我们有两种定义元素到表单的方法。第一种将是定义表单的正常方法,它是对Zend\Form\Form的扩展,就像在如何做...部分中的表单示例一样,以及它的注解表单,就像在如何做...部分中的AnnotationForm第二个示例。

第一个示例假设我们在扩展自Zend\Form\Form的表单中定义了__construct()。它所做的就是调用Zend\Form\Formadd()方法,我们向该方法提供一个方法数组(是的,你同样可以在配置文件中创建整个表单!)。

添加一个元素就像那样简单。显然,还有更多的元素可供选择,它们都有自己的选项和属性,但我们不会深入讨论所有这些,因为讨论起来会非常冗长。

向注解表单添加元素既简单又复杂。说它简单是因为在最基本的概念中,它只需要你向类中添加一个属性,这已经足够简单了。但如果你想做得更多,添加验证或过滤器,你需要在属性上方添加注解注释。

正如我们在前面的例子中所看到的,通过注解定义元素的方式并不特别困难,只是我们需要知道使用哪个@Annotation。当设置属性/选项或有时其他注解时,我们会看到两个花括号{},这代表 JavaScript 中的一个对象,并用于 JSON。

显然,这并不困难,但需要我们稍微改变一下思维方式。

表单、过滤和验证

一个从Zend\Form\Form扩展的普通表单通过查看表单的$this->elements来创建元素,其中所有表单元素都将被存储。一旦触发表单渲染器,所有这些元素都将装饰成真实的 HTML 标签。在注解表单中,将类转换为 HTML 的过程需要额外一步,简单来说就是将注解类转换成一个类似于Zend\Form\Form扩展类的框架。这样我们就可以像使用真实表单对象一样使用从注解类构建的表单。

当我们提交表单时(你不必一定指定一个 POST,因为它默认已经是POST了),我们让表单检查值是否正确,更重要的是,我们想要确保我们得到的是我们预期的值。

不仅从安全角度验证表单很重要,从过滤角度也很重要。如果我们对我们的元素应用多个过滤器(例如,字符串修剪和去除标签),我们希望它们都为我们准备好了,而不是在之后再次使用这些过滤器。显然,更大的问题是保护我们的应用程序免受恶意用户的侵害,并验证用户的输入。

正如我们在最后一个前面的代码示例中所看到的,我们首先创建表单,然后检查用户是否尝试提交表单。如果是这样,我们将设置我们为该表单创建的特定表单验证器。然后,我们将请求数据(这是用户填写我们的表单的内容)分配给表单。在将数据分配给表单后,我们调用isValid()来查看数据是否有效。如果是,我们使用getData()将过滤后的数据分配给我们的示例模型以保存它。

最后,我们将表单再次分配给视图,这样我们就可以显示在验证过程中发生的任何验证错误。很简单!

还有更多...

我们也可以仅通过配置来定义一个表单形式,这被称为通过工厂创建表单形式,我们鼓励您看看它是如何工作的,因为这同样是一种创建表单的极好方式。

为了增加表单的安全性,人们可能会考虑向我们的表单中添加一个 Zend\Form\Element\Csrf 元素,该元素检查表单的来源以确保没有执行跨站请求伪造(CSRF)。这是一个添加到表单中的唯一密钥,用于验证过程。我们甚至会进一步说,建议创建一个已经添加了 CSRF 元素的基础表单,这样我们就不必担心是否忘记了,只要我们扩展自基础表单即可。

使用表单视图助手

与 Zend Framework 1 的装饰器(在表单的创建和渲染中是一个关键)不同,我们现在知道在 Zend Framework 2 中,使用不同的视图助手和渲染器来渲染表单会更好。

如何做到这一点...

视图助手对于开发者来说是非常重要的工具,在这里我们将讨论如何在我们的代码中使用它们。

Form

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// Just open and close the form tag
echo $this->form()->openTag();
echo $this->form()->closeTag();

// Use a form to pull the attributes from
echo $this->form()->openTag($formObject);

/** Do stuff in between **/

// Close the tag again with no form object attached
echo $this->form->closeTag();

这将渲染出以下内容:

<form></form>

FormButton

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// First we create a simple button (this is better done 
// inside a form/controller or model of course)
$buttonElement = new \Zend\Form\Element\Button(
  // This is the name of the button
  'somebutton'
);

// Render the button immediately through the button 
// element
echo $this->formButton($buttonElement);

// Render the button in 3 steps:
// Step 1, the opening tag: Can be called without a 
// parameter, and array of attributes or an instance of
// Zend\Form\Element
echo $this->formButton()->openTag($buttonElement);

// Step 2, the inner HTML: Output our custom inner HTML 
// here, like the label of the button
echo '<span>Life is short, click now!</span>';

// Step 3, the closing tag: Close the tag again.
echo $this->formButton()->closeTag();

如果我们现在查看渲染输出,它应该看起来像以下这样:

<button name="somebutton"><span>Life is short, click now!</span></button>

FormCaptcha

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php
$captchaElement = new \Zend\Form\Element\Captcha(array(
  // What is the name of the element
  'name' => 'captcha',
  // Now add some captcha specific configuration
  'captcha' => array(
    // The class is necessary for the factory to know 
    // what kind of captcha we want. The options are 
    // Dumb, Figlet, Image and the famous ReCaptcha
    'class' => 'Dumb',
  )
));

// That's all folks, the $captchaElement needs to be of 
// the instance Zend\Captcha\AdapterInterface to make it 
// work
echo $this->formCaptcha($captchaElement);

FormCheckbox

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php
// Create a simple checkbox with the name someCheckbox
$checkboxElement = new \Zend\Form\Element\Checkbox('someCheckbox');

// The $checkboxElement needs to be of the instance 
// Zend\Form\Element\Checkbox to make it work
echo $this->formCheckbox($checkboxElement);

渲染输出可能如下所示:

<input type="checkbox" name="someCheckbox" />

FormCollection

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

$object = new \Zend\Form\Element\Collection(
  // The name of the collection
  'someCollection', 

  // Some additional options
  array(
    // The label we want to display
    'label' => 'collectionSample',

    // Should the collection create a template of our 
    // template element so that we easily duplicate it
    'should_create_template' => true,

    // Are we allowed to add new elements
    'allow_add' => true,

    // And how many elements do we want to render
    'count' => 2,

    // Define the target element to render
    'target_element' =>array(
      'type' => 'Zend\Form\Element\Text'
    ),
));

// The $object can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formCollection($object);

这将渲染出如下极其模糊的输出:

<fieldset><legend>collectionSample</legend><span data-template="&lt;input type=&quot;text&quot; name=&quot;__index__&quot; value=&quot;&quot;&gt;"></span>

这对于集合了解它需要做什么已经足够了,就像在这个例子中,它持有我们的 input 字段的模板。

FormColor

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// We want a simple text field for our color
$color = new \Zend\Form\Element\Color('someColor');

// The $color can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formColor($color);

FormDate, FormDateTime, 和 FormDateTimeLocal

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php
// Create a date element
$date = new \Zend\Form\Element\Date('someDateElement');

// The $date can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formDate($date);
echo $this->formDateTime($date);
echo $this->formDateTimeLocal($date);

FormEmail

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php
// Add a simple text field
$element = new \Zend\Form\Element\Text('someElement');

// The $email can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formEmail($email);

FormFile

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// The $file can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formFile($file);

FormHidden

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// The $hidden can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formHidden($hidden);

FormImage

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// The $image can be of any class that implements the 
// Zend\Form\ElementInterface
$image->setAttrib('src', '/our/image.jpg');

echo $this->formImage($image);

FormInput

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// The $input can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formInput($input);

FormLabel

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php
// Create a simple text input
$element = new \Zend\Form\Element\Text('someElement');

// 1\. This will declare the label immediately. The
// $element can be of any class that implements
// the Zend\Form\ElementInterface

echo $this->formLabel($element);

// 2\. Or we can declare the formLabel like this
echo $this->formLabel()->openTag(array(
    'for' => 'someElement',
));

// We are putting some html in between the 
// <label></label> tags
echo "Some output in between!";
// Close the tag again
echo $this->formLabel()->closeTag();

// 3\. Or as a last method, there is still some other way 
// to define the element. This will prepend 
// $someOtherElement with our $element's label. Instead 
// of prepend we can also use append.
echo $this->formLabel(
    $element, 
    $someOtherElement, 
    'prepend'
);

FormElementErrors

我们对名为 example-viewscript.phtml 的视图脚本进行了以下修改:

<?php

// Create a simple text box
$element = new \Zend\Form\Element\Text('someInput');

// 1\. Just display the element errors, with the optional 
// attributes added as the second parameter.
// The $element can be of any class that implements the 
// Zend\Form\ElementInterface
echo $this->formElementErrors($element, array(
    'class' => 'element-error',
    'id' => 'error_three'
));

// 2\. Custom formatted validation error messages.
echo $this->formElementErrors()
          ->setMessageOpenFormat('<a href="/help-me">')
          ->setMessageSeparatorString(
                   '</a><a href="/help-me">'
         )->setMessageCloseString('</a>')
          ->render($element);

它是如何工作的...

表单元素视图助手是一种渲染表单元素的好方法。在 Zend Framework 的上一个版本中,这是通过表单装饰器完成的,这些装饰器与 ZF2 中的视图助手不同,因为它们是在表单到达视图脚本之前使用的。现在的工作方式是,当表单到达视图脚本时,它仍然处于其原始状态,这意味着我们可以完全操纵表单以符合我们的布局。这创建了一个更动态的输出,我们可以为每个视图脚本定义布局(这在 ZF1 中非常难以实现)。

由于表单元素视图助手负责在视图脚本中渲染元素,它们也可以更贴近开发者的需求。总的来说,这是一种创建外观和功能都出色的表单的绝佳方法。

可以使用各种视图助手和/或渲染器来创建完美的布局。有许多标准视图助手可以用来标记你的表单。

Form

这个助手渲染你的 <form /> 标签,如果需要,可以从我们的 Zend\Form 对象中提取一些属性作为属性使用。

表单助手(通过解析表单)支持的属性有 accept-charsetactionautocompleteenctypemethodnamenovalidatetarget

FormButton

我们可以使用这个助手来渲染 <button /> 标签,显然它可以根据我们的需求以不同的方式工作。它可以通过 Zend\Form\Element 来渲染按钮,或者以三步法来完成,其中我们可以在中间添加自己的内容。

FormButton 助手(通过解析 Element)支持的属性有 nameautofocusdisabledformformactionformenctypeformmethodformnovalidateformtargettypevalue

FormCaptcha

Captcha 用于防止用户在不验证其为人类的情况下提交表单。偶尔,我们会收到被大量垃圾邮件填满的表单。这就是为什么我们现在有了这个小工具,它可以生成一个小图像,这是一个自动化的图灵测试,用来确定我们是否是人类。

这个助手只能通过 Zend\Element\Captcha 对象来渲染,所以在这方面没有太多可以进一步解释的。

FormCheckbox

默认情况下,这个助手将渲染两个元素:

  • 类型为 checkbox<input /> 元素

  • 一个类型为 hidden<input /> 元素,其值为复选框状态

它创建隐藏输入,因为如果复选框未被选中,则不会提交,所以我们可以想象当元素不存在时表单验证的后果。这就是为什么总是在复选框元素之前渲染一个隐藏字段,以确保至少有某些内容被提交。

此外,复选框元素还有一些其他酷炫的选项,例如使用隐藏字段。对于那些有复选框经验的开发者来说,他们可以松一口气,因为未选中的复选框永远不会在表单中由浏览器提交。

因此,在复选框元素之前放置了一个隐藏字段,其名称与复选框元素相同,但填充了未选中的值。这意味着每当复选框未选中时,它将发送隐藏字段的值,否则复选框的选中值将覆盖它。

FormCollection

这个辅助器在例如我们想要在一个实例中渲染一个完整的表单时使用。如果我们使用Zend\Form对象作为此辅助器的参数,我们将得到一个完全渲染的 HTML 表单返回。如果我们使用Zend\Form\Element\Collection,另一方面,我们将得到一个完全渲染的 HTML 集合返回,如果需要,还包括模板。

FormColor

这是一个 HTML5 元素,它是一个具有类型 color 的<input />元素。它创建一个用户可以选择颜色的输入表单,或者当在非 HTML5 兼容的浏览器中使用时,它将简单地显示一个输入字段。

FormDate、FormDateTime 和 FormDateTimeLocal

另一个输出具有类型date<input />元素的 HTML5 元素是FormDate。在 HTML5 兼容的浏览器中,它通常会输出一个日历下拉菜单,用户可以选择他们喜欢的日期;在非兼容的浏览器中,它再次只显示一个文本输入字段。

FormEmail

这个 HTML5 字段是一个很好的字段,它随 HTML5 兼容的浏览器一起提供,具有巧妙的验证功能,可以检查输入的值是否为实际的电子邮件地址。最好不要过于依赖它,我们仍然需要自行验证值,以防用户没有使用 HTML5 兼容的浏览器。

可以在FormEmail上设置的属性有nameautocompleteautofocusdisabledformlistmaxlengthmultiplepatternplaceholderreadonlyrequiredsizetypevalue

FormFile

FormFile辅助器对于显示具有类型 file 的<input />非常有帮助。它不仅显示了输入元素,还可以为任何我们想要监控的上传进度准备元素。像许多其他元素辅助器一样,此辅助器也支持属性:nameacceptautofocusdisabledformmultiplerequiredtypevalue

FormHidden

隐藏的<input />字段便于在不要求用户输入的情况下将信息发布到应用程序。这个辅助器没有什么特别之处,但它支持namedisabledformtypevalue属性。

FormImage

FormImage <input />标签主要用于在表单中替代提交按钮。使用简单,只需src属性(图像的位置)。它还支持namealt(推荐)、autofocusdisabledformformactionformenctypeformmethodformnovalidateformtargetheighttypewidth属性。

FormInput

FormInput 是一个简单的 <input /> 元素,通过自然选择类型为我们渲染元素。并不一定推荐使用这个,因为它相当通用,并且会有其缺陷(例如,当它不是一个必需的 input 标签时)。

FormLabel

如果我们想显示一个 <label />,那么使用这个助手是完美的,因为我们可以声明标签的位置(FormLabel::APPENDFormLabel::PREPEND),我们还可以添加标签的内容。它只支持 forform 作为属性。

FormElementErrors

此助手用于显示表单验证错误。默认情况下,这将在表单元素下方显示,但使用此助手我们可以更自定义地显示此错误。

创建自定义表单元素和表单视图助手

随着我们继续在 Zend Framework 2 中开发,并且我们的应用程序不断增长,就越有必要停止复制粘贴,只是用简单地输出我们想要的类的复制部分来替换所有这些重复的部分。在 ZF2 中,这可以通过视图助手轻松完成。

如何做到这一点...

在这个菜谱中,我们将创建我们自己的表单元素,以及相应的视图助手来显示它。

创建新元素

我们所需要做的只是设置元素的类型,然后就这样了。我们对 /module/Application/src/Application/Form/Element/Video.php 文件进行了以下修改,让我们看看代码应该是什么样子:

<?php

// Set our namespace just right
namespace Application\Form\Element;

// We need to extend from the base element
use Zend\Form\Element;

// Set the class name, and make sure we extend from the 
// base element
class Video extends Element
{
  // The type of the element is video, 'nuff said.
  protected $attributes = array(
      'type' => 'video',
  );
}

如我们所见,这是一项相当容易的工作,我们现在已经成功创建了一个新元素,可以在 ZF2 中使用。

创建新的视图助手

视图助手将创建我们刚刚声明的 HTML 元素,让我们看看视图助手在 /module/Application/src/Application/Form/View/Helper/FormVideo.php 文件中应该是什么样子:

<?php

namespace Application\Form\View\Helper;

use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\ElementInterface;
use Zend\Form\Exception;

class FormVideo extends AbstractHelper
{
  /**
   * Attributes valid for the video tag
   *
   * @var array
   */
  protected $validTagAttributes = array(
    'autoplay' => true,
    'controls' => true,
    'height' => true,
    'loop' => true,
    'muted' => true,
    'poster' => true,
    'preload' => true,
    'src' => true,
    'width' => true,
  );

首先,我们添加了此元素可以拥有的属性,这是为了确保我们不会声明不存在的属性(尽管在大多数情况下这不会造成太大的问题)。

  /**
   * Invoke helper as functor
   *
   * Proxies to {@link render()}.
   *
   * @param ElementInterface|null $element
   * @return string|FormInput
   */
  public function __invoke(ElementInterface $element = null)
  {
    if (!$element) {
      return $this;
    }

    return $this->render($element);
  }

创建了前面的 __invoke 方法,这样我们就不必在我们想要调用视图助手之前初始化类。这样我们就可以通过使用 formVideo() 来在视图脚本中使用它,而不是首先实例化一个新的 FormVideo()

  /**
   * Creates the <source> element for use in the <video>
   * element.
   * 
   * @param array|string $src	Can either be an 
   *                           array of strings, or a 
   *                           string alone.
   * @return string
   */
  protected function createSourcesString($src) 
  {
    $retval = '';

    if (is_array($src) === true) {
      foreach ($src as $tmpSrc) {
        $retval .= $this->createSourcesString($tmpSrc);
      }
    } else {
     $retval = sprintf(
       '<source src="img/%s">',
       $src
     );
    }

    return $retval;
  }

createSourcesString 方法获取包含所有视频 URL 的字符串或数组。如前所述,这可以是字符串或数组,在后一种情况下,它将遍历数组并输出带有源标签的字符串。

  /**
   * Render a form <video /> element from the provided 
   * $element
   *
   * @param ElementInterface $element
   * @throws Exception\DomainException
   * @return string
   */
  public function render(ElementInterface $element)
  {
    // Get the src attribute of the element
    $src = $element->getAttribute('src');

    // Check if the src is null or empty, in that case 
    // throw an error as we can 't play a video without 
    // a video link!
    if ($src === null || $src === '') {
      throw new Exception\DomainException(sprintf(
        '%s requires that the element has an assigned'.   
        'src; none discovered',
        __METHOD__
      ));
    }

    // Get the attributes from the element
    $attributes = $element->getAttributes();

    // Unset the src as we don't need it right here as 
    // we render it separately
    unset($attributes['src']);

    // Return our rendered object
    return sprintf(
        '<video %s>%s</video>',
        $this->createAttributesString($attributes),
        $this->createSourcesString($src)
    );
  }
}

将视图助手添加到配置中

现在我们需要将视图助手添加到模块配置中,以确保视图助手可以在视图脚本中找到。我们可以简单地通过在我们的 /module/Application/Module.php 中添加另一个方法来实现,如下面的代码所示:

class Module 
{
  public function getViewHelperConfig()   
  {
    return array(
        'invokables' => array(
        // Add our extra view helper to render our video
        'formVideo' => 'Application\Form\View\Helper\FormVideo',
      )
    );
  }
}

我们没有把整个类放进去,因为这会对这个例子来说太多无用的信息。然而,想法是,我们可以简单地把这个方法放在我们的 Module.php 中,以确保我们的视图助手会被定位。

显示新的元素

我们对 /module/Application/view/application/index/video.phtml 文件做了以下修改:

<?php
use Application\Form\Element\Video;

// Declare a new video element
$video = new Video();

// Set the attribute src for this element
$video->setAttribute('src', array(
// These are some public video urls from 
// w3schools.com
  'http://www.w3schools.com/html/mov_bbb.mp4',
  'http://www.w3schools.com/html/mov_bbb.ogg',
  ));

// We also want to begin auto playing once loaded
$video->setAttribute('autoplay', true);

// Output the formatted element
echo $this->formVideo($video);

现在我们已经创建了一个新的表单元素和一个新的表单视图助手!

它是如何工作的...

创建元素

首先,我们需要在 ZF2 中创建新的元素,然后再使用它。这可以通过扩展 Zend\Form\Element 的基本元素来轻松完成。

接下来是视图助手,因为我们想确保我们的元素也能正确地渲染给用户。由于我们的元素不是任何现有类型(否则这将是一个非常无聊的食谱),我们需要确保我们为自己创建一个视图助手。

我们代码的最后一部分是创建实际的渲染方法,正如其名称所表明的,它渲染实际的 HTML 对象。

在我们的案例中,我们希望在 src 未定义时触发一个异常,因为没有它,这个 HTML 元素将非常无用。现在,我们已经设置好了一切,我们可以使用这个元素在表单中,或者在其自身的视图脚本中单独使用。在上一个例子中,我们只是在视图脚本中声明了表单元素来展示它如何工作;然而,在视图脚本中使用逻辑并不是一个建议的做法,因为我们希望保持视图尽可能干净,并且只输出与之相关的代码。任何与 HTML 或用户输出相关的内容都应该放在控制器或模型中。

我们做了什么

我们所做的是创建了一个新的表单元素,它原本应该是一个 <video /> 标签,一个全新的 HTML5 元素。这个视频标签可以拥有几个属性,其中之一就是 src。在这个例子中,src 属性告诉视频元素我们可以在哪里找到我们想要播放的视频。

创建我们自己的视图助手的一个好理由是,如果我们有一段 HTML 代码在我们的应用程序中反复出现(比如工具提示或帮助文本),并且只需要复制粘贴并更改一些属性就可以使用。为了节省我们的时间和空间(从代码和可读性的角度来看),我们会将其转换成一个简单的视图助手类,它复制了确切的对象,我们可以通过添加选项来转换这个对象。

最后,我们在视图脚本中简单地使用 formVideo 视图助手来实际渲染对象,这样就可以减轻我们的负担,因为它渲染了一段易于复制的代码。

第四章:使用视图

在本章中,我们将涵盖:

  • 与视图一起工作

  • 使用视图助手

  • 创建全局布局模板

  • 创建可重用的视图

  • 使用视图策略/渲染器

  • 使用上下文切换以实现不同的输出

  • 编写自定义视图策略/渲染器

简介

在本章中,我们将讨论使用视图,这是我们之前在几个地方简要提到过的。视图是为了开发者的利益而创建的,以严格区分前端和后端的一切。这样,后端开发者可以专注于控制器和模型,而前端开发者可以在视图中工作。视图的另一个巨大好处是视图决定了数据是如何输出的,所以在大多数情况下这将是 HTML,在其他情况下可能是 JSON 等等。

我们将在本章的最后一个小节中向您展示如何进行自定义,这样我们就可以完全理解一切是如何工作的。

与视图一起工作

视图可以被认为非常重要,因为它实际上渲染了输出到用户浏览器的内容。因此,我们可以假设了解视图的工作原理在创建 Web 应用程序时非常有用。

准备工作

对于这个配方来说,如果我们已经设置了 Zend Framework 2 骨架并准备好工作,那就很有好处。我们将做一些基本的事情来帮助你开始,所以不需要额外的扩展。

如何做到这一点...

我们将通过使用默认的视图策略PhpRenderer来输出内容到浏览器。

配置视图管理器

我们对/module/Application/config/module.config.php文件进行以下修改:

<?php
return array(
  'view_manager' =>array(
    // We want to show the user if the page is not found
    'display_not_found_reason' => true,

    // We want to display exceptions when the occur
    'display_exceptions' => true,

    // This defines the doctype we want to use in our 
    // output
    'doctype' => 'HTML5',

    // Here we define the error templates
    'not_found_template' => 'error/404',
    'exception_template' => 'error/index',

    // Create out template mapping 
    'template_map' =>array(

      // This is where the global layout resides
      'layout/layout' => __DIR__ . '/../view/layout/layout.phtml',

      // This defines where we can find the templates 
      // for the error messages
      'error/404' => __DIR__ . '/../view/error/404.phtml',
      'error/index' => __DIR__ . '/../view/error/index.phtml',
    ),

    // The template path stack tells our view manager 
    // where our templates are stored
    'template_path_stack' =>array(__DIR__ . '/../view',
    ),
  ),
);

在 ViewModel 实例中设置变量

现在我们已经设置了视图管理器;我们可以去我们的控制器,并在我们的控制器导入部分添加以下内容。

use Zend\View\Model\ViewModel;

现在我们可以使用ViewModel实例来为我们的动作控制器中的PhpRenderer服务。让我们现在就来做这件事:

public function someAction()
{
  $view = new ViewModel();

  // One way of setting a variable in the view 
  $view->setVariable('example', 'Output this to user');

  return $view;
}

就这么简单;在我们定义完所有想要的东西之后,简单地返回ViewModel实例。

标记模板文件

现在是我们完成之前最后一步的时候了,那就是创建一个需要渲染的模板文件。我们可以通过首先在view/index文件夹中创建一个文件(例如)来做到这一点,命名为some.phtml(正如我们之前的例子中所叫的那样)。

现在,我们将只做一件简单的事情,那就是输出我们在ViewModel实例中刚刚声明的变量。

<h1><?php echo $this->example ?></h1>

就这样。我们现在已经在动作中输出了我们在ViewModel实例中声明的变量示例。还有其他方法可以将变量设置到视图中,例如通过将变量声明为ViewModel构造函数的第一个参数。

$view = new ViewModel(array(
  'variable_one' => 'Some Variable',
  'variable_two' => 'Some other Variable',
));

或者,如果我们想同时设置多个变量,但不是在构造函数执行时间,我们也可以执行以下操作:

// First we have the view instantiated
$view = new ViewModel();

// And now we assign a lot of variables at the same time
$view->setVariables(array(
  'variable_one' => 'Some Variable',
  'variable_two' => 'Some other Variable',
));

现在,既然我们是输出变量到视图的专家,我认为是时候来点蛋糕了!

它是如何工作的…

视图在向用户返回请求的输出之前会使用几种不同的方法。

配置

如果我们要使用 Zend Framework 2 的骨架应用程序,那么默认情况下这已经设置好了,但让我们假设我们还没有进行任何配置,我们正在盲目地工作。我们首先想要做的是确保通过依赖注入DI)设置了 ViewManager。我们可以通过在 config 文件夹中打开名为 module.config.php 的模块配置文件(假设我们使用的是标准布局)并在那里添加 ViewManager 配置来实现这一点。

在我们继续之前,还有一点需要注意,那就是 template_path_stack 通过在数组中定义的基本目录中搜索模板来工作。然后它将在这些目录中进一步搜索使用我们描述的格式的模板。

例如,在我们的情况下,带有 aboutActionIndexController 默认解析为 view/index/about.phtml 路径。

ViewModel 实例

ViewModel 实例通常只在控制器中使用,基本上是一个容器,它包含所有需要输出给用户的信息。尽管 ViewModel 实例在技术上可以在任何地方使用,但这并不是一个好的实践,因为控制器的主要责任是处理模型和视图。如果我们改变控制器本质,应用程序将变得难以维护。

ViewModel 实例本身没有其他目的,只是跟踪我们想要输出给用户的所有变量,以及其他选项,比如我们想要使用的模板,以及我们是否想要渲染主布局。

接下来发生的事情是,ViewModel 实例将被 ViewStrategyViewRenderer 拿来用于输出。

几乎每个 ViewStrategy 都有其自己类型的 ViewModel,专为该特定目的设计。这样我们就可以轻松地使用另一个 ViewModel 实例,并为用户创建不同类型的输出。

ViewStrategy

ViewStrategy 类用于确定我们将如何以及是否向用户输出内容。其工作方式通常是,首先 ViewStrategy 确定它接收到的 ViewModel 实例是否与它们期望的模型兼容。它是通过将一个 ViewEvent附加到 EVENT_RENDERER 事件上实现的,该事件将在框架搜索合适的渲染器时触发。

在那个时刻,ViewStrategy 会检查模型是否兼容,如果是,它将返回一个合适的 ViewRenderer,如果不是,它将返回 null。然后框架完成其工作并渲染输出(更多关于这一点在 The ViewRenderer helper 部分)之后,它将触发另一个名为 EVENT_RESPONSEViewEvent

这个事件基本上是ViewStrategy类在输出发送给用户之前可以做的最后一个端点。在这个ViewEvent中,ViewStrategy类如果需要的话可以对响应进行最后的修改。我们应该考虑内容类型、额外头信息或是一些其他最后一刻的事情。

下面的简化版过程如下所示:

ViewStrategy 类

视图渲染器辅助器

渲染器在ViewStrategy类之前提到的两个事件之间使用,它确实像你预期的那样工作;它渲染输出。它从ViewModel实例中获取数据,并根据这些数据渲染输出。它通常需要一个视图脚本,例如PhpRenderer使用的 PHTML 文件,但有时它根本不需要任何脚本,并且可以完全自行渲染输出(例如,考虑以 JSON 格式输出)。我们将在本章后面介绍如何使用不同的 ViewStrategy 和 ViewRenderer。

Using view helpers

我们在 View 中添加的复杂性越多,就越难以正确维护它。这就是为什么我们将逻辑提取出来,并将其放置在视图脚本之外的 View 中,并将它们放在所谓的视图辅助器中。

准备工作

对于这个配方,建议使用 Zend Framework 2 骨架应用程序。我们不会需要任何非同寻常的扩展来使用这个配方。

如何做到这一点...

在 Zend Framework 2 中,有一系列默认的视图辅助器与框架一起提供。让我们看看其中的一些,看看它们的作用以及如何使用它们。

BasePath 视图辅助器

BasePath视图辅助器是一个非常容易使用的视图辅助器,例如:

<!-- 
  The following will prepend the URL with the base path
  which can be /website/public/js/script.js e or /js/script.js.
  The path is something for the basePath to decide.
-->
<script src="img/script.js'); ?>">
</script>

Doctype 视图辅助器

我们对/module/Application/config/module.config.php文件进行了以下修改:

<?php
// This is just a snippet of the code that needs to be
// there for doctype to be defined.
return array(
  'view_manager' => array(
    'doctype' => 'HTML5',
  ),
);

然后在视图脚本中,我们可以执行以下操作来输出格式良好的doctype辅助器:

<?php echo $this->doctype(); ?>

URL 视图辅助器

如果我们想要为特定的路由生成 URL,URL 视图辅助器非常方便使用,例如:

<a href="<?php echo $this->url(
    // This is the name we gave the route in our 
    //configuration file
    'route-name', 

    // Give the parameters for the URL, such as the 
    // controller, action or any parameters that should    
    // be added to the URL
    array(
      'controller' => 'someController',
      'action' => 'anotherAction', 
      'id' => 1234,
    )); ?>">Go to this page!</a>

部分视图辅助器

首先,确保我们实际上有一个模板(/view/application/index/partial/partial.phtml)被用作部分内容。

<div><?php echo $this->partial_variable; ?></div>

然后,我们可以进入我们的正常布局,并使用Partial视图辅助器添加我们的额外模板(/view/application/index/index.phtml):

<div>Some Content.</div>

<div>
  <?php echo $this->partial(
    './partial/partial.phtml', 
    array(
      'partial_variable' => 'Partial content!',
    )
  ); ?>
</div>

它是如何工作的...

一旦我们进入严肃的开发,视图辅助器是不可或缺的。它们通过尽可能地将逻辑与 HTML 分离来确保我们的代码不会变成意大利面(例如)。视图辅助器仅在视图脚本中工作(如果当前视图策略支持的话,但让我们假设它支持),所以下面给出的所有示例都只与视图目录中的.phtml文件相关。

如果我们有一个视图辅助器,我们通常可以立即在视图中通过调用它们来使用:

$this->someViewHelper('some-parameter');

这之所以有效,是因为在首先实例化视图助手之前,someViewHelper 类已经定义了一个 __invoke() 方法。这意味着可以在不需要先实例化的情况下调用它。

然而,有时我们有一些视图助手不能通过之前显示的调用方式使用;实际上,它们需要首先被构建。这可以通过执行以下操作来完成:

$helper = $this->someViewHelper();
$helper->someMethod('some-parameter');

一个视图助手也可以有多个公共方法可用,这通常用于将功能分组在一起。例如,一个(在 Zend Framework 2 中不存在的)名为 Person 的视图助手可能有 getAddress($person)getName($person) 作为公共方法,然后可以通过以下方式使用 invoke 来调用:

echo $this->person()->getAddress($person);
echo $this->person()->getName($person);

Zend\View\Helper\AbstractHelper

技术上 Zend\View\Helper\AbstractHelper 不是一个视图助手,但我们仍然提到它,因为这是我们想要扩展以创建自己的视图助手的类。它实现了一些对于视图助手类正确工作所必需的方法。

BasePath 视图助手的解释

如果我们使用自定义结构来构建应用程序,并且公共文件夹不在网站文件夹的底部,即 /website/public,那么 BasePath 视图助手可以非常有帮助。然后我们可以使用 BasePath 来让它决定我们的位置。BasePath 视图助手通常用于更频繁地处理静态资源,例如图像、样式表和脚本,这对于确保应用程序在更改或根 URL 下保持稳健性非常有用。

Doctype 视图助手的解释

Doctype 是一个非常有用的视图助手,因为我们往往会忘记那些 Doctype 助手是如何构建的。与其在互联网上查找如何再次声明它们,我们现在可以只使用这个小巧的宝石。

您可以随时指定 Doctype 助手,但最好在视图管理器的配置中这样做,以确保应用程序的其他部分也知道我们正在使用哪个 Doctype(有时它们可能只想输出不同的事物)。

我们可以使用的有效 Doctype 视图助手有:

  • XHTML11

  • XHTML1_STRICT

  • XHTML1_TRANSITIONAL

  • XHTML1_FRAMESET

  • XHTML1_RDFA

  • XHTML1_RDFA11

  • XHTML_BASIC1

  • XHTML5

  • HTML4_STRICT

  • HTML4_LOOSE

  • HTML4_FRAMESET

  • HTML5

设置 Doctype 助手对于其他视图助手是至关重要的,因为它们(例如在表单元素的情况下)基于所选类型做出渲染决策。例如,一个 HTML4_* doctype 可能会将输入字段渲染为 <input type="text"></input>,而 XHTML1_STRICT 会将其渲染为 <input type="text" />。如果我们想使用 W3C 的验证服务,Doctype 助手不仅仅是至关重要的。

URL 视图助手的解释

URL 视图助手是个小巧实用的工具,它会根据我们在配置中定义的命名路由构建 URL。这意味着如果我们想构建一个正确格式的 URL,可以使用这个视图助手来帮我们构建。

解释部分视图助手

Partial视图助手在我们想要将布局分成不同的部分时特别有用,如果我们想确保我们的模板可维护并且可以在多个地方重用,这总是很有用的。

我们存储部分视图的目录不是严格规定的,但建议将它们放置在一个我们可以随时找到它们的位置。

还有更多…

我们只讨论了四个默认在 Zend Framework 2 中的视图助手,然而框架中默认还有大量的视图助手同样有用。我个人会建议也浏览一下那些,并稍微了解一下它们,因为即使你永远不会使用它们,它们中的大多数也非常有趣。特别是CycleGravatarHeadStyleHeadTitle视图助手,在我们构建 HTML 页面设置时可能会很有用。

视图助手的完整列表始终可在官方 Zend Framework 2 文档中找到。

创建全局布局模板

视图脚本可以非常动态,但大多数时候我们需要一个全局模板,我们希望将其包裹在来自我们的Action视图脚本输出的内容周围。这个配方将详细解释如何做到这一点,并告诉我们它是如何工作的。

准备工作

对于这个配方,需要一个工作的 Zend Framework 2 骨架应用程序,因为我们将创建和编辑一些文件,这些文件在其中使用。

如何做到这一点…

以下是我们如何着手实现这一目标的:

创建主布局文件

现在让我们创建主文件/module/Application/view/layout/layout.phtml,我们用它来创建我们的布局:

<!-- first of all we want to output the doctype -->
<?php echo $this->doctype(); ?>

<!-- now we add the HTML tag -->
<html>

<!-- enter our head tag -->
<head>
  <!-- we want to output in UTF-8 -->
  <meta charset="utf-8">

  <!-- let's use the headTitle View Helper to output our 
       website title -->
  <?php echo $this->headTitle('Awesome website!') ?>

  <!-- make sure mobile browsers get the best of it with 
       the use of the headMeta View Helper, and setting 
       the viewport -->
  <?php echo $this->headMeta()->appendName(
    'viewport', 
    'width=device-width, initial-scale=1.0'
  ) ?>

  <!-- add a favicon.ico file reference for older 
       versions of Internet Explorer, as that doesn't 
       pick it up by itself -->
  <?php echo $this->headLink(array(
    'rel' => 'shortcut icon', 
    'type' => 'image/vnd.microsoft.icon', 

    // Use the basePath to find our public folder
    'href' => $this->basePath('/images/favicon.ico')
  )) ?>

  <!-- add a style sheet to our template -->
  <?php echo $this->headStyle()->appendStyle(
    $this->basePath('/style.css')
  ); ?>

  <!-- now add a javascript that we need as well, which 
       is only used by Internet Explorer version less 
       than 9 -->
  <?php echo $this->headScript()->prependFile(
    $this->basePath('/script.js'),

    // Non HTML5 browsers need a type set for script 
    // tags
    'text/javascript',

    // Add the extra script conditions
    array(
      'conditional' => 'lt IE 9',
    )
  ); ?>
</head>

我们现在已经成功设置了 head 标签,并使用了大量的视图助手来使我们在添加与 head 相关的标签时生活变得更加轻松。

现在让我们设置一个简单的代码主体,看看我们能在那里做什么:

<!-- let's continue with our body tag now -->
<body>
  <!-- output our main content from our actions -->
  <?php echo $this->content ?>

  <!-- render any inline scripts that we have -->
  <?php echo $this->inlineScript(); ?>
</body>

<!-- we are done here -->
</html>

好了,就是这样,一旦我们输出了content变量,它基本上就会渲染从控制器/操作输出生成的内容。

创建错误模板

错误文件很容易创建,因为它们只需要几样东西。让我们首先创建/module/Application/view/error/404.phtml文件,因为它相当直接。

<h1>404: Page not found!</h1>

<p>
  <!-- show the message of the 404 error, generated by 
       the framework -->
  <?php echo $this->message; ?>
</p>

<!-- there is usually also a separate reason attached, 
     which (if exists) we want to show as well -->
<?php
  if (isset($this->reason) && $this->reason) {
    switch ($this->reason) {
      case 'error-controller-cannot-dispatch':
        $reason = 'Could not get dispatch controller.';
        break;
      case 'error-controller-invalid':
        $reason = 'Undispatchablecontroller.';
        break;
      case 'error-controller-not-found':
        $reason = 'Controller could not be found.';
        break;
      case 'error-router-no-match':
        $reason = 'URL could not be matched by router.';
        break;
      default:
        $reason = 'Unknown';
        break;
    }

    // Now show the reason to the user
    echo $reason;
  }

我们可以使用更多的变量来向用户显示在路由中出了什么问题,我们也可以查看,例如,他们请求了什么,但通常这些更多是用于开发而不是用于生产服务器,因为我们不想暴露太多数据。

现在让我们创建一个文件(/module/Application/view/error/index.phtml),当出现异常时,它将被显示出来,这是开发者最喜欢的事情之一(显然不是)。

<h1>An error occurred!</h1>

<p>
  <!-- show the error message, that is the least we can 
       do -->
  <?php echo $this->message; ?>
</p>

<!-- now show the exception, if we have turned this on 
     in the configuration -->
<?php
  if (isset($this->display_exceptions) && $this->display_exceptions) :
    // Now let's see if we have an exception, and if it 
    // is the right instance as well
    if(isset($this->exception) && $this->exception instanceof Exception) :
?>

<!-- Yup, it is an exception all right -->
<div>
  Exception:

  <!--Show which class threw the exception -->
  <?php echo get_class($this->exception); ?>
</div>

<!-- Show the message thrown -->
<h2>Exception message:</h2>
<div><?php echo $this->exception->getMessage() ?></div>

<!-- And the *beautiful* stack trace as well -->
<h2>Stack trace:</h2>

<div>
  <?php echo $this->exception->getTraceAsString() ?>
</div>

<?phpendif; ?><?phpendif; ?>

它是如何工作的…

AbstractActionController在错误发生时显示错误,并选择正确的模板(在view_manager配置中定义)用于错误消息。我们唯一要做的就是确保模板存在。

如果我们要使用 Zend Framework 2 的 MVC 模型并且预期会反复使用相同的布局,那么全局布局是一个很好的主意,这在大多数情况下都会发生。

创建一个全局布局将真正使我们的生活变得更简单,因为这是一种使我们的代码更易于维护的方法,作为一个程序员,这是你工具箱中最重要工具之一。

首先,我们需要确保view_manager已经被正确定义,这在使用视图菜谱中已经描述过,所以我们假设在这个点上我们使用的是相同的配置。

我们使用了inlineScript视图助手来确保内容也可以输出不是 head 标签部分的脚本,但仍然应该用于输出。

我们希望使用inlineScript来定义任何脚本,而不是将它们添加到模板文件中,因为我们希望尽可能地将 JavaScript 与正常的 HTML 内容分开(我们还想尽可能使内联脚本可重用,从维护的角度来看,这看起来更好)。

错误模板示例是一个非常基本的错误文档,当发生异常时显示。我们还可以做更多的事情,例如,如果有更多的异常,我们可以通过$this->exception->getPrevious()来获取它们,然后作为数组解析它们。

创建可重用视图

在这个动态应用程序的时代,我们有可以多次使用的部件或内容。我们不想一次性获取所有内容,而是希望能够动态地加载新的对象,或者至少不需要做很多工作就能让功能正常工作。

准备工作

对于这个菜谱,我们需要的只是一个工作的 Zend Framework 2 骨架应用程序。

如何操作…

在这个菜谱中,我们将讨论如何创建可重用模板以及最佳的使用方法。

使用 Action 视图助手获取可重用内容

Action视图助手是调用我们代码中的不同动作以检索应用程序其他部分的一个很好的方法:

<div class="left">Some content on the left!</div>

<div class="right">
  <?php
    echo $this->action(
      // The action to call
      'sidebar',

      // The controller to call
      'templates',

      // The module to call
      'application',

      // Parameters to parse along
      array('show' => true)
    );
  ?>
</div>

将子对象定义为 ViewModel 实例

首先,我们应该创建一个简单的视图脚本(/module/Application/view/application/template/sidebar.tpl)来输出:

Hello from the sidebar!

之后,我们需要在控制器中(/module/Application/src/Application/Controller/IndexController.php)。

public function indexAction() 
{
  // Instantiate our main view model
  $view = new ViewModel();

  // Now let's instantiate our child model
  $child = new ViewModel();

  // For the child we want to render a different 
  // template, namely our sidebar.tpl
  $child->setTemplate('template/sidebar.tpl');

  // Now add the child to our main view model
  $view->addChild($child, 'childModel');

  // Return our view model
  return $view;
}

现在我们已经设置了控制器,我们希望在视图脚本中也输出子对象。我们将使用与第一种方法相似的 HTML 布局,这样我们就可以发现它们之间的差异。

<div class="left">Some content on the left!</div>

<div class="right">
  <?php echo $this->childModel; ?>
</div>

它是如何工作的…

当我们在开发 Web 应用程序时,我们会发现自己需要重复使用之前已经制作的内容,比如表单的构建或我们想在多个页面上使用的侧边栏布局。

在那种情况下,我们可以做两件事:

  • 使用 Action 视图助手获取可重用内容

  • 将子对象定义为ViewModel实例

这两种方法都可以在不同的场景中使用,让我们来探讨这两种选项。

动作视图助手的解释

我们主要想使用这个(如果可重用内容在当前模块之外),例如,如果内容是由提供页面小部件的模块创建的,那么它可以在应用的任何地方使用。如果我们想在模块内部使用内容,我们最好使用第二个选项,因为它比第一个选项轻量级,因为它不需要经历整个路由和分发过程。

这个视图助手所做的就是在视图脚本内部调用一个动作,并将该动作调用的结果发送到当前视图脚本。

如果我们看看第一个例子,它会在当前视图脚本内部调用动作并渲染输出。与使用部分视图脚本相比,区别在于这个过程会经历整个路由和分发过程,而部分视图只是简单地显示渲染后的输出。如果我们,例如,需要从数据库中获取记录,部分视图就不够用了。

定义 ViewModel 实例的子项解释

这种渲染可重用内容的方法主要用于(当可重用内容在当前模块内部时),例如,当我们想使用一个特定的概述表,它需要比视图助手能提供的更多智能时。我们渲染的内容不需要我们在不同的模块中捣鼓,我们更愿意远离在控制器内部依赖其他模块。我们通常希望尽可能保持模块的独立性,这样我们才能运行应用,即使其他模块中有一个不可用。

现在我们看看如何工作中展示的例子,我们可以看到在这个方法中比视图助手类有更多的工作要做,但区别在于视图助手类需要在后台做更多的工作来使一切工作。

优缺点

当我们说我们主要应该在当前模块之外使用Action视图助手时,有些人可能会不同意,可能也有很好的理由。反对意见之一是,对于开发者(或大多数情况下的设计师)来说,从不同位置获取内容更简单,而不必受限于在控制器中添加作为ViewModel子项。然而,视图助手类确实需要框架首先找到动作、控制器和模块,然后渲染它们,最后输出它们。

虽然设置起来更简单,但如果我们没有充分的理由使用这个选项,它会对网络应用造成更大的压力。有时,编写更多的代码并利用应用速度的优势,然后变得懒惰,让应用降低速度,可能更好。

当然,每件事都有其利弊,所以我们应该首先考虑情况,以确保我们得到尽可能可维护和可重用的代码。

使用视图策略/渲染器

通常,我们将使用视图来输出 HTML,但有时我们想要以更多样化的方式输出,例如 JSON 或 XML。这个配方将为我们提供足够的信息来轻松完成这项任务。

准备工作

我们只需要使用 Zend Framework 2 的骨架应用来开始这个配方。不需要任何特别的东西。

如何做到这一点...

在应用程序中使用不同的视图策略和渲染器是一种常见的做法。在这个配方中,我们将解释如何做到这一点。

添加视图策略

我们可以通过简单地追加模块配置文件中的 view_manager 配置来轻松地为我们的应用程序添加视图策略(/module/Restful/config/module.config.php),如下所示:

<?php

return array(
  'view_manager' =>array(
    'strategies' => array(
      // This could also be ViewFeedStrategy if we want 
      // to output as a feed
      'ViewJsonStrategy',
    ),
  ),
);

JSON 策略

如果我们从 JSON 策略中收到输出,它可能看起来非常像以下内容:

{
  "hello": "My name is",
  "first": "Terrible Richard",
  "address: {
    "street": "12 Coronation Street",
    "postcode": "SE1 2PE",
    "city": "London"
  }
}

源数据策略

使用源数据策略与其他策略非常相似,正如以下示例所示:

// Assume we have a controller set up wrapped around 
// this
public function indexAction()
{
  // Start a new feed
  $feed = new \Zend\Feed\Writer\Feed();

  // Set the feed name/title
  $feed->setTitle('My Awesome Feed!');

  // Set the link to where the feed can be found, and 
  // the format of the feed
  $feed->setFeedLink(
    'http://winter.example.com/rss', 
    'atom'
  );

  // Who is the author of our feed
  $feed->addAuthor(array(
     'name' => 'N. Stark',
     'email' => 'ned@winter.example.com',
     'uri' => 'http://winter.example.com',
  ));

  // Add some description to the feed
  $feed->setDescription('Loremipsum..');
  $feed->setLink('http://winter.example.com');
  $feed->setDateModified(time());

我们现在已经设置了主数据,这些数据将用于生成我们的源数据。现在让我们添加一些示例数据到输出中:

$data = array(
  array(
    'title' => 'Post 1', 
    'link' => 'http://winter.example.com/post/1',
    'description' => 'Loremipsum..',
    'date_created' => strtotime('2001-01-01 12:03:23'),
    'date_modified' => strtotime('2001-02-12 11:05:24'),
  ),

  // More entries here
);

现在,我们需要解析数据(我知道,这有点奇怪,因为我们刚刚声明了它,但在现实中这种情况永远不会发生),并将它们作为条目放入源数据中:

foreach ($data as $row) {
  $feed->addEntry(
    $feed->createEntry()
         ->setTitle($row['title'])
         ->setLink($row['link'])
         ->setDescription($row['description'])
         ->setDateModified($row['date_modified'])
         ->setDateCreated($row['date_created'])
  );
}

现在剩下的工作就是将源数据导出为特定的格式,并将其添加到实际的 FeedModel 类中。

// Export our feed to RSS style
$feed->export('rss');

// Instantiate a new feed model
$feedModel = new FeedModel();

// Set the created feed in the feed model
$feedModel->setFeed($feed); 

// Action done, return the feed model
return $feedModel;

它是如何工作的...

视图策略类

在 Zend Framework 2 框架应用的骨架中使用的默认视图策略是 PhpRenderer 类,它所做的只是在一个定义的位置搜索 .phtml 文件;默认情况下,这将是 /module/ModuleName/viewPhpRenderer 类能够解析视图脚本中的 PHP 代码,这使得它对于执行一些最后的脚本操作非常方便(但也非常熟悉),例如解析记录以创建表格或显示用户名等。

注意

虽然在 PhpRenderer 类中允许使用 PHP,但应该指出,开发者应该小心不要在视图脚本中放置业务逻辑。逻辑应该放在模型或至少控制器中,因为它们从未打算驻留在视图脚本中。

这个策略将在没有其他策略可用时始终被使用。

默认视图策略

在 Zend Framework 2 中,现成可用的视图策略数量很少,它们是:

  • PHP 策略(默认)

  • JSON 策略

  • 源数据策略

JSON 策略的解释

JSON 对象是 JavaScript 对象表示法的简称,它是一种基于文本、可读的输出格式,主要在全世界现代网络服务中使用。它是从 JavaScript 语言派生出来的,因此,它具有很多类似的特点。

这可能是一个很好的例子,因为我们已经在输出中添加了新行,而实际的 JSON 策略永远不会包含这些。但是,嘿,如果只是服务器之间的通信,我们为什么要关心呢?

JSON 策略不需要模板或视图脚本,因为它基本上解析视图模型中使用的变量,简单!

Feed 策略解释

Feed 策略输出一个 XML 新闻源,可以被用户订阅,例如,作为 RSS 或 RSS2 格式的源。不过,使用 Feed 策略的视图模型会有点不同,因为直接在视图模型中设置变量可能是一个棘手的问题。相反,你可以使用一个 Zend\Feed\Writer\Feed 对象来确定源布局,然后通过将对象作为参数传递给 setFeed 方法来将其传递给 FeedModel

更多关于视图策略

Zend Framework 2 的优点在于,更改输出并不是特别困难,因为它自带了一种称为视图策略的技术,实际上就是视图渲染器。

视图策略是一个类,它识别一个模型并返回一个视图渲染器,该渲染器随后渲染内容的输出。视图策略将确定使用哪个渲染器以及如何使用它。

大多数时候,视图策略都会附带自己的视图模型,这是为了确保我们想要输出的内容与渲染器兼容。视图策略在接收到模型后,将确定是否可以或不能渲染某个模型。

例如,框架中的 JSON 渲染器只渲染 JsonModel 类型的模型,当接收到 ViewModel 时,它将不执行任何操作,因为它在技术上与渲染器不兼容。

有时候我们只需要以不同的方式输出内容。如果我们谈论 REST 服务、RSS 源或自定义内容,我们总是应该能够在不做太多工作的情况下在不同的输出格式之间切换。

使用上下文切换实现不同的输出

我们不仅想要能够通过不同的视图策略输出内容,有时还希望按需这样做,这样我们就可以通过简单地更改请求中的头部来切换输出,例如,从 HTML 切换到 JSON。

准备工作

在某些情况下(例如,在 REST 服务器中),根据用户的要求切换内容的响应输出是必要的。用户可以添加一个 Accept 头部来让服务器知道它接受哪些输出格式,例如 application/jsontext/html

我们将要创建一个简单的网站,默认情况下输出 text/html 格式(这是正常的),但每当我们的头部有 Accept: */json 时,它也会输出 JSON 字符串。

如何实现...

有时候我们不仅想要满足查看我们网站的用户,还想要满足许多不同的受众,例如,源阅读器或其他应用程序。因此,我们将讨论如何在配方中切换上下文。

定义多个策略以输出

首先,我们想要确保我们有了 JSON 视图策略就绪,这样我们就可以轻松地在视图之间切换。我们可以通过在 /module/Restful/config/module.config.php 中添加 ViewJsonStrategy 来实现,如下所示:

<?php

return array(
  // Add the JSON strategy to the view manager for our 
  // output
  'view_manager' =>array(
    'strategies' => array(
      'ViewJsonStrategy',
    ),
  ),
);

根据 Accept 头确定视图模型

在控制器中,有一个叫做 AcceptableViewModelSelector 的小巧控制器插件,它可以用来返回基于 Accept 头部的视图模型。

为了让事情更清晰,我们首先想要定义我们希望在输出中支持哪种类型的模型。让我们在我们的控制器中创建一个属性来调节我们支持哪些视图模型:

<?php

namespace Restful\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  protected $acceptCriteria = array(
    'Zend\View\Model\ViewModel' =>array(
      'text/html',
    ),
    'Zend\View\Model\JsonModel' =>array(
      'application/json',
      'text/json',
    ),
  );
}

如我们所见,我们将按照优先级支持两种模型。首先,我们希望默认视图模型使用正常的 PhpRenderer 类,这样用户将看到正常的 HTML 输出。其次,我们希望任何 application/jsontext/json 都由我们的 JsonRenderer 类渲染。

现在让我们创建一个简单的 indexAction 方法,并利用视图模型的选择能力:

public function indexAction()
{
  // Get the right view model that goes with the Accept-
  // header
  $viewModel = $this->acceptableViewModelSelector(
    $this->acceptCriteria
  );

  // Set the variables in the given view model
  $viewModel->setVariables(array('output' => array(
    'one' => 'Row, row, row your boat,',
    'two' => 'gently down the stream.',
    'three' => 'Merrily, merrily, merrily, merrily,',
    'four' => 'life is but a dream.',
  )));

  // output the view model
  return $viewModel;
}

就是这样,朋友们!这已经是最简单的方法了,因为 AcceptableViewModelSelector 为我们做了所有的工作,我们唯一需要做的就是确保模型中声明了一切。

当我们现在为正常的 PhpRenderer 类添加视图脚本,以便它能够良好地渲染我们的正常 text/html 输出时,我们可以肯定一切都已经完成。请确保这个视图脚本(/module/Restful/view/restful/index/index.phtml)位于我们的新 Restful 模块中。

<table>
  <tr>
    <!-- output our variables -->
    <?php foreach ($this->output as $col) : ?>
    <td><?php echo $col ?></td>
    <?php endforeach; ?>
  </tr>
</table>

对于具有 Accept: application/json 头部的用户,输出将如下所示,为此我们不需要视图脚本,因为渲染器会立即输出这个。

{"output":{"one":"Row, row, row your boat,","two":"gently down the stream.","three":"Merrily, merrily, merrily, merrily,","four":"life is but a dream."}}

默认的 PhpRenderer 输出将如下所示:

<table>
  <tr>
    <!-- output our variables -->
    <td>Row, row, row your boat,</td>
    <td>gently down the stream. </td>
    <td>Merrily, merrily, merrily, merrily, </td>
    <td>life is but a dream.</td>
  </tr>
</table>

它是如何工作的...

AcceptableViewModelSelector 通过查看请求中发送的头部来决定使用哪个视图模型。它通过查看我们解析到其中的 array 和查看我们定义并支持的不同 Accept 头部来决定模型。

接下来,它将取那个特定 array 项的键,那将是将被实例化的视图模型。

还有更多...

为了测试不同的头部,我喜欢使用安装了 Header Tool 插件的 Mozilla Firefox 浏览器(addons.mozilla.org/en-us/firefox/addon/header-tool),或者类似的 Chrome 扩展,或者如果我们特别勇敢,可以直接使用命令行 cURL。在那里,你可以输入你想要发送的头部,并打开或关闭它。然而,发送头部的方式也有不同。这取决于你更喜欢如何做事。

编写自定义视图策略/渲染器

在编码中,没有什么比开发自己的自定义功能并与框架集成更令人兴奋的了。在这个菜谱中,我们将讨论如何创建自己的 XML 视图策略。我们将向您展示如何简单地创建新策略的基础,而不会太麻烦。

如何做到这一点...

有时候,默认的策略和渲染器提供的并不足以满足特定情况,所以让我们讨论一下如何创建我们自己的视图策略/渲染器。

创建 XmlOutput 渲染器

让我们先看看我们的渲染器会是什么样子,因为这可能是我们编写的最懒惰的类之一。我们将在这个新类中完成这个任务,该类位于 /module/XmlOutput/src/XmlOutput/View/Renderer/XmlRenderer.php

<?php

namespace XmlOutput\View\Renderer;

use Zend\View\Renderer\PhpRenderer;

/**
 * This is the XML Renderer, which is as you can see 
 * empty as we don't really need
 * to do anything to get this one going, the PhpRenderer
 * basically does everything
 * we need. 
 */
class XmlRenderer extends PhpRenderer {}

这个模型的代码非常简单,因为我们实际上不需要编写很多代码就能让它工作,我们将在 /module/XmlOutput/src/XmlOutput/View/Model/XmlModel.php 文件中完成这个任务。

<?php

namespace XmlOutput\View\Model;

use Zend\View\Model\ViewModel;

/**
* This is the XML View Model
*/
class XmlModel extends ViewModel
{

  /**
   * XML probably won't need to be captured into a
   * a parent container by default.
   *
   * @var string
   */
  protected $captureTo = null;

  /**
   * XML is usually terminal
   *
   * @var bool
   */
  protected $terminate = true;

  /**
   * UTF-8 Default Encoding
   * @var string
   */
  protected $encoding = 'utf-8';

  /**
   * Content Type Header
   * @var string
   */
  protected $contentType = 'application/xml';

  /**
   * Set the encoding
   *
   * @param string $encoding
   * @return XmlModel
   */
  public function setEncoding($encoding) 
  {
    $this->encoding = $encoding;
    return $this;
  }

  /**
   * Get the encoding
   *
   * @return string
   */
  public function getEncoding()
  {
    return $this->encoding;
  }

在前面的代码片段中,我们有一个简单的编码获取器和设置器,它通常将是 UTF-8,因为它也被声明为属性的默认值。

  /**
   * Set the content type
   *
   * @param string $contentType
   * @return XmlModel
   */
  public function setContentType($contentType) 
  {
    $this->encoding = $contentType;
    return $this;
  }

  /**
   * Get the content type
   *
   * @return string
   */
  public function getContentType() 
  {
    return $this->contentType;
  }	
}

现在我们需要创建更令人兴奋的部分,即 XmlStrategy(位于 /module/XmlOutput/src/XmlOutput/View/Strategy/XmlStrategy.php),这是实际告诉框架如何、什么以及如何通过处理两个视图事件(这是必需的)来渲染内容的部分。

<?php

namespace XmlOutput\View\Strategy;

use XmlOutput\View\Model\XmlModel;
use XmlOutput\View\Renderer\XmlRenderer;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\View\ViewEvent;

/**
 * This is the XML View Strategy
 */
class XmlStrategy implements ListenerAggregateInterface
{
  /**
   * @var \Zend\Stdlib\CallbackHandler[]
   */
  protected $listeners = array();

  /**
   * @var XmlRenderer
   */
  protected $renderer;

再次定义了我们需要的所有属性。第一个 $listeners 将包含一个 CallbackHandler 数组,我们将使用它将事件附加和分离到 EventManager 实例。

第二个成员变量 $renderer 将存储我们刚刚创建的 XmlRenderer

  /**
   * Constructor
   *
   * @param XmlRenderer $renderer
   */
  public function __construct(XmlRenderer $renderer) 
  {
    $this->renderer = $renderer;
  }

现在我们已经定义了简单的构造函数,它基本上将给定的 XmlRenderer 类分配给我们的本地属性以进行安全存储,这是渲染策略的典型行为。接下来,我们将继续实现事件处理器。

  /**
   * Make sure we only use our renderer when we are also 
   * using our XmlModel.
   *
   * @param ViewEvent $e
   * @return null|XmlRenderer
   */
  public function selectRenderer(ViewEvent $e) 
  {
    if (!$e->getModel() instanceof XmlModel) {
      // This is not our type of model, can't do 
      // anything
      return;
    }

    return $this->renderer;
  }

  /**
   * We can inject the response now with the XML content 
   * and the appropriate Content-Type header
   *
   * @param ViewEvent $e
   * @return void
   */
  public function injectResponse(ViewEvent $e) 
  {
    if ($e->getRenderer() !== $this->renderer) {
      // The renderer we got is not ours, returning
      return;
    }

    $result = $e->getResult();

    if (is_string($result)) {
      // String is empty, we cannot output anything
      return;
    }

    $model = $e->getModel();
    $response = $e->getResponse();
    $response->setContent($result);
    $headers = $response->getHeaders();
    $charset = '; charset='. $model->getEncoding(). ';';

    $headers->addHeaderLine(
      'content-type', 'application/xml'. $charset
    );
  }

对于这个策略,我们需要做的最后一件事是附加和分离我们的事件。在这种情况下的事件方法是 selectRendererinjectResponse,它们将在代码的不同点被触发。第一个将在事件 ViewEvent::EVENT_RENDERER 发生时被触发,第二个将在 ViewEvent::EVENT_RESPONSE 上被触发。一旦框架使用完它所需的一切,它将调用 detach 方法,然后我们需要确保所有的事件都将被分离。

  /**
   * Let's attach the aggregate to the specified event 
   * manager
   *
   * @param EventManagerInterface $events
   * @param int $priority
   * @return void
   */
  public function attach(EventManagerInterface $events, $priority = 1) 
  {
    $this->listeners[] = $events->attach(
          ViewEvent::EVENT_RENDERER, 
          array($this, 'selectRenderer'), 
          $priority
    );

    $this->listeners[] = $events->attach(
          ViewEvent::EVENT_RESPONSE, 
          array($this, 'injectResponse'),
          $priority
    );
  }

  /**
   * We can detach the aggregate listeners from the 
   * specified event manager
   *
   * @param EventManagerInterface $events
   * @return void
   */
  public function detach(EventManagerInterface $events) 
  {
    foreach($this->listeners as $index => $listener) {
      if ($events->detach($listener)) {
        unset($this->listeners[$index]);
      }
    }
  }
}

接下来是之前我们没有使用过的东西,那就是 ViewXmlStrategyFactory 类。工厂基本上实例化了 XmlStrategy 类(在这种情况下)并确保一切都被正确实例化。我们将在以下位置创建我们的新文件:/module/XmlOutput/src/XmlOutput/Service/ViewXmlStrategyFactory.php

<?php

namespace XmlOutput\Service;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use XmlOutput\View\Strategy\XmlStrategy;

/**
 * Creates the service for the Xml Strategy.
 */
class ViewXmlStrategyFactory implements FactoryInterface
{
  /**
   * Creates and returns the XML view strategy
   *
   * @param ServiceLocatorInterface $serviceLocator
   * @return XmlStrategy
   */
  public function createService(ServiceLocatorInterface $serviceLocator) 
  {
    return new XmlStrategy($serviceLocator->get('ViewXmlRenderer'));
  }
}

就这样,正如我们所看到的,这并不复杂,只是在类中定义了createService方法。在这个方法中,我们唯一做的事情是获取ViewXmlRenderer参数,并确保XmlStrategy类使用该渲染器作为参数被构造。

现在,让我们看看ViewXmlRendererFactory(位于/module/XmlOutput/src/XmlOutput/Service/ViewXmlRendererFactory.php),它也是一个工厂,但现在是为渲染器。

<?php

namespace XmlOutput\Service;

use XmlOutput\View\Renderer\XmlRenderer;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * Creates the service for the Xml Renderer.
 */
class ViewXmlRendererFactory implements FactoryInterface
{
  /**
   * Creates and returns the XML view renderer
   *
   * @param ServiceLocatorInterface $serviceLocator
   * @return XmlRenderer
   */
  public function createService(ServiceLocatorInterface $serviceLocator) 
  {
    $renderer = new XmlRenderer();

    // Set the View resolvers and helper managers.
    $renderer->setResolver(
      $serviceLocator->get('ViewResolver')
    );

    $renderer->setHelperPluginManager(
      $serviceLocator->get('ViewHelperManager')
    );

    return $renderer;
  }
}

虽然这个createService方法比之前的要复杂一些,但它仍然是一个非常轻量级的方法。这里真正发生的事情只是实例化了XmlRenderer类,并确保ViewResolverViewHelperManager被设置。

现在我们已经设置了基本功能,让我们将其全部整合起来,以便我们可以开始使用它!

首先,我们需要创建/module/XmlOutput/config/module.config.php文件,以确保我们的服务被正确实例化,并且我们的视图管理器知道我们提供的新策略。

<?php
  return array(
    // Set our factories, so our service manager can find 
    // them
    'service_manager' =>array(
      'factories' => array( 
        'ViewXmlStrategy' => 'XmlOutput\Service\ViewXmlStrategyFactory', 
        'ViewXmlRenderer' => 'XmlOutput\Service\ViewXmlRendererFactory'
    ), 
  ), 

  // Add our strategy to the view manager for our output
  'view_manager' =>array(
    'strategies' => array(
      'ViewXmlStrategy',
    ),
  ),
);

这相当简单,因为我们只需告诉serviceManager一切的位置,它就会立即工作。

在我们的新XmlOutput模块中,我们需要创建的最后一件事情是Module.php文件,这基本上与 Application 模块中提供的默认Module.php相同。我们可以简单地复制那个文件,更改文件中的命名空间,然后就可以完成。该文件应位于/module/XmlOutput/Module.php

<?php

namespace XmlOutput;

use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

class Module
{
  public function onBootstrap(MvcEvent $e)
  {
    $eventManager= $e->getApplication()->getEventManager();

    $moduleRouteListener = new ModuleRouteListener();
    $moduleRouteListener->attach($eventManager);
  }

  public function getConfig()
  {
       return include __DIR__. '/config/module.config.php';
  }

  public function getAutoloaderConfig()
  {
    return array(
      'Zend\Loader\StandardAutoloader' =>array(
        'namespaces' => array(
          __NAMESPACE__ => __DIR__ . '/src/' .__NAMESPACE__,
        ),
      ),
    );
  }
}

现在需要将我们的新模块添加到/config/application.config.php文件中,这样框架就会尝试实例化该模块。我们只需将XmlOutput添加到模块数组中,然后就可以完成,那里不需要做任何其他更改。

return array(
  // This should be an array of module namespaces used 
  // in the application.
  'modules' => array(
    'Application',

    // Add our module to this array
    'XmlOutput',
  ),

  // After this comes the rest of the file, but that is 
  // irrelevant at the moment.
);

一切都已准备就绪并设置好,现在是时候真正开始行动,将内容输出为 XML。首先,我们将在IndexControllerindexAction(位于/module/Application/src/Application/Controller/IndexController.php)中使用XmlModel。我们只需将一些变量分配给XmlModel并立即返回,现在不需要任何花哨的东西。

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use XmlOutput\View\Model\XmlModel;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    return new XmlModel(array(
      "some_variable" => "Awesome!",
      "why_not_another_one" => "While we are here?"
    ));
  }
}

一旦我们完成了这些,我们就可以构建我们的视图脚本(位于/module/Application/view/application/index/index.phtml),并添加必要的 XML。

<nodes>
  <variable_1><?php
    echo $this->some_variable;
  ?></variable_1>
  <variable_2><?php
    echo $this->why_not_another_one; 
  ?></variable_2>
</nodes>

就这样!一旦运行,我们现在可以看到我们的 HTTP 头设置为application/xml,输出的是我们刚刚放入的 XML。显然,这并不复杂,但它展示了创建我们自己的视图策略是多么容易。

它是如何工作的...

由于我们将我们的工厂添加到了ServiceManager中,我们可以很容易地通过它们的别名ViewXmlStrategyViewXmlRenderer来使用它们。并且因为我们已经告诉ViewManager我们的新策略ViewXmlStrategy存在,我们可以开始行动。

由于我们将在控制器中使用XmlModel,框架将遍历所有视图策略以确定要使用的正确策略。一旦它找到了所需的策略,它将触发EVENT_RENDEREREVENT_RESPONSE事件,这些事件反过来将触发我们的策略方法。这些方法将确定内容的输出。

我们的渲染器确保内容被正确渲染。在我们的例子中,我们采取了偷懒的方式,让PhpRenderer基本上完成所有工作,但这可能因渲染器而异。

我们正在创建这个新的视图策略作为一个独立的模块,具有独立的命名空间,这样我们就可以轻松地将它转移到另一个应用程序中。当然,当我们将功能组件分开时,这也带来了更高的可维护性。

当我们完成时,我们可以很容易地根据需要进一步扩展类,但现在是保持基本。

在我们可以至少拥有自定义视图策略的最基本形式之前,需要创建五个文件;这些文件需要以下形式:

  • 渲染器

  • 模型

  • 策略

  • 策略工厂

  • 渲染器工厂

前三个我们已经知道了,因为我们已经在本章中讨论过,但最后两个工厂的却是新的。

XmlRenderer 和 XmlModel

因为我们只想将 XML 作为字符串输出,所以我们将使用PhpRenderer,因为它确实做了我们想要它做的确切的事情。

接下来是编码模型。如前所述,该模型将在控制器中使用以存储变量,然后我们可以在视图中使用这些变量。我们将创建 XmlModel,以便当我们在这个控制器中使用这个模型时,我们的框架知道我们想要使用我们的XmlStrategy输出。

如我们所见,我们在XmlModel中把所有属性都设置为受保护的,因为这些属性在我们试图扩展的类(ViewModel)中也是受保护的。在扩展属性时,有必要给它相同的访问级别或更低的级别。在这种情况下,它是受保护的,这意味着较低的选项是公共的。然而,私有会导致如下所示的致命错误:

PHP Fatal error:  Access level to XmlOutput\View\Model\XmlModel::$captureTo must be protected (as in class Zend\View\Model\ViewModel) or weaker in /var/www/module/XmlOutput/src/XmlOutput/View/Model/XmlModel.php on line 0

XmlModel中我们需要做的最后一件事是为内容类型创建 getter 和 setter,在我们的例子中这将变成application/xml,因为我们想要输出 XML,而不是纯文本。

XmlStrategy

selectRenderer中,我们想要确保我们拥有的模型也是我们期望的模型。如果不是这样,我们无法返回一个渲染器,这意味着框架需要寻找另一种类型的渲染器。例如,使用ViewModel实例会导致selectRenderer返回 null,这将告诉框架寻找另一种合适的策略。在这种情况下,可能是PhpStrategy,在这种情况下,它将接受ViewModel作为有效的模型,这就是视图策略如何与框架通信,告诉它是否可以使用该模型。

injectResponse 是一个方法,它将准备输出内容,并确保内容类型已设置在头部。作为参数提供的 ViewEvent 包含我们所需的所有收集到的信息,例如 XmlModel,以及其响应。接下来的代码将把刚刚创建的最后两个方法结合起来,并将它们用作相应 ViewEvent::EVENT_RENDERERViewEvent::EVENT_RESPONSE 事件的处理器。

还有更多...

我们之前提到我们对渲染器有点懒,基本上是把所有的工作都推给了 PhpRenderer,它反过来又基本上渲染了包含 XML 的视图脚本。自然地,人们会希望有一个使视图脚本变得过时的渲染器,它只需从 XmlModel 中的数组创建 XML。

所以是的,还有很多可以说的,但如果我们开始探索渲染内容的不同方式,真正的乐趣就开始了。

第五章:配置和使用数据库

在本章中,我们将涵盖:

  • 连接到数据库

  • 执行简单查询

  • 使用 TableGateway 执行查询

  • 使用 DB 分析器进行优化

  • 创建数据库访问对象

简介

显然,如果我们想存储数据,数据库是必不可少的,而且由于有各种各样的数据库引擎,有时候很难看清事物的本质。然而,Zend Framework 2 为我们带来了一丝标准化的希望。在本章中,我们将展示从数据库连接到优化查询性能的大量示例。

可用的默认数据库引擎

Zend Framework 2 提供了一组默认的数据库驱动程序可供使用,并且显然它也支持 PHP PDO 扩展,以实现更标准化的数据库使用方式。

IBM DB2 驱动程序

IBM DB2是由 IBM 设计的一个数据库服务器,根据 IDC 2009 年的报告,它是第二常用的数据库管理系统(DBMS)(www.marketresearch.com/IDC-v2477/Worldwide-Database-Management-Systems-Forecast-2393193/view-stat/ibm-14.html)。数据库引擎可以追溯到 20 世纪 70 年代,直到 20 世纪 90 年代才开始支持其他更广泛使用的操作系统。

现在,DB2 主要在 ZF2 的 IBM i Power Systems(如 AS/400)中使用,但仍然是一个非常强大的数据库引擎。

要求:

  • IBM DB2 通用数据库客户端需要安装在 PHP 机器上

  • PHP 配置为使用--with-IBM_DB2选项,或者在php.ini中启用(并安装)了ibm_db2扩展

MySQLi 驱动程序

对于 PHP 开发者来说,这可能是最常用的数据库引擎,MySQLi而不是普通的 MySQL 驱动程序,在现代 MySQL 系统版本(4.1.3 及更高版本)中为扩展提供了几个优势。这个改进的扩展支持以下现代 MySQL 功能:

  • 增强的服务器支持

  • 事务支持

  • 预处理语句支持

  • 面向对象接口

  • 多语句支持

  • 增强的调试可用性

MySQLi 驱动程序的要求是 PHP 配置为使用--with-mysql--with-mysqli选项,或者在php.ini中启用(并安装)了mysqlmysqli扩展。

OCI8 驱动程序

OCI8 驱动程序支持 Oracle 数据库 11g、10g、9i 和 8i(根据 PHP 手册),在 PHP 社区中广泛使用。

要求:

  • PHP 机器上的 Oracle 9ir2、10g 或 11g 客户端库

  • PHP 配置为使用--with-oci8选项,或者在php.ini中启用(并安装)了oci8扩展

PGSQL 驱动程序

PostgreSQL是一个对象关系型数据库,也是我个人最喜欢的数据库,这个数据库自 1995 年以来一直存在,并被 Reddit、Instagram 和 Yahoo!等网站使用。

对于此要求,PHP 需要配置为带有--with-pgsql选项,或者在php.ini中启用(并安装)pgsql扩展。

SQLSRV 驱动程序

微软 SQL Server(以及 SQL Azure)是一个仅在 Microsoft Windows 上运行的数据库,并且普遍被认为是一个非常良好且稳定的数据库引擎。PHP 扩展的 3.0 或更高版本支持 SQL Server 2005。

要求:

  • 需要在 PHP 机器上安装 Microsoft SQL Server 2012 Native Client

  • 应在 PHP 机器上启用(并安装)php_sqlsrv_5*_nts.dllphp_sqlsrv_5*_ts.dll

PDO 驱动程序

PHP 中的 PDO 扩展可能是连接数据库的最佳方法。它不仅支持广泛的数据库引擎,而且还有更标准化的方式与它们交互,这使得长期支持变得更加容易(这也是长期的优势)。

不仅支持起来更容易,例如,其标准化的数据库连接和查询执行方式使得我们开发者切换起来更加容易。

对于此要求,至少需要在php.ini文件中启用一个pdo扩展,否则它将无法工作。

所有驱动程序都通过内置编译或作为库的扩展与 PHP 通信。没有这些扩展,PHP 将无法确定如何与特定库通信。一些扩展(如 Oracle 扩展)甚至需要更多,例如客户端库才能使其工作。

我们应该始终检查 php.net 文档,以了解我们尝试启用的特定扩展的要求。

连接到数据库

在看到 Zend Framework 2 支持的数据库类型之后,我们终于可以开始连接到它们了。在这个菜谱中,我们将连接到 MySQL 服务器,并展示不同的连接方式。

准备工作

为了充分利用以下菜谱,应使用一个带有 MySQL 服务器可供连接的 Zend Framework 2 骨架应用程序。别忘了连接到 MySQL 服务器需要在 PHP 中启用mysqlmysqli扩展。

如何操作…

在这个菜谱中,我们将给出一些如何连接到单个数据库或多个数据库的示例。

通过配置连接到 MySQL 数据库

我们可以对/config/autoload/global.php文件进行以下更改:

<?php

return array(

  // Set up the service manager
  'service_manager' => array(

    // Initiate the connection at the start of the 
    // application
    'factories' => array(

      // Use the service factory to start up our db 
      // adapter
      'Zend\Db\Adapter\Adapter' =>
      'Zend\Db\Adapter\AdapterServiceFactory',
    ),

    'aliases' => array(
      // Use this db alias in the controllers to get the 
      // initialized connection. The value of the db key refers to 
      // the factories key with the same name.
      'db' => 'Zend\Db\Adapter\Adapter',
    ),
  ),
  'db' => array(
    // We want to use the PDO to connect to the database
    'driver' => 'pdo',

    // DSN, or data source name is a connection url that 
    // shows the driver (in this case the PDO) where to 
    // connect to. The first bit is the driver to use, 
    // then follows the database name and the host. More    
    // information on the dsn options can be found here:
    // http://php.net/manual/en/pdo.construct.php
    'dsn' => 'mysql:dbname=some_db_name;host=localhost',

    // Username and password (or at the very least the 
    // password) should NOT be in the global.php. This 
    // file usually will be committed to a version 
    // control, which means your password will be 
    // publicly available.
    'username'  => 'aGreatUser',
    'password'  => 'somePassword',
  ),
);

如我们在示例中所见,设置数据库配置相当简单。现在,如果你想知道如何在现实世界的例子中使用这样的配置,让我们考虑以下控制器:

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class SomeController extends AbstractActionController
{
  public function indexAction()
  {
    // Get the db adapter through our service manager
    $db = $this->getServiceLocator()->get('db');

    // Now we can execute queries
    $query = $db->query('SELECT * FROM table');
  }
}

如我们所见,现在在控制器中启动它非常容易。

通过配置连接到多个数据库

一些应用程序要求我们同时连接到多个数据库,我们也可以在 Zend Framework 2 中通过在/config/autoload/global.php文件中执行以下操作轻松实现:

<?php
return array(
  'db' => array(
    'adapters' => array(
      // The first (default) database connection
      'db_one' => array(
        'driver' => 'pdo',
        'dsn' => 'mysql:dbname=db_1;host=localhost',
        'username'  => 'someUser',
        'password'  => 'aGreatPassword',
      ),

      // Now the second database connection
      'db_two' => array(
        'driver' => 'pdo',
        'dsn' => 'mysql:dbname=db_2;host=localhost',
        'username'  => 'someOtherUser',
        'password'  => 'anotherGreatPassword',
      ),
    ),
  ),
  'service_manager' => array(
    // Let's make sure our adapters get instantiated
    'abstract_factories' => array(      
      'Zend\Db\Adapter\AdapterAbstractServiceFactory',
    ),
  ),
);

在我们的控制器(或我们能够访问服务管理器的地方)中,我们可以通过以下方式轻松地获取 db DBAdapter:

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class SomeController extends AbstractActionController
{
  public function indexAction()
  {
    // Get the first db adapter
    $dbOne = $this->getServiceLocator()->get('db_one'); 

    // Get the second db adapter
    $dbOne = $this->getServiceLocator()->get('db_two');
  }
}

通过代码连接到 MySQL 数据库

虽然这种方法不如我们之前展示的方法干净,但有时通过传统的实例化连接是必要的。

首先,让我们看一个示例,如果我们想连接到一个 MySQL 服务器:

<?php 

// We need to import this to use the Db Adapter
use Zend\Db\Adapter\Adapter;

class someClass 
{
  // This is the property where our database adapter will be 
  // stored in 
  private $db;

  // First we want to connect to the database on instantiation of 
  // this class
  public function __construct()
  {
    // Create the new database adapter
    $this->db = new Adapter(array(
      'driver' => 'Pdo_Mysql', 
      'hostname' => 'localhost',
      'database' => 'example_database',
      'username' => 'developer',
      'password' => 'developer-password'
    ));
  }

  // This method will execute a query on the database, to show 
  // how easy it is to now make use of our database
  public function someData()
  {
    // Create a statement where we select everything from our 
    // tableName table
    $statement = $this->db->createStatement(
      "SELECT * FROM tableName"
    );

    return $statement->execute();
  }
}

现在,我们可以轻松地在实例化的 $db 上执行查询。

它是如何工作的…

在 Zend Framework 2 中,有许多定义数据库连接的方法,在本节中,我们将讨论其中的三种。

通过配置连接到 MySQL 数据库

我们将要展示的第一个方法是通过配置文件连接到一个(可以是任何类型)数据库。这可能是最简单的方法,但显然并不总是我们想要的。然而,在代码越少越好维护的情况下,我们应该始终考虑以这种方式连接到数据库的选项。

我们应该避免在控制器中放置业务逻辑,因为 MVC 不是为此而设计的,我们在这里只是作为示例展示。我们可以从任何有服务管理器的地方获取 db 适配器。

通过配置连接到多个数据库

如我们所见,我们现在在 db => adapters 数组中定义了适配器,而不是直接在 db 数组中。这种功能可以在任何版本大于或等于 2.2 的 Zend Framework 2 中实现。

关于 ServiceManager

当我们使用 ServiceManager 连接到我们的数据库时,ServiceManager 首先检查它是否有我们需要的键。如果找到了键,它首先在其内部注册表中检查是否有请求服务的实例。如果没有,它将使用 config 数据来实例化它。实例化完成后,它将在其内部注册表中保存引用,这样我们下次请求时可以再次检索它。这样,数据库适配器(或任何其他服务)将由 ServiceManager 只实例化一次。以这种方式实例化数据库连接有几个优点:

  • 我们始终有一个数据库连接,这通常在服务器端有限制

  • 我们不会花费宝贵的时间不断连接和重新初始化连接

  • 没有多余的内存浪费在多个实例上

执行简单查询

查询数据库显然是我们连接到数据库后需要做的事情。这个配方解释了如何做到这一点,以及可用的不同方法。

准备工作

为了充分利用以下配方,应该使用一个 Zend Framework 2 骨架应用程序,并确保有一个可连接的 MySQL 服务器。别忘了连接到 MySQL 服务器需要在 PHP 中启用 mysqlmysqli 扩展。

我们已经配置了一个名为 book 的数据库,其中包含一个名为 cards 的表,该表有 idcolortypevalue 列。创建数据库和表的 SQL 查询包含在本书的代码中。

如何做到这一点…

查询以各种形式出现,在这个菜谱中,我们将讨论一些基本的查询。

使用原始 SQL

对于这个示例,我们将编辑 /module/Application/src/Application/Controller/IndexController.php 文件:

<?php 

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Let's assume there is a service called 'db' that connect to 
    // the database
    $connection = $this->getServiceLocator()->get('db');

    // We now start to build up our query
    $query = $connection->query(
      // We will put our raw SQL statement in here, and 
      // every variable we want to put in we replace with 
      // a question mark. This means we will fill in the 
      // blanks later.
      "SELECT * FROM cards WHERE type = ?", 

      // We don't want to execute the statement yet, just 
      // prepare it.
      Adapter::QUERY_MODE_PREPARE
    );

    // These are the parameters that will replace the question 
    // marks (?) in the SQL statement above, in the defined order
    $replacements = array('number');

    // Now execute the query with the parameters attached to 
    // replace
    $result = $query->execute($replacements);

    // Iterate over the results
    foreach ($result as $res) {
      // Do something with the result, in this case a raw echo
      echo '<pre>'. print_r($res, true). '</pre>';
    }
  }
}

使用数组或 ParameterContainer 对象传递变量的示例:

// We now start to build up our query
$query = $connection->query(
  // We will put our raw SQL statement in here, and 
  // every variable we want to put in we replace with 
  // a question mark. This means we will fill in the 
  // blanks later.
  "SELECT * FROM cards WHERE type = ?", 

  // These are the parameters that will replace the question 
  // marks (?) in the SQL statement above, in the defined order
  array('number')
);

使用预处理语句

对于这个示例,我们将编辑 /module/Application/src/Application/Controller/IndexController.php 文件:

<?php 

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Let's assume there is a service called 'db' that connect to 
    // the database
    $connection = $this->getServiceLocator()->get('db');

    // Now let's create a prepared statement
    $statement = $connection->createStatement();

    // Set up the prepared statement
    $statement->setSql("
      SELECT 
      * 
      FROM cards 
      WHERE type = :type
      AND color = :color
    ");

    // Create a new parameter container to store our where 
    // parameters in
    $container = new ParameterContainer(array(
      // These are the variables used in the same order as 
      // displayed in the where condition
      'type' => 'picture', 'color' => 'diamond'
    ));

    // Set the container to be used in our statement 
    $statement->setParameterContainer($container);

    // Prepare the statement for use with the database
    $statement->prepare();

    // Now execute the statement and get the resultset
    $result = $statement->execute();

    // Iterate over the results
    foreach ($result as $res) {
      // Do something with the result, in this case a raw echo
    echo '<pre>'. print_r($res, true). '</pre>';
    }
  }
}

引用标识符

此方法将以安全的方式引用将在 SQL 查询中使用的标识符:

<?php

// Adapter is of type Zend\Db\Adapter\Adapter
echo $adapter->getPlatform()->quoteIdentifier('some_var');

上述代码将产生以下输出:

"some_var"

引用标识符链

quoteIdentifierChain 方法将引用多个标识符,并用标识符分隔符(见方法 getIdentifierSeparator())将它们粘合在一起:

<?php

// Adapter is of type Zend\Db\Adapter\Adapter
echo $adapter->getPlatform()->quoteIdentifierChain(array(
  'some_table', 'some_column'
));

上述代码将产生以下输出:

"some_table"."some_column"

引用(可信)值

quoteValuequoteTrustedValue 用于引用在 WHERE 子句中使用的值。quoteTrustedValue() 应仅在我们信任值时使用(例如,如果我们自己放入):以下是一个 quoteValuequoteTrustedValue 的示例:

<?php

// You can either use quoteValue or quoteTrustedValue,
// quoteValue will log an error in the PHP error log if 
// there is no driver or module available to quote the 
// value. Both methods output the same value.
echo $adapter->getPlatform()->quoteValue("great-value");

// Adapter is of type Zend\Db\Adapter\Adapter
echo $adapter->getPlatform()->quoteTrustedValue("great-value");

上述代码将产生以下输出:

'great-value'

引用值列表

引用值列表引用整个值列表并返回它们,用逗号分隔。例如,如果我们想在 WHERE 子句中使用 IN 操作符,这个功能就很有用。没有处理可信值的方法,因此我们应该意识到,如果没有可用的驱动程序或模块来引用值,这可能会在我们的 PHP 错误日志中触发错误,然而,它总是会返回预期的值。以下是一个 quoteValueList 的示例:

<?php

// Adapter is of type Zend\Db\Adapter\Adapter
echo $adapter->getPlatform()->quoteValueList(array(
  "value_one", "value_two"
));

上述代码将产生以下输出:

'value_one', 'value_two'

在片段中引用标识符

quoteIdentifierInFragment 方法通过正则表达式模式提取标识符,并确保只引用正确的标识符。如果我们使用以下字符之外的字符:A-z,0-9,*,"." 或 'AS',我们需要通过使用第二个参数将它们作为安全词放弃。

<?php

// Adapter is of type Zend\Db\Adapter\Adapter
echo $adapter->getPlatform()->quoteIdentifierInFragment(
  '(fork.* AS spoon)',

   // Use the braces as a safe word so that they
   // will not be quoted.
   array('(', ')')
);

上述代码将产生以下输出:

`fork`.* AS `spoon`

它是如何工作的…

让我们理解我们刚才所做的操作。

使用原始 SQL

执行 SQL 的第一种方法是在数据库连接上简单地使用 query() 方法。这是查询的最简单形式,它既有优点也有缺点,一个优点是查询快速简单,缺点是它实际上并不适用于重用,因为每次执行查询时都需要新的输入,或者每次想要执行查询时都需要传递变量。

如示例所示,我们首先以QUERY_MODE_PREPARE模式创建了一个查询,这意味着查询不会立即执行,而是仅准备执行。当我们执行查询时,我们会看到我们使用execute()方法解析WHERE子句的变量。然后,execute()语句执行查询并返回结果。

除了query()方法的第二个参数之外,我们还可以使用QUERY_MODE_EXECUTE立即执行查询(从而直接返回结果集)或解析带有参数的数组或ParameterContainer。有关ParameterContainer的更多信息,请参阅以下章节。

如果我们将数组或ParameterContainer对象作为query()方法的最后一个选项进行解析,这将导致查询参数被填充,并将查询模式设置为QUERY_MODE_PREPARE。这意味着因为我们已经将查询参数解析到query()方法中,所以我们不需要在execute()方法中再次添加它们。

使用预处理语句

query()方法被描述为一个便利函数,当我们想要保护自己免受 SQL 注入或想要使用具有不同参数的单个查询时,它并不是非常有用。另一方面,createStatement()函数提供了一个在安全且负责任的方式下存储和准备 SQL 语句以供使用的好方法。

如示例所示,我们执行了一个类似于query()方法的类似语句,然而这种方法比query()方法更易于维护和重用。通过使用ParameterContainer,我们可以轻松地将变量注入 SQL 中,并简单地因为对象具有容器性质而管理它们。

因为我们使用了:type:color,所以语句知道我们的参数数组(ParameterContainer实现了ArrayAccess类)应该包含typecolor键以匹配 SQL 语句。

在我们的 SQL 中引用

通常情况下,只要有数据库访问,就会有用户输入,而我们绝对不应该信任的正是用户输入。尽管大多数人没有黑客攻击您网站的意图,但少数恶意的人会尝试这样做。

Zend Framework 2 提供了一系列引用方法,我们可以使用这些方法来保护自己免受任何伤害。然而,我们应该注意的是,这些只是一小部分工具,您可以在预防灾难性情况时使用,我们建议使用一系列的实用工具来防止 SQL 注入。

使用createStatement

当我们使用 createStatement() 时,结果对象将通过驱动程序实例化,因此 MySQL 的语句工作方式可能与 Oracle 不同(可以,并且我认为会这样)。一旦我们创建了一个语句,它也会自动连接到数据库,这很方便,但我们必须小心,不要在不需要数据库的地方创建语句。如果我们省略了这样的事情,可能会创建不必要的泄漏,尽管可能不是那么大的泄漏,但仍然是一种泄漏。

query() 方法直接在连接适配器上工作,尽管使用起来很方便,但在实际生活中并不推荐使用,因为它不促进可重用性(就我个人而言)。如果有疑问,最好总是执行 createStatement(),除非我们只是测试一些事情,那么我们可以使用 query()

使用 TableGateway 执行查询

在我们了解了如何执行简单查询之后,现在是时候告诉你关于 TableGateway 以及它的强大功能了。这个示例完全是关于通过它查询数据库并展示其功能。

准备工作

为了充分了解以下食谱,应使用 Zend Framework 2 框架的骨架应用程序,并确保有一个可连接的 MySQL 服务器。别忘了连接到 MySQL 服务器需要在 PHP 中启用 mysqlmysqli 扩展。

如何操作...

我们首先要做的是在我们的示例表中插入一条记录。之后,我们将检查它是否成功插入。接下来,我们将使用一些新数据更新记录,如果成功了,我们将再次从表中删除它。

插入新记录

在我们开始更新记录之前,如果我们实际上有一个可以用来更新的记录,那将很有帮助。Zend Framework 2 有一些新的巧妙数据库工具,使我们在数据处理方面的生活更加轻松。

卡片表有以下列:

  • id (主键)

  • color

  • value

  • type

让我们考虑以下示例(/module/Application/src/Application/Controller/IndexController.php):

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\Db\TableGateway\TableGateway;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Let's assume there is a service called 'db' that connect to 
    // the database
    $connection = $this->getServiceLocator()->get('db');

    // Let's make this object for examples later on
    // $sql = new Sql($this->connection);

    // Create a new Zend\Db\Sql\Insert object
    // You can also do $sql->insert();
    $insert = new Insert('cards');

    // Define the columns in the table, although not 
    //required, it is best practice
    $insert->columns(array(
      'id',
      'color',
      'type',
      'value',
    ));

    // Assign the values we want to insert, the column 
    // names are in the keys so that the code knows what 
    // to insert where.
    $insert->values(array(
      'color' => 'diamond',
      'type' => 'picture',
      'value' => 'Goblin'
    ));

    // Create a new table gateway to perform our SQL on
    $tableGateway = new TableGateway(
      'cards', $connection
    );

    // We will now use the TableGateway to insert our 
    // statement in the table.
    // The insert() / insertWith() method throws an 
    // exception whenever the query goes wrong. We need to  
    // make sure we catch that.
    try {
      $tableGateway->insertWith($insert);

      // If we reach this point we can assume that the 
      // query went fine.
      echo "Insert success!";
      $hasResult = true;
    } catch (Exception $e) {
      echo "Insert failed.";
    }
  }
}

表插入操作到此结束,显然这只是在表中插入数据的一种方式。另一种执行插入语句的方法是使用我们之前创建的 $sql 对象。如果我们这样做,我们可以去掉 TableGateway 并直接使用它。

如果我们愿意,我们可以这样进行:

// This will prepare a StatementInterface for us to use
$statement = $sql->prepareStatementForSqlObject(
  // Put the insert object in here
  $insert
);

// Now we simply execute the statement to insert the 
// record.
$statement->execute();

更新记录

我们现在可以继续检查插入操作是否成功,然后我们将使用一些新数据更新记录:

// If an Exception happened, we will have a false in our 
// result.
if (isset($hasResult)) {
  // Let's get the primary key from our last insert for 
  // later use.
  $primaryKey = $tableGateway->getLastInsertValue();

  // Now let's update our record
  // You can also do $sql->update();
  $update = new Update('cards');

  // Set the new values (and column names as keys) for 
  // the data we want to update.
  $update->set(array(
    'color' => 'spade',
    'value' => '10',
    'type' => 'number',
  ));

  // Now create a where statement
  $where = new Where();

  // We want to match our record on the primary key that 
  // we got back from our insertion.
  $where->equalTo("id", $primaryKey);

  // Set the where in the update statement so that we 
  // use that when executing the update. We can add as 
  // many where statements as we like, but we only match 
  // on one here.
  $update->where($where);

  // Now update the record
  $updated = $tableGateway->updateWith($update);

更新操作的结果将是受我们的 update 语句影响的行数。在我们的例子中,这只会是一条记录,因为我们与表的主键完全匹配。

删除记录

现在,我们已经完成了所有想要更新的操作,我们想要再次开始删除这条记录,让我们看看以下代码片段:

// Delete everything again
// You can also do $sql->delete();
$delete = new Delete('cards');

// We can use the same where statement as before!
$delete->where($where);

// Now let's delete it, as there is nothing else to it.
$deleted = $tableGateway->deleteWith($delete);

好吧,这很简单。我们可以直接使用相同的 where 语句,因为它已经定义了从之前的查询中过滤主键的子句。

高级选择 - 连接条件

在开发 Web 应用程序时,我们大多数时候需要查询多个表,这是因为我们只需要从各个地方拉取大量数据以获取所需的结果。实现这一目标的一种方法是在我们的 select 语句中使用连接条件。

让我们来看看以下表组成,我们正在我们的虚拟环境中进行:

people 表将包含以下列:

  • Id(主键)

  • First_name

  • Last_name

  • Age

  • Gender

  • Address_Idaddresses 表的外键)

addresses 表将包含以下列:

  • Id(主键)

  • Street

  • Number

  • Postcode

  • City

  • Country

我们想要实现的是检索属于某个人的地址并在我们的结果中显示它。

让我们看看一个例子(/module/Application/src/Application/Controller/IndexController.php),看看我们如何以最佳方式实现这一点:

<?php 

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\Db\TableGateway\TableGateway;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Let's assume there is a service called 'db' that connect to 
    // the database
    $connection = $this->getServiceLocator()->get('db');

    // First create our Zend\Db\Sql\Sql object, and let's 
    // assume $connection has a Zend\Db\Adapter defined.
    $sql = new Sql($connection);

    // Now create a Zend\Db\Sql\Select statement with 
    // 'people' as the table we want to select from.
    $select = $sql->select('people');

    // By default we will select all the fields, but let's 
    // just change that a bit for sake of the example
    $select->columns(array('first_name', 'last_name'));

    // Now set up our join condition
    $select->join(
      // We want to join the 'addresses' table
      'addresses',

      // We now define the join condition to match the 
      // records on 
      'addresses.id = people.address_id',

      // We want to select different columns than the 
      // default wildcard selection.
      array('street', 'number', 'city', 'postcode'),

      // We want to do a LEFT JOIN on the table
      Select::JOIN_LEFT
    );

    // Now we are ready to execute the statement.
    $statement = $sql->prepareStatementForSqlObject(
      $select
    );

    // .. And finally execute it
    $records = $statement->execute();

    // Output to the screen for convenience
    echo '<pre>'. print_r($records, true). '</pre>';
  }
}

在一个 select 语句上创建一个 join 条件竟然如此简单。易如反掌!

它是如何工作的…

在 Zend Framework 2 中,他们将所有操作如InsertDropTableUpdateDeleteWhere都分离到它们自己的类中,这使得它们对开发者来说非常可重用。它的好处在于它还使代码更加清晰。

TableGatewayInterface 定义了由 AbstractTableGatewayTableGateway 实现的最小方法集合,因为 TableGateway 首先是从 AbstractTableGateway 扩展而来的。简而言之,TableGateway 实现了进行表操作所需的大部分常见功能。

因此,TableGatewayInterface 定义了以下方法:

  • getTable()

  • select($where = null)

  • insert($set)

  • update($set, $where = null)

  • delete($where)

使用 DB 分析器进行优化

在应用程序中,最常见的瓶颈之一是对数据库的查询,因为有时我们根本不知道查询了多少,或者我们无法找出为什么某些事情出了问题。这个配方为我们提供了找到甚至是最小查询的工具。

准备工作

数据库分析器用于查找查询性能的瓶颈,是调试会话中执行的查询以及它们执行所需时间的优秀工具。一旦我们开发了更大的应用程序,我们往往会忘记某些代码何时以及如何执行,这有时会导致我们的代码出现不必要的复杂性。

如何做到这一点…

分析应用程序的数据库使用情况可以清楚地了解应用程序的性能,在这个配方中,我们将讨论如何设置一个简单的分析器。

设置新的分析器

设置一个新的分析器非常简单,因为目前只有 Zend Framework 2 中的一个类可以用作分析器。这个类被称为Zend\Db\Adapter\Profiler\Profiler,可以立即实例化。让我们看一下以下代码片段:

<?php
use Zend\Db\Adapter\Profiler\Profiler;

// Instantiate the Zend\Db\Adapter\Profiler\Profiler
$profiler = new Profiler();  

// Let's assume $connection is an active Db\Adapter,
// we then need to set the profiler to be used by the 
// adapter.
$connection->setProfiler($profiler);

就这些;这基本上是开始从数据库中分析一切所需的所有内容。我们剩下要做的只是在我们完成查询(或真正需要时)获取配置文件。让我们考虑以下示例:

<?php
// This will return all the statements that have been 
// executed by the adapter.
$results = $profiler->getProfiles();

$result变量现在将填充关于执行语句的统计信息。这个结果可能看起来像以下这样:

array(3) {
   [0] => array(5) {
     ["sql"] => string(77) 
        "INSERT INTO `cards` (`color`, `type`, `value`) 
        VALUES (:color, :type, :value)"
     ["parameters"] => object(
  Zend\Db\Adapter\ParameterContainer)#255 (3) {
         ["data":protected] => array(3) {
         ["color"] => string(7) "diamond"
         ["type"] => string(7) "picture"
         ["value"] => string(6) "Goblin"
      }
     ["positions":protected] => array(3) {
       [0] => string(5) "color"
       [1] => string(4) "type"
       [2] => string(5) "value"
      }
       ["errata":protected] => array(0) {
      }
    }
     ["start"] => float(1372316727.1188)
     ["end"] => float(1372316727.1209)
     ["elapse"] => float(0.0020461082458496)
  }
}

它是如何工作的…

数据库分析器首先被附加到数据库适配器上,使适配器意识到分析器的存在。适配器将在每次执行语句时开始分析(它通过使用Profiler::profileStart()方法来完成),确保关于语句的所有重要信息都将被记录。

当数据库适配器完成语句的执行后,它将通知分析器该语句已完成(它将执行Profiler::profileFinish()方法)。

如我们从之前的示例中可以看到,我们可以查看执行的 SQL 语句以及使用的参数。之后,还会添加开始时间、结束时间和耗时,这样我们就可以轻松地发现代码中的任何潜在瓶颈。

总的来说,这是一个非常实用的工具,几乎不需要编写任何代码就能工作,并且对于想要找出数据库性能问题的开发者来说仍然非常高效。

更多内容…

另一个我们可以查看的出色的小工具是 Zend 开发者工具,这是一个由 Zend 制作的模块,适用于 Zend Framework 2,提供了非常有用的调试工具。如果我们想了解更多,我们可以在github.com/zendframework/ZendDeveloperTools找到这些工具。

创建数据库访问对象

尽管我们可以使用十几种不同的方法来标准化我们的数据库功能,但数据库访问对象(或 DAO)可以有效地用来实现这一点。这个示例是一个如何制作自己的示例,并开始组织你的功能的实际应用。

准备工作

数据库访问对象(以下简称 DAO)用于简化与我们的数据库之间的功能。DAO 背后的想法是创建具有单一功能责任的映射类。这意味着,例如,我们有一个名为cards的表,它还有一个名为Cards的映射。这个Cards映射将包含我们在该表中需要使用的所有功能。

这可能包括,例如,CRUD(创建、读取、更新和删除)功能,也可能包括更复杂的方法,如计算。映射类的理念是我们能够隐藏数据库布局,并为应用程序的其余部分提供一个可靠且一致的接口,而应用程序无需了解数据库的结构。

对于配方,我们将使用具有名为cards的表的数据库布局,以下列出了以下列:

  • id (主键)

  • color

  • value

  • type

如何做…

DAO 是一种在应用程序中组织数据库功能的好方法,这样我们就可以始终有一个清晰的逻辑结构。在这个配方中,我们将展示如何创建自己的 DAO。

创建我们的新模块和配置

我们的 DAO 将完全独立于另一个模块,因为这是分离不同代码片段的最佳方式。因此,我们继续创建一个新的模块 DAO,它应该具有以下目录结构:

module\DAO\
  config\
    module.config.php
  src\
    DAO\
  Connection\
    Connector.php
  DTO\
    Cards.php

Mapper\
  Cards.php
  MapperAbstract.php
  MapperInterface.php
  Module.php

一旦我们创建了必要的文件夹,我们可以将Application模块中的默认Module.php复制到我们的DAO文件夹中。然后我们打开我们新的Module.php,确保namespace设置为DAO

现在,是时候创建一个新的/module/DAO/config/module.config.php文件,并添加以下行:

<?php

return array(
  // This is going to be the configuration from which we 
  // will read. Obviously the username/password should 
  // be in the local.php but we will just put it here 
  // example wise.
  'dao' => array(
    'hostname' => 'localhost',
    'username' => 'some_user',
    'password' => 'some_password',
    'database' => 'book',

    // This mapper will contain all of our mapper 
    // classes such as DAO\Db\Mapper\Cards and let them 
    // know which table they need to connect to.
    'mapper' => array(
      'Cards' => 'cards',
    ),
  ),

  // Initialize our service manager so that we can reach 
  // our mappers from anywhere else in the application 
  // (every mapper should have its own entry) and our 
  // connector which should be reached only by the 
  // mappers and not anywhere else
  'service_manager' => array(
    'invokables' => array(
      'DAO_Connector' =>'DAO\Db\Connection\Connector',
      'DAO_Mapper_Cards' =>'DAO\Db\Mapper\Cards',
    ),
  ),
);

这个相当基本的配置将被我们的数据库连接器稍后用于获取连接详情。

创建一个连接器

接下来,我们想要创建我们的连接器,它基本上是一个类,将创建数据库适配器并为我们设置一切。它不会做其他任何事情,所以我们应该能够轻松地编写一个。

让我们现在创建一个名为/module/DAO/src/DAO/db/Connection/Connector.php的文件,在DAO\Db\Connection命名空间中,并添加以下代码:

<?php

// Set the correct namespace
namespace DAO\Db\Connection;

// We will be using the following classes
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Db\Adapter\Adapter;

// We are going to make this as a Service, so make sure 
// we implement the ServiceLocatorAwareInterface
class Connector implements ServiceLocatorAwareInterface 
{
  // Our service locator will be placed in here
  protected $serviceLocator;

  // Now set our service manager instance required by the 
  // ServiceLocatorAwareInterface
  public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
  {
    $this->serviceLocator = $serviceLocator;
  }

  // And add our getter for the service manager, as is required by 
  // the ServiceLocatorAwareInterface
  public function getServiceLocator()
  {
    return $this->serviceLocator;
  }

  /**
   * Initializes a connection and returns a fresh 
   * adapter.
   *
   * @return \Zend\Db\Adapter\Adapter
   * @throws \Exception
   */
  public function initialize()
  {
    // Get the configuration from the module.config.php
    $dao = $this->getServiceLocator()->get('config');

    // The following array of configuration items should 
    // be in there
    $configItems = array(
      'hostname', 
      'username', 
      'database', 
      'password'
    );

    // Check if everything is there in the configuration
    foreach ($configItems as $required) {
      if (!in_array($required, array_keys($dao['dao']))) 
      {
        // If there is a config item missing, just let
        // the develop know
        throw new \Exception("{$required} is not in the DAO configuration!");
      }
    }

    // We can assume we have everything, now set up our 
    // MySQL connection
    return new Adapter(array(
      'driver' => 'Pdo_Mysql',
      'database' => $dao['dao']['database'],
      'hostname' => $dao['dao']['hostname'],
      'username' => $dao['dao']['username'],
      'password' => $dao['dao']['password'],
    ));
  }
}

那就是类的定义;我们现在能够初始化连接,如果我们配置中有正确的项目。如果没有,该方法将抛出异常并让我们知道。

创建一个映射器接口

现在,我们想要创建一个映射器接口,我们将基于它创建所有未来的映射器类。我们这样做是因为我们想要确保所有映射器类都包含我们想要的至少一些方法。因此,我们的映射器接口将定义我们希望映射器类拥有的方法的小集合。

现在,让我们在DAO\Db\Mapper命名空间中创建一个名为/module/DAO/src/DAO/Db/Mapper/MapperInterface.php的文件,并添加以下代码:

<?php

// Make sure we have the namespace right
namespace DAO\Db\Mapper;

// Note that this is an interface, and not a regular 
// class.
interface MapperInterface
{
  // We need an insert method in our mapper.  
  public function insert($data);

  // And obviously we want to update data
  public function update($data);

  // If we want to update, we also want to delete data
  public function delete($id);

  // And of course we want to load one specific record
  public function load($id);

  // Last but not least we also want a method to get all 
  // the records in the table
  public function getAll();
}

如我们所见,这是一个相当直接的文件,因为接口实际上并没有对代码进行任何实现。

创建一个抽象映射器类

虽然接口没有实现任何代码,但抽象类可以。我们想在同一个DAO\Db\Mapper命名空间中创建一个名为/module/DAO/src/DAO/Db/Mapper/MapperAbstract.php的文件,它将包含一个创建数据库连接、指向正确的表并返回一个新鲜出炉的Zend\Db\Sql\Sql对象的方法:

<?php

// Namespace, do I need to say more ;-)
namespace DAO\Db\Mapper;

// Use the following classes
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Db\Sql\Sql;

// Note that we are again using the 
// ServiceLocatorAwareInterface and therefore need to 
// implement the getServiceLocator and setServiceLocator 
// (not shown here).
class MapperAbstract implements ServiceLocatorAwareInterface 
{
  // Our sql object will be put here
  private $sqlObject;

  // We'll just put our service locator in here	
  protected $serviceLocator;

所有的设置都完成了,现在让我们创建我们需要的连接方法(别忘了创建setServiceLocatorgetServiceLocator方法!):

// This method will set up our connection, initialize 
// the right table and return a Sql object
protected function getSqlObject()
{
  // We only want to set up our connection once, no 
  // point in doing it more, right?

  if ($this->connection === null) {
    // Get our configuration from the 
    // module.config.php
    $config = $this->getServiceLocator()->get('config');

  // Get our class name
  $class = explode('\\', get_class($this));

  // Now check if our class name is defined in the 
  // mapper configuration of the dao configuration, 
  // so that we can get our table name. Looks more 
  // complicated than it is really.
  if (isset($config['dao']['mapper']) === true && isset($config['dao']['mapper'][end($class)])) {

    // Get the database adapter from our connector
    $adapter = $this->getServiceLocator()
                    ->get('DAO_Connector')
                    ->initialize();

    // We have a configuration, now return our SQL 
    // object with the right table name included
    $this->sqlObject = new Sql(
      $adapter, 
      $config['dao']['mapper'][end($class)]
    );
      } else {
        // Make sure the developer knows not all the 
        // configuration is set.
        throw new \Exception("Configuration dao\mapper\\". end($class). " not set.");
   }
    } 

    // Now return our sql object
    return $this->sqlObject;
  }
}

我们新创建的连接方法现在可以被映射器用来获取一个与它们想要操作的表相关的Zend\Db\Sql\Sql对象。

创建数据传输对象

现在,让我们创建一个新的数据传输对象DTO)文件,名为/module/DAO/src/DAO/Db/DTO/Cards.php,在DAO\Db\DTO命名空间中,并添加以下代码:

<?php

// Namespace, quite essential
namespace DAO\Db\DTO;

// We should name our class simply Cards, as that is 
// used in the mapper later on as well
class Cards
{
  // Our 'cards' table exists of an id column, color, 
  // type and value, let's just define them as private 
  // properties.
  private $id;
  private $color;
  private $type;
  private $value;

现在我们已经设置了私有属性,我们还将为它们创建一些基本的获取器和设置器。以下是为获取器使用的代码:

public function getId() { return $this->id; }
public function getColor() { return $this->color; }
public function getType() { return $this->type; }
public function getValue() { return $this->value; }

获取器现在已经完成,这相当简单,现在让我们来做设置器:

// The id will only be set if we update a record, or 
// when we retrieve a record from a database. Never 
// when we want to insert a record.
public function setId($id) {
  $this->id = $id;
}

// Make sure we can only use colors that are valid in 
// our table.
public function setColor($color) 
{
  $validColors = array('diamond', 'spade', 'heart', 'club');

  if (in_array($color,$validColors)== false) {
    throw new \Exception(
      "Type can only be 'diamond', 'spade', 'heart'".   
      "or 'club'."
    );
  }

  $this->color = $color;
}

// Make sure only a valid type is entered.
public function setType($type) 
{
  $validTypes = array('number', 'picture');

  if (!in_array($type, $validTypes)) {
    throw new \Exception(
      "Type can only be 'number' or 'picture'."
    );
  }

  $this->type = $type;
}

// A value can only have a maximum of 6 character
public function setValue($value) 
{
  $maxValue = 6;

  if (strlen($value) >$maxValue) {
    throw new \Exception(
      "Maximum length of value is 6."
    );
  }

  $this->value = $value;
}

设置器显然要复杂一些,因为我们还想要确保我们放入的数据对我们的数据库是有效的。这样我们就可以安全地将对象解析到映射器中,并确保一切顺利。

现在,创建最后一个方法,即构造函数,这样我们就可以轻松设置属性,而无需在之后手动进行:

  public function __construct($type, $value, $color, $id = null) 
  {
    // Id is optional, so see if it is parsed or not
    if ($id !== null) $this->setId($id);

    $this->setColor($color);
    $this->setType($type);
    $this->setValue($value);
  }
}

我们现在创建了一个简单的 DTO,我们可以用它来与映射器中的某些方法进行通信。现在,最后但同样重要的是让我们创建映射器类!

创建映射器类

映射器将是我们在应用程序中使用的主体 DAO 类,因为这将是一个具有insertgetAll等方法类的类。

让我们从创建一个位于DAO\Db\Mapper命名空间中的/module/DAO/src/DAO/Db/Mapper/Cards.php文件开始,并添加以下代码:

<?php

namespace DAO\Db\Mapper;

use Zend\Db\Sql\Where;
use DAO\Db\DTO\Cards as CardsDto;
use DAO\Db\Mapper\MapperInterface;

// This class will extend and implement both our 
// Abstract as our Interface class
class Cards extends MapperAbstract implements MapperInterface
{

让我们先创建一个用于删除行的方法:

/**
 * Delete a specific row.
 * 
 * @param int $id
 */
public function delete($id) 
{
  // Get our fresh Sql object from our Abstract method
  $sql = $this->getSqlObject();

  // Create a new WHERE clause
  $where = new Where();

  // When deleting we want to match on an id
  $where->equalTo('id', $id);

  // Statements can throw exceptions, so make sure we 
  //catch them in time.
  try {
    // Create a new delete object with our where class    
    // attached and then immediately turn it into a 
    // statement. That is called pure laziness
    $statement = $sql->prepareStatementForSqlObject(               
      $sql->delete()->where($where)
    );

    // Execute the statement
    $result = $statement->execute();

    // If there is more than 0 rows deleted return 
    // true, otherwise false
    return $result->getAffectedRows() > 0;
  } catch (\Exception $e) {
    // Something went terribly wrong, just ignore it 
    // for now ;-)
    // TIP: Don't do this in real life, at least log your 
    //exceptions.
    return false;
  }
}

我们已经创建了一个简单的delete方法,现在让我们继续创建我们的getAll方法,它将检索数据库中的所有记录:

/**
 * Returns all the records in the database.
 * 
 * @return \DAO\Db\DTO\Cards
 */
public function getAll()
{
  // Get the SQL object
  $sql = $this->getSqlObject();

  // Prepare a select statement
  $statement = $sql->prepareStatementForSqlObject(
    $sql->select()
  );

  // Execute the freshly made statement
  $records = $statement->execute();

  // Create our return array
  $retval = array();

  // Loop through the records and add them to the 
  // result array
  foreach ($records as $row) {
    // Create a new Cards DTO and assign our record
    $retval[] = new CardsDto(
      $row['type'], 
      $row['value'], 
      $row['color'], 
      $row['id']
    );
  }

  return $retval;
}

在我们创建了getAll,它返回包含 Cards DTO 的数组之后,我们现在将创建一个插入记录的方法:

/**
 * Inserts a record.
 * 
 * @param \DAO\Db\DTO\Cards $data
 */
public function insert($data)
{
  // We can easily insert this as we know the DTO has 
  // already taken care of the validation of the values.
  if (!$data instanceof DAO\Db\DTO\Cards) {
    throw new \Exception(
      "Data needs to be of type DAO\Db\DTO\Cards"
    );
  }

  // Get our SQL object
  $sql = $this->getSqlObject();

  try {
    // Create our insert statement with the values 
    // assigned into it.
    $statement = $sql->prepareStatementForSqlObject(
      $sql->insert()
          ->values(array(
            'color' => $data->getColor(),
            'type' => $data->getType(),
            'value' => $data->getValue()
      ))
  );

    // Execute our statement
    $result = $statement->execute();

    // Return our primary key after insertion
    return $result->getGeneratedValue();
  } catch (\Exception $e) { 
    // Something went wrong, handle exception and 
    // return false
    return false;
    }
  }

现在,让我们继续到我们的load方法,它将只返回一条记录:

public function load($id) 
{
  // Get the SQL object
  $sql = $this->connection();

  // A fresh WHERE clause
  $where = new Where();
  $where->equalTo('id', $id);

  try {
    // Prepare a select statement with the where 
    // clause attached.
    $statement = $sql->prepareStatementForSqlObject(
      $sql->select()->where($where)
    );

    // Execute the statement and return the first row
    $record = $statement->execute()->current();

    // Now let's return a fresh Cards DTO object
    return new CardsDto(
      $record['type'], 
      $record['value'], 
      $record['color'], 
      $record['id']
    );
  } catch (\Exception $e) {
 return false;
  }
}

我们现在创建了load方法,它将为我们返回一个 Cards DTO 对象以供使用,现在最后但同样重要的是update方法:

  public function update($data) 
  {
    // We can easily insert this as we know the DTO has 
    // already taken care of the validation of the   
    // values.
    if (get_class($data) !== 'DAO\Db\DTO\Cards') {
      throw new \Exception(
        "Data needs to be of type DAO\Db\DTO\Cards"
      );
    }

    if ($data->getId() === null) {
      throw new \Exception(
           "Can't update anything if we don't have a card id!"
      );
    }

    // Get the connection
    $sql = $this->connection();

    try {
      // Create the WHERE clause
      $where = new Where();
      $where->equalTo('id', $data->getId());

      // Create the update class
      $update = $sql->update();

      // Set the where clause
      $update->where($where);
      $update->set(array(
        'color' => $data->getColor(),
        'type' => $data->getType(),
        'value' => $data->getValue()
      ));

      // Create the statement
      $statement = $sql->prepareStatementForSqlObject($update);

      // Execute the statement
      $result = $statement->execute();

      // If more than 0 rows were updated return true, 
      // otherwise false
      return $result->getAffectedRows() > 0;
    } catch (\Exception $e) { 
      return false;
    }
  }
}

我们现在已经成功创建了一个映射器类,这也标志着我们的 DAO 的结束。现在我们可以通过服务管理器(例如,在控制器/module/Cards/src/Cards/Controller/CardController.php)轻松获取映射器,使用以下代码:

<?php

namespace Cards\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class CardsController extends AbstractActionController
{
  public function viewAction()
  {
    if (!$this->getParam('id'))
        throw new \Exception("Missing id");

    // Get the record to load from the query string
    $id = $this->params()->fromQuery('id');

    // Get the card mapper from the service manager
    $cardMapper = $this->getServiceLocator()
                       ->get('DAO_Mapper_Cards');

    // Load the requested card
    $card = $cardMapper->load($id);

    // Dump the loaded record to the screen
    echo '<pre>'. Print_r($card, true). '</pre>';
  }
}

由于我们创建了一个抽象类和接口,因此我们很容易创建新的映射器。显然,这需要我们保持一致性,但这是一件好事。

它是如何工作的…

关于 DAO

DAO(数据库访问对象)是一种设计模式,它为开发者创建了一个抽象的环境,以便他们可以访问数据库相关的方法。这意味着我们创建了一个标准化的工作环境,它不仅一致,而且非常稳定。因为,我们在处理数据库查询和创建的对象的方式上限制了自己,所以我们创建了一段非常容易工作的代码。

在这个食谱中,我们创建了一个非常简单的 DAO,这在个人看来是一个很好的基础,但可能不是创建 DAO 最有效的方法。我们只是举了一个例子,说明 DAO 可以如何实现,但我们绝不应该忽视实际上有数十种不同的实现方式。

关于食谱

由于我们的配置包含一个包含所有映射类名称的映射器数组(DAO\Db\Mapper\Cards在配置中简化为 cards),我们不可能出错。这将数据库环境的本地配置与代码分离。因此,如果我们想将表名更改为'books',我们只需更改配置,代码仍然可以工作!

我们将创建一个 DTO(数据传输对象),这样我们就可以通过标准化的方式轻松地插入、更新并返回记录。因此,在我们的选择中,我们不再返回一个数组,而是返回一个包含我们所需一切内容的对象。这样我们确保我们的数据被过滤并且可以简单地传输。

正如我们在 Mapper 类中的insert方法中看到的,我们假设 DTO 对象包含我们插入记录所需的所有正确信息。尽管这个方法远非完美,但它是一种将我们的数据检查和验证分离到另一个对象(在我们的情况下是 DTO)的好方法,这样我们就可以专注于插入记录。这种分离对于良好的 DAO 工作至关重要。

第六章:模块、模型和服务

在本章中,我们将涵盖:

  • 创建一个新的模块

  • 将模块作为小部件使用

  • 一个模型和一个 Hydrator

  • 基本服务

简介

本章全部关于充分利用我们的模块、模型和服务及其配置。由于 Zend Framework 2 是一个模块化框架,模块显然是其中最重要的功能之一。我们将讨论如何自定义模块的配置以及如何与模型和服务一起工作。

创建一个新的模块

Zend Framework 2 库的核心是模块化的,一切都是基于模块化系统。这就是为什么我们将在本食谱中详细解释这一点,以便我们可以以最佳方式使用它。

准备工作

我们将使用 Zend Framework 骨干应用程序来创建新模块。提醒一下,Zend Framework 2 骨干应用程序可以在 github.com/zendframework/ZendSkeletonApplication 找到。

如何做…

创建一个新的模块就像开始一幅新的画作一样,创建新的功能既令人兴奋又有趣,但总有规则需要我们遵守。在本食谱中,我们将讨论设置新模块的规则。

创建 Module.php

我们可以开始于一个简单的类文件(即 /module/Sample/Module.php),在正确的命名空间(Sample)中没有任何内容,这是模块的唯一要求。

<?php

  namespace Sample;

  class Module {}

我们可以将以下方法添加到我们的Module类中:

  public function getConfig()
  {
    return include __DIR__ . '/config/module.config.php';
  }

现在让我们创建一个/module/Sample/config/module.config.php文件,现在它将返回一个空数组,因为我们目前实际上没有要配置的内容。

<?php

return array();

要连接到引导事件,模块只需在我们的Module.php文件中有一个onBootstrap方法,它为我们完成所有引导工作,或者我们可以定义在引导被调用时执行的引导事件(我个人最喜欢的)。

让我们看看两种方法,从onBootstrap方法开始:

public function onBootstrap(MvcEvent $e)
{
  // Let's do something on the bootstrap!
}

如我们所见,一个简单的方法就足以创建引导,一旦应用程序的引导事件被触发,它就会引导模块。

附加到 loadModules.postevent

以下示例使用了 /module/Application/Module.php 文件:

<?php 

namespace Application;

// Use the following classes
use Zend\ModuleManager\ModuleManager;
use Zend\ModuleManager\ModuleEvent;

class Module
{
  public function init(ModuleManager $moduleManager)
  {
    // We can get the event manager from our module manager
    $eventManager = $moduleManager->getEventManager();

    // Now we will attach ourselves to the event manager's event
    $eventManager->attach(
      ModuleEvent::EVENT_LOAD_MODULES_POST,
      function(ModuleEvent $event)
      {
        // Do something with our event, for example print the name  
        // of the module to the screen.
        echo '<pre>'. $event->moduleName. '</pre>';
      },
      // Make sure the rest of the triggers all have been 
      // triggered already
      -1000
    );

  }
}

实现 getAutoloaderConfig

以下示例是Module.php中的Module类的一部分:

public function getAutoloaderConfig()
{
  return array( 
    'Zend\Loader\StandardAutoloader' => array(
      'namespaces' => array(
        __NAMESPACE__ => __DIR__. '/src/'. __NAMESPACE__
      ),
    ),
  );
}

让我们考虑以下更新的代码片段:

public function getAutoloaderConfig()
{
  return array(
    'Zend\Loader\ClassMapAutoloader' => array(
      __DIR__. '/autoload_classmap.php',
    ),
    'Zend\Loader\StandardAutoloader' => array(
      'namespaces' => array(
        __NAMESPACE__ => __DIR__. '/src/'. __NAMESPACE__
      ),
    ),
  );
}

一个类映射文件的示例(文件 /module/Application/autoload_classmap.php)如下:

<?php
return array(
  'Sample\Model\Test' => __DIR__. '/src/Sample/Model/Test.php', 
  'Sample\Model\Test2' => __DIR__. '/src/Sample/Model/Test2.php',
);

实现 getControllerConfig、getControllerPluginConfig 和 getViewHelperConfig

看看以下getViewHelperConfig的实现(在/module/Application/Module.php文件中):

<?php

namespace Application;

// We need this for the view helper config to be picked up
use Zend\ModuleManager\Feature\ViewHelperProviderInterface;

class ModuleViewHelperProviderInterface
{
  public function getViewHelperConfig()   
  {
    // See if the class exists first, to show off that we can use 
    return array(
      'invokables' => array(
          // This is a non existing view helper, but is just to 
          // show off how to use it.
          // Note: You cannot use a closure as an invokable.
          'exampleHelp' => 'Application\View\Helper\Example',
       )
    );
  }
}

它是如何工作的…

模块在application.config.php文件中引入后,由框架实例化。将模块的名称添加到文件中,框架将寻找名为模块的目录中的Module.php文件。Module.php文件包含一系列方法,这些方法将在框架在特定时间调用,例如加载配置或运行模块的引导。

在我们的示例中,我们将创建一个名为Sample的模块,它将有一个简单的控制器和一个输出一些文本的操作。

为了确保 Zend Framework 2 的ModuleManager能够识别我们的新模块,我们需要了解ModuleManager是如何工作的。ModuleManager执行的操作有三个:

  • 它收集已启用的模块

  • 如果需要,它初始化模块

  • 它从所有模块收集配置

虽然我们可以使用ZFTool自动创建一个全新的模块,但我们仍然建议我们了解如何在没有它的情况下创建和构建模块。现在,我们将开始创建一个确保ModuleManager满意的模块。

创建一个新的模块目录

当创建一个新模块时,我们将尽可能地遵循推荐的方式,以便我们能够清楚地了解它是如何工作的。首先,在模块目录中创建一个名为Sample的新目录。这个目录将成为我们与 Sample 模块命名空间相关的代码的主要目录,这样我们就可以将所有相关的代码都包含在这个目录中。

创建Module.php

每个模块最重要的文件是Module.php文件,它不仅是必需的,而且还向框架提供了有关诸如代码位置和配置等重要信息的重要信息。

虽然它实际上不会在模块中初始化任何内容,但拥有一个模块的基本要求。请注意,由于Module.php中缺少代码,我们的应用程序无法访问模块内部的任何代码。

我们首先想确保框架能够读取我们模块的配置。这可以通过在Module.php中定义一个getConfig方法来实现,该方法需要一个数组作为返回值。

因为懒惰也是一种技能,我们只需将完整的module.config.php文件返回给ModuleManager。我们不必这样做,我们也可以返回一个包含配置的数组,但为了便于维护,最好将实际的配置与代码分开。这样我们就不必编辑代码来编辑配置。

现在我们知道我们的ModuleManager将加载我们的配置,是时候回顾模块的引导过程了,这在配置加载后有时是必要的。这可以通过在Module.php中使用onBootstrap方法或附加到ModuleManager事件来实现。

可选地处理 ModuleManager 事件

确保额外的代码片段将被执行的另一种方式是将它们附加到四个其他战略事件之一,即:loadModulesloadModules.resolveloadModuleloadModules.post

为了更好地解释它们,让我们简要地了解一下所有这些。

理解 loadModules 事件

当框架加载模块时,将触发 loadModules 事件,因此对于初始化模块,此事件几乎毫无用处,因为它永远不会在 Module.php 文件中调用(此时事件已经过去)。

在这一点上,框架仍在加载模块,我们的模块还没有发生任何事情。这就是为什么这个事件主要在框架的内部使用,而不是在我们的开发侧使用。然而,由于这个事件在整个加载模块的过程中都是活跃的,当所有其他事件都完成后,它也会做一些额外的事情。

此事件默认触发以下功能:

  • Zend\Loader\ModuleAutoloader::register:确保 Module 类可以被找到并启动(它还没有启动,只是检查)。

  • Zend\ModuleManager\Listener\ConfigListener ::onLoadModulesPre ::onLoadModulesPost:当所有模块都已加载时,此功能会将配置文件与通过应用程序配置中定义的 glob() 找到的本地配置文件合并,但仅当配置未内部缓存时(默认情况下不是这种情况)。

  • Zend\ModuleManager\Listener\LocatorRegistration::onLoadModulesPost:如果 Module 类实现了 LocatorRegisteredInterface 接口,则此操作会将模块的服务附加到 ServiceManager,并将 Module 类立即添加到 DI 中。这是在所有模块都已加载时完成的。

loadModules.resolve 事件

另一个内部事件,模块无法使用的事件是此事件,它为我们在 application.config.php 中定义的每个模块触发。实际上,此事件将尝试在我们的模块的 Module.php 文件中找到 Module 类,所以虽然对我们模块(目前)没有用,但它已经接近了!

此事件默认触发以下功能:

  • Zend\ModuleManager\Listener\ModuleResolverListener::__invoke:启动 Module 类。

loadModule 事件

现在,Module 类的对象已经创建;loadModule 事件将通过其他监听器传递它。

此事件默认触发以下功能:

  • Zend\ModuleManager\Listener\ConfigListener::onLoadModule:通过获取所有 Module 类的 getConfig() 来合并配置。

  • Zend\ModuleManager\Listener\AutoloaderListener::__invoke:如果可用,此操作会在 Module 类中调用 getAutoloaderConfig,以便我们可以为新模块启动自动加载。

  • Zend\ModuleManager\Listener\InitTrigger::__invoke:如果可用,此方法会调用Module类中的 init 方法。

  • Zend\ModuleManager\Listener\OnBootstrapListener::__invoke:这会将Module类的onBootstrap方法附加到应用程序的引导事件,因此它将在那时运行。

  • Zend\ModuleManager\Listener\ServiceListener::onLoadModule:如果存在,此方法会调用Module类中的以下方法(我们将在本食谱的稍后部分更详细地讨论这些方法):

    • getServiceConfig:从模块类获取ServiceManager配置。

    • getControllerConfig:从Module类获取控制器配置。

    • getControllerPluginConfig:从Module类获取控制器插件配置。

    • getViewHelperConfig:从Module类获取视图助手配置。

显示模块加载简化版本的流程图如下:

模块加载事件

loadModules.post

当模块成功加载并且需要完成最后一些工作以完成整个过程时,会触发loadModules.post事件。

此事件默认触发Zend\ModuleManager\Listener\ServiceListener::onLoadModulesPost功能,并指示ServiceManager根据需要创建更多服务。

附加到 loadModules.post 事件

loadModules.post事件是我们可以在应用程序中附加处理程序的第一个事件,因为在此事件之前的事件只能由 Zend Framework 2 的内部监听器使用。这意味着没有很好的方法可以挂钩到这些事件,而不需要对我们自己的框架进行扩展。

然而,loadModules.post事件仍然可能很有用,例如,确保我们的模块被正确加载,或者用于其他与模块配置相关的事情。将我们自身附加到这个事件的最佳方式是通过尽可能高地使用EventManager。在这种情况下,这将是模块的init()方法,因为该方法在loadModule事件期间被调用,并且是第一个包含EventManager的方法。

更具体的非配置文件模块配置

有时我们选择不始终使用module.config.php文件,并需要更动态的实例化,例如,服务或配置。幸运的是,Zend Framework 2 完全支持任何动态配置功能。如前所述,我们可以向我们的Module类添加五个额外的方法,这些方法在模块实例化期间被拾取,分别是getAutoloaderConfiggetServiceConfiggetControllerConfiggetControllerPluginConfiggetViewHelperConfiguration

getAutoloaderConfig 方法

getAutoloaderConfig方法将加载我们模块的自动加载器配置,并期望一个与AutoloaderFactory兼容的数组。在 Zend Framework 2 中,通常有两种接受的方式来自动加载。第一种是使用StandardAutoloader,它需要一个要加载的命名空间和一个要递归的目录。第二种是使用ClassMapAutoloader,它基本上是一个包含每个完整域名和类名及其对特定文件的引用的数组的文件。

它们在示例中都显示出来了,所以请查看它们以了解差异。

在第一个例子中,我们使用StandardAutoloader是因为我们只想让我们的框架通过[当前目录]/src/Sample目录中的目录结构加载命名空间__NAMESPACE__(对于我们的模块来说是Sample)中的所有类。这意味着完全在Sample\Model\Test中调用的类将在/src/Sample/Model/Test.php中搜索。虽然这在开发环境中非常方便,但在生产环境中并不方便,因为大型应用程序会对搜索我们需要的类名造成很大压力。在这种情况下,我们可以使用这个StandardAutoloader,但除此之外(具有更高的优先级),我们还将使用一个ClassMapAutoloader,它加载一个静态文件,其中包含所有类名映射到特定目录。

这告诉 PHP,当我们搜索类Sample\Model\Test时,它可以在/src/Sample/Model/Test.php(或者实际上任何地方,因为我们直接将 PHP 指向我们的文件)中找到。这两个自动加载器都是 PSR-0,其中 PSR 代表 PHP 标准建议的兼容性。

在第二个例子中,我们可以看到我们优先考虑了我们的autoload_classmap.php文件,而不是StandardAutoloader,这意味着它将首先在我们的类映射文件中查找,然后再尝试自己查找。

要使框架使用getAutoloaderConfig方法,我们必须确保我们的Module类实现了Zend\ModuleManager\Feature\AutoloaderProviderInterface类,并且它包含单个公共方法getAutoloaderConfig(),否则它将不会尝试执行它。记住,仅仅实现该方法是不够的,因为它会具体检查我们是否实现了接口。

getControllerConfiggetControllerPluginConfiggetViewHelperConfig方法

我们可以通过get***Config方法,而不是通过module.config.php或作为覆盖来加载控制器配置。我们可以像getServiceConfig方法一样创建该方法,因为返回的对象可以是Zend\ServiceManager\Config的实例,或者是一个包含配置的简单数组,就像在module.config.php中一样。

如果我们想使用这些方法,我们不应该忘记用相应的接口实现我们的类:

对于 getControllerConfig 方法,我们需要实现 Zend\ModuleManager\Feature\ControllerProviderInterface 接口。

对于 getControllerPluginConfig 方法,我们需要实现 Zend\ModuleManager\Feature\ControllerPluginProviderInterface 接口。

最后,对于 getViewHelperConfig 方法,我们需要实现 Zend\ModuleManager\Feature\ViewHelperProviderInterface 接口。

将模块作为小部件使用

将模块作为小部件使用是一个很好的方法,可以在我们应用程序的不同位置使用模块。这就是为什么这个配方将解释我们如何以最佳方式完成这项工作。

准备工作

需要一个可工作的 Zend Framework 2 骨架应用程序才能充分利用这个配方。

如何做到这一点…

小部件,听起来就很好!我们将在这个配方中解释它们的作用以及如何使用它们。

创建 Comment/Controller/Index

我们将创建一个小控制器,它将返回一些示例评论,这些评论是静态的,仅用于示例。首先,我们应该确保我们有一个Comment模块,因此我们创建以下目录和文件:

module/
   Comment/
      config/
         module.config.php
      src/
         Comment/
            Controller/
               IndexController.php
      view/
         comment/
            index/
               index.phtml
      Module.php

一旦我们建立了结构,我们就在/module/Comment/Module.php中尽可能简单地放置代码以初始化模块,如下所示:

<?php

namespace Comment;

class Module
{
  // Get our module configuration
  public function getConfig()
  {
    return include __DIR__ 
         . '/config/module.config.php';
  }

  // Initialize our autoloader to load in our sources
  public function getAutoloaderConfig()
  {
    return array(
      'Zend\Loader\StandardAutoloader' => array(
        'namespaces' => array(
          __NAMESPACE__ => __DIR__. '/src/'
                         . __NAMESPACE__,
        ),
      ),
    );
  }
}

如我们所见,这是一个最基本的Module类,因为我们不需要比这更高级的。现在让我们快速在/module/Comment/config目录中创建我们的module.config.php配置文件:

<?php

return array(
  // Set up a quick route to our comment output
  'router' => array(
    'routes' => array(
      'comment' => array(
        'type' => 'Zend\Mvc\Router\Http\Literal',
        'options' => array(
          'route'    => '/comment',
          'defaults' => array(
            'controller' => 'Comment\Controller\Index',
            'action'     => 'index',
          ),
        ),
      ),
    ),
  ),

  // Make sure the controllers are invokable by us
  'controllers' => array(
    'invokables' => array(
      'Comment\Controller\Index' =>
                    'Comment\Controller\IndexController'
    ),
  ),

  // Set the path to our view templates
  'view_manager' => array(
    'template_path_stack' => array(
       __DIR__ . '/../view',
    ),
  ),
);

现在我们已经使用一个响应/路径并映射到Comment\Controller\IndexController::indexAction的快速配置设置好了,我们可以继续处理实际的控制器(位于文件/module/Comment/src/Comment/Controller/IndexController.php):

<?php

namespace Comment\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
  // This is the action that will be called whenever we 
  // browse to /comment
  public function indexAction()
  {
    // Initialize our view model
    $view = new ViewModel();
    $comments = array();

    // Create some static comments and put them in our 
    // comments array
    for ($i = 0; $i < 10; $i++) {
      $comments[] = array(
          'name' => 'John Doe ('. $i. '),
          'comment' => 'Lorem ipsum dolor sit amet...'
      );
    }

    // Return our view with the comments and make sure 
    // the renderer doesn't output our layout 
    // (setTerminal(true) does that)
    return $view->setVariable('comments', $comments)
                ->setTerminal(true);
  }
}

在创建我们的控制器之后,我们还需要创建的是视图脚本(位于文件/module/Comment/view/comment/index/index.phtml),以便实际以 HTML 表格的形式输出数据:

<?php /* loop through the comments to display them */ ?>
<?php foreach ($this->comments as $comment) : ?>
  <tr>
    <td>
      <?php echo $comment['name'] ?>:
    </td>
    <td>
      <?php echo $comment['comment'] ?>
    </td>
  </tr>
<?php endforeach; ?>

现在我们已经完全设置了我们的模块,我们可以继续并以小部件的形式显示评论。

使用视图助手来静态显示评论

首先,我们想要创建视图助手本身,让我们在Comment模块(文件是/module/Comment/src/Comment/View/Helper/Comments.php)中这样做,因为数据无论如何都是从那里来的:

<?php

namespace Comment\View\Helper;

use Zend\View\Helper\AbstractHelper;
use Comment\Controller\IndexController;

class Comments extends AbstractHelper
{
  public function __invoke()
  {
    // Instantiate the controller with the comments
    $controller = new IndexController();

    // Execute our indexAction to retrieve the 
    // ViewModel, and then add the template of that 
    // ViewModel so it renders fine
    $model = $controller->indexAction()->setTemplate(
              'comment/index/index'
    );

    // Now return our rendered view
    return $this->getView()
               ->render($model);
  }
}

现在我们需要做的只是在我们能够将其用于视图之前,将这个视图助手添加到我们的模块配置(文件是/module/Comment/config/module.config.php)中:

  // Add our custom view helper to the configuration
  'view_helpers' => array(
    'invokables' => array(
      'comments' => 'Comment\View\Helper\Comments',
    ),
  ),

显然,我们在这里省略了其余的配置,因为我们不想重复自己。现在剩下的就是实际上在代码中使用新的视图助手。我们可以在视图脚本中添加以下代码行:

<?php echo $this->comments() ?>

使用转发来静态渲染评论

让我们看看CommentController(文件是/module/Application/src/Application/Controller/CommentController.php)中forward()操作的代码片段:

public function forwardAction()
{
  $view = new ViewModel();

  // Get the comments from the index action 
  $comments = $this->forward()
                   ->dispatch(
    // Which controller do we want to invoke
    'Comment\Controller\Index', 

    // Any specific options we want to give it
    array('action' => 'index')
  );

  // If we keep this on true it will return an 
  // exception, so let us not do that
  $comments->setTerminal(false);

  // Return the view model with the comments as child
  return $view->addChild($comments, 'comments');
}

这获取了特定控制器(我们的 Comment\Controller\Index::indexAction)中动作的已分发状态,并将其作为 $comments 返回给我们,这是一个 ViewModel 实例。我们将它作为子实例添加到当前的 ViewModel 实例中,然后我们可以在视图脚本中使用以下代码片段简单地输出它:

<?php echo $this->comments ?>

这与输出一个正常变量相同,尽管这给人一种干净解决方案的感觉,但 forward() 方法在压力下是出了名的糟糕。

通过 AJAX 获取评论

让我们看看我们的视图脚本在 JavaScript 中的样子:

<!-- our comments will load in here -->
<table class="comments"></table>

<!-- first we want to make sure that we load in the jQuery script that comes with the Zend Framework 2 skeleton application -->
<script src="img/jquery.min.js') ?>"></script>

<!-- this is the JavaScript bit -->
<script>
  // This means jQuery will execute this code whenever 
  // the document is done loading
  $(document).ready(function() {
    // We want to do a GET request in the background
    $.get(
      // We want to get this URL
      '/comment', 

      // This function will be executed when the 
      // data comes back from the server
      function(data) {
        // Put our data (the comments) in our 
        // comments table
       $('table.comments').html(data);
      }
    );
  });
</script>

它是如何工作的…

这是这种情况:我们有一个页面,其中包含一个用户应该能够评论的小故事。然而,评论部分在代码的几个其他位置也被使用,因此应该是可重用的。但是有一个前提,评论部分在布局上不会改变,它总是需要以相同的方式显示。

我们将要创建三个不同但有效的模块实现,该模块被用作小部件。前两个将给整个系统带来更多的静态感,而第三个将使用 JavaScript(确切地说,是 jQuery)来加载评论。我们还将讨论一个理论上的第四种解决方案,这应该被考虑。

但首先,我们将设置一个小环境,我们将在获取评论的示例中使用它。

我们将设置 Application/Controller/Comment 控制器,该控制器将定义 helperActionforwardActionajaxAction 方法。然后我们将使用这个控制器和动作在 Comment/Controller/Index 控制器和 indexAction 方法中显示评论。

使用视图助手静态显示评论

在静态方式显示评论的最佳选项是创建一个特定于此小部件的视图助手。我们将要做的是创建一个小视图助手,它将渲染我们的评论并将它们返回到我们的视图。这样我们就可以在我们的视图中到处使用它,而无需像 forward() 或 AJAX 方法那样造成很多麻烦。

正如我们在示例中所见,我们实例化控制器并手动检索动作的输出,然后手动渲染并返回给视图。这样做并不总是这么简单,但它接近现实。

使用 forward() 方法静态渲染评论

另一个不太好的想法但也值得提及的是,通过 forward() 方法获取评论,这种方法虽然脆弱,但至少它不像 AJAX 功能那样需要经过整个 MVC 初始化。

通过 AJAX 获取评论

最后但同样重要的是,当我们想要更加创新,或者我们的环境需要异步 AJAX 实现时,我们也有一个更技术性的非 PHP 解决方案。这种方法的想法是,我们简单地通过 JavaScript(具体来说是 jQuery 库)从 URL 中检索我们的评论。

这只需要我们在视图脚本中输入一点客户端 JavaScript 代码来使其工作,这很好,因为我们不需要在代码中做太多调整。然而,它有一个很大的缺点,那就是我们又将经历整个 MVC 流程来从数据库接收评论。另一方面,它将加快我们主要动作的响应时间,因为它不需要静态地加载评论。另一个缺点是,访问网站的访客需要启用 JavaScript 的浏览器才能看到评论,但我们假设现在每个人都有这样的浏览器。

如我们从示例中看到的那样,这是一个检索评论相当简单的方法,但它附带有之前提到的缺点。然而,有时从其他地方获取数据在性能上可能是最佳选择。这完全归因于应用程序的架构。

关于小部件化

将模块小部件化并不是框架绝对原生支持的功能,但正如上文所述,我们可以通过尽可能多地使用(而不是滥用)框架来轻松实现这一点。

尤其是自行实例化控制器和执行动作是处理应用程序其他部分数据的一种极好方法。然而,我们必须小心,模块本身应该是独立的(或者至少尽可能独立),我们不应该过分依赖它们的存在。

但公平地说,完美的状况永远不会出现,我们有时只需要做一些妥协。在我们的情况下,这可能是依赖于可能不存在的模块。

模型和水化器

模型是向我们的应用程序提供功能的一种极好方式,并且它们将控制器保持得非常干净,远离任何关键逻辑。水化器也非常适合在模型之间传输属性和值,这就是为什么我们要进一步探讨它,以便充分利用它。

准备工作

为了充分利用这个菜谱中的示例,需要一个可工作的 Zend Framework 2 框架骨架应用程序。

如何操作…

在这个菜谱中,我们将设置一个模型和一种将数据从模型中填充到模型以及从模型中提取数据的方法,以便我们能够轻松访问我们的数据。

访问模型

我们可以通过在文档顶部简单地添加一个 use 语句在任何地方访问模型:

use Application\Model\SampleModel;

$object = new SampleModel();

或者通过使用包括命名空间在内的类的完全限定名称,如下所示:

$object = new \Application\Model\SampleModel();

如果已经存在一个名为 SampleModel 的类,但来自不同的命名空间,或者如果我们只想给它一个更易识别的名字,我们也可以使用别名(但这不是模型特有的,我们可以在任何命名空间类中使用它),如下所示:

use Application\Model\SampleModel as NewModel;

$object = new NewModel();

创建水化器

现在的首要任务是设置一个非常简单的模型(文件位于 /module/Application/src/Application/Model/SampleModel.php),我们将使用它来进行数据填充,如下所示:

<?php

namespace Application\Model;

class SampleModel 
{
  private $engine;
  private $primary;
  private $text;

  public function getEngine() {
    return $this->engine;
  }

  public function setEngine($engines) {
    $this->engine = $engines;
  }

  public function getPrimary() {
    return $this->primary;
  }

  public function setPrimary($primary) {
    $this->primary = $primary;
  }

  public function getText() {
    return $this->text;
  }
  public function setText($text) {
    $this->text = $text;
  }
}

这个极其基础的模型除了几个属性及其 getter 和 setter 之外,没有其他东西,简单,但它将适用于我们试图实现的目标。在接下来的示例中,我们将为我们的虚拟数据库表创建一个Hydrator,然后我们将使用表中的数据水化我们的SampleModel(文件位于/module/Application/src/Application/Model/Hydrator/SampleModelHydrator.php):

<?php

// Don't forget to namespace our class
namespace Application\Model\Hydrator;

// We extend from this class
use Zend\Stdlib\Hydrator\AbstractHydrator;

class SampleModelHydrator extends AbstractHydrator
{
  private $mapping = array(
    'id' => 'primary',
    'value' => 'engine',
    'description' => 'text',
  );

  // Extracts the hydrated model   
  public function extract($object) {}

  // Hydrates our values to our model
  public function hydrate(array $data, $object) {}
}

我们现在已经设置了 hydrator 的基本类,现在实现的方法只是由于AbstractHydrator类我们需要拥有的定义。接下来我们想要做的是在其中有代码来真正使其工作。我们将进一步实现的是hydrate()方法,这将使我们的SampleModel变得可水化:

public function hydrate(array $data, $object) 
{
  // If we are not receiving an object, throw an 
  // exception
  if (is_object($object) === false) {
    throw new \Exception(
      "We expect object to be an actual object!"
    );
  }

  // Loop through the properties and values 
  foreach ($data as $property=>$value) {
    // Check if the property exists in our mapping
    if (array_key_exists($property, $this->mapping)) {
      // Build the setter method from our property
      $setter = 'set'. ucfirst(
         $this->mapping[$property]
      );

    // Set the value of the property
    $object->$setter($value);
    }
  }

  // Now return our hydrated object
  return $object;
}

现在,让我们使用extract()方法,它从我们的SampleModel中提取值,并将它们放回一个数组中,这个数组也是我们最初用于水化对象的方式:

public function extract($object) 
{
  // If we are not receiving an object, throw an 
  // exception
  if (is_object($object) === false) {
    throw new \Exception(
         "We expect object to be an actual object!"
      );
  }

  $return = array();

  foreach ($this->mapping as $key=>$map) {
    // Build the getter method from our property
    $getter = 'get'. ucfirst($map);

    // Get the property value from the object
    $return[$key] = $object->$getter();
  }

  return $return;
}

那就是如何再次从水化对象中提取值。

创建水化策略

如果我们稍微改变SampleModel(文件位于/module/Application/src/Application/Model/SampleModel.php)中的主属性设置器,使其反映在以下代码片段中:

public function setPrimary($primary) 
{
  // Throw an exception if there is no valid integer.
  if (!is_int($primary)) {
    throw new \Exception(
       "Primary ({$primary}) should be an integer!"
    );
  }

  $this->primary = $primary;
}

让我们先创建我们的策略(文件位于/module/Application/src/Application/Model/Hydrator/Strategy/SampleHydratorStrategy.php):

<?php

namespace Application\Model\Hydrator\Strategy;

// We need to implement this interface to make it 
// eligible to be a strategy
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;

class SampleHydratorStrategy implements StrategyInterface
{

  // This method is called every time an object is 
  // extracted
  public function extract($value) 
  {
    // Check if the value is an integer
    if (is_int($value) === true) { 
      return (int)$value;
    } else {
      // No integer, just randomly return an integer
      return rand(0, 10000);
    }
  }

  // This method is called just before the property of 
  // the object is hydrated
  public function hydrate($value) 
  {
    // Check if it is a valid integer
    if (is_int($value) === true) {
      return (int)$value;
    } else {
      // No integer, random integer is returned
      return rand(0, 10000);
    }
  }
}

现在,我们需要在我们的Hydrator类中更改两个东西,以便它也支持水化策略(文件位于/module/Application/src/Application/Model/Hydrator/SampleModelHydrator.php):

public function extract($object)
{
  [.. current code in between ..]

  $return[$key] = $this->extractValue(
      $key, $object->$getter()
  );

  [.. rest of the code ..] 
}

public function hydrate(array $data, $object)
{
  [.. current code in between ..]

  $object->$setter($this->hydrateValue(
      $this->mapping[$property], $value)
  );

  [.. rest of the code ..]
}

正如我们所见,我们只需更改之前显示的代码行,以确保它将使用我们的Hydrator中的水化策略。

在下一个示例中,我们将使用Hydrator将我们的SampleModel水化到我们的控制器中(文件位于/module/Application/src/Application/Controller/IndexController.php):

<?php

namespace Application\Controller;

use Application\Model\SampleModel;
use Application\Model\Hydrator\SampleModelHydrator;
use Application\Model\Hydrator\Strategy\SampleHydratorStrategy;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // First initialize our model
    $model = new \Application\Model\SampleModel();

    // Now create a sample array of data to hydrate
    $data = array(
      'id' => 'Some Id',
      'value' => 'Some Awesome Value',
      'description' => 'Pecunia non olet',
    );
    // Now create our Hydrator
    $hydrator = new SampleModelHydrator();

    // Now add our strategy to it to check when the primary 
    // value is set (if we put id, it would be when the 
    // value would be retrieved)
    $hydrator->addStrategy(
        "primary", 
        new SampleHydratorStrategy()
    );

    // Now hydrate our model
    $newObject = $hydrator->hydrate($data, $model);

    // And if necessary extract the values again
    $extract = $hydrator->extract($newObject); 

    // Now output it to the browser
    echo "<pre>". print_r($extract, true). "</pre>";
  }
}

如果我们知道比较提取值与原始值,我们可以看到 ID 现在已更改为随机数,这告诉我们水化策略已经完成了其工作。

它是如何工作的…

考虑模型的目的

根据定义,模型应该只包含与应用程序一个非常具体的部分相关的功能。这意味着如果我们开始编写模型,我们应该注意这个要求,并使我们的模型轻量级且针对单一目的。

有很多小块代码的想法是我们可以更容易地维护它们,我们只加载我们想要使用的。而不是加载一个包含所有所需功能的 40k 行长的模型,我们希望将它们拆分成小型功能类,这些类只做它们名字所表示的事情。

考虑模型的位置

模型的位置特别重要,因为我们仍然希望能够在我们的代码中找到它。我们应该给它一个与其功能相似的名字,并且它应该尽可能具体。

如果我们需要命名一个模型,我们还需要将其放置在一个有意义的地点,这样当我们寻找某些功能时,我们只需在最有意义的位置搜索即可找到它。

例如,让我们看看以下命名空间和类名:

Api\Model\Db\User\Information

如果我们搜索一个检索用户信息的方法,那么这个类将是开始搜索的好方法。

考虑模型的方法

模型的方法显然是模型最重要的部分,它也是通常被高度忽视的部分。例如,开发者有时在定义方法时使用错误的可见性,这导致其他开发者错误地使用它们,因为他们认为可以(或不能)使用特定方法,因为其可见性。

有时方法命名不正确或可见性设置错误,结果我们不得不重构代码。所有这些都可以通过事先考虑来避免。

小贴士

也很明智地为你的方法命名正确,正确放置可见性,并在我们的应用程序中使用严格的命名约定。

方法名应该通过camelCase命名,并且只有受保护和私有方法可以以下划线开头。

单元测试模型

测试你的模型是确保方法输出始终与预期输出匹配的绝佳方式。一个更伟大的(个人)开发模型的方式是进行 TDD(测试驱动开发),这样你就有了一个客观的测试,而不是主观的测试,如果你在编写方法之后编写测试。我们将在第九章捕捉错误中更多地讨论单元测试和 TDD。

记录你的类

通常,在类被忽视和/或未维护的同时,它应该是你日常工作中存在的东西。即使我们是这个项目唯一的开发者,并且我们知道十年后我们仍然是唯一的开发者,这仍然是一个很好的方式,让未来的我们知道我们为什么创建那个方法,它做什么,以及我们可以期望它返回什么。

PHP DocBlock,简称 PHPDoc,是我们以注释格式记录代码的正式标准。首先,docblocks 可以通过以下语法来识别:

/**
 * This is used to describe the method, file or class.
 *
 * @param string $parameterOne Some description here.
 * @result Boolean
 * @throws Some\Exception
 * @author J. Callaars <bcallaars@gmail.com>
 */
public function someMethod($parameterOne);

如我们所见,普通注释块与文档块之间的区别在于开头使用的两个星号。之后的第一行应始终描述当前的方法、类或文件(无论上下文如何)。随后的行包含标签,这些标签用于定义文档块的一些属性。例如,@param 标签用于定义方法参数,这些参数具有定义的类型和其后参数的名称。@result 表达了方法调用的返回值,而 @throws 告诉我们在这个方法中可能会抛出异常。最后但同样重要的是,@author 告诉我们谁最初创建了该方法/文件/类。

显然,还有许多其他标签可以使用,其中大多数可以在 en.wikipedia.org/wiki/PHPDoc 找到。

我们建议使用 phpDocumenter 语法作为创建方法和类文档的标准,因为它是一个行业标准,并为我们提供了轻松生成技术文档的选项。

创建填充器

填充器是那些可以用来使用给定的值填充特定类的类。

这在从数据库表检索数据并将其映射到另一个模型时特别有用,其中模型不需要知道表和 TableGateway 如何将它们映射到模型。在这种情况下,Hydrator 作为模型和数据访问层之间的中介是完美的。

mapping 属性定义了接收到的数组(我们用它来填充)与对象侧属性之间的映射。例如,如果我们的数组包含一个键 ID,我们将在对象中设置 primary 属性。显然,这是 hydrate 方法可能的最基本形式,因为它只是检查我们是否有一个有效的对象,然后检查我们是否具有想要设置的属性名称,如果存在则设置它。

创建填充器策略

现在我们有一个简单的填充器,我们可能想看看 Zend 框架中的另一件新奇的部件:填充器策略。简单地说,填充器策略是将正在解析的值转换成 Hydrator 的过程。

我们现在已更改填充器的首要设置器,以便当它接收到除整数之外的其他内容时,将抛出异常。

但我们的 Hydrator 不熟悉模型中的属性,这反过来意味着当使用不兼容的值时,将抛出异常。为了克服这个问题(以及许多其他问题),我们可以使用填充器策略,它将在值传递到模型之前有最后的机会设置值。

现在的计划是我们将创建一个 hydrator 策略,该策略将检查我们的主要属性并确保它返回一个整数。正如我们稍后可以看到的,我们基本上创建了一个extract和一个hydrate方法,这些方法将检查是否存在整数值,如果不存在,则返回一个随机整数。这样我们就可以确保进入我们模型的所有值至少是我们期望的类型。

关于模型

模型只是普通的类,与任何其他类没有区别。然而,模型背后的原则是所有业务关键逻辑都定义在其中。MVC 倾向于拥有瘦控制器(这意味着没有或几乎没有逻辑)和胖模型。

另一方面,hydrator 是用于模型之间的类,例如,在从一个模型到另一个模型或从TableGateway到模型以及相反的数据交换时使用。显然,我们编写的每个模型都不需要Hydrator,但随着应用程序的增长,我们喜欢在不改变现有功能的情况下实现新功能,而Hydrator可以作为关键因素,因为它们可以作为对象之间的代理。

还有更多…

还有更多关于 hydrator 的内容要写,特别是与 Zend Framework 2 一起提供的不同类型的默认 hydrator。如果我们想了解更多关于这方面的信息,我们应该查看Zend\Stdlib\Hydrator\ArraySerializableZend\Stdlib\Hydrator\ClassMethodsZend\Stdlib\Hydrator\ObjectProperty hydrator 的文档。

基本服务

Zend Framework 2 最大的特性之一是ServiceManager,其在我们应用程序的初始引导阶段就可以看到它的影响。我们不需要理由来解释为什么这个菜谱会在这个主题上深入探讨,对吧?

准备工作

再次强调,为了充分利用这个菜谱中的示例,应该有一个 Zend Framework 2 骨架应用程序正在运行。

在我们继续之前,让我们弄清楚服务和模型之间的区别。尽管服务的定义有时是一个判断性的选择,但可以安全地假设服务是一个位于控制器和模型之间的类,它隐藏了控制器中所有糟糕的逻辑,例如检查身份验证或调用模型中的方法。另一个不同之处在于,我们案例中的服务将由ServiceManager管理,因此可以从我们的应用程序中的任何控制器(和其他服务)中调用。

如何做…

服务是确保我们的功能可以在应用程序的几乎任何地方访问的绝佳方式,在这个菜谱中,我们将展示如何做到这一点!

创建服务

我们将在/module/Application/src/Application/Service/Example.php文件中创建我们的服务:

<?php

namespace Application\Service;

use Zend\ServiceManager\ServiceLocatorAwareInterface,
    Zend\ServiceManager\ServiceLocatorInterface;

class Example implements ServiceLocatorAwareInterface 
{
  protected $serviceLocator;

  // This is set by our initialization so we don't 
  // actually have to do this ourselves probably
  public function setServiceLocator(ServiceLocatorInterface $serviceLocator) 
  {
    $this->serviceLocator = $serviceLocator;
  }

  // Retrieve the service locator, handy if we want to 
  // read some configuration
  public function getServiceLocator() 
  {
    return $this->serviceLocator;
  }

  // Let's create a simple string to rot13 encoder as an 
  // example
  public function encodeMyString($string)
  {
    return str_rot13($string);
  }
}

现在唯一剩下的事情是将此服务添加到模块配置(文件是/module/Application/config/module.config.php)中,这样它也可以被应用程序的其余部分访问:

<?php 
return array(
  'service_manager' => array(
    'invokables' => array(
      // We are going to call our service through the 
      // ExampleService name
      'ExampleService' => 'Application\Service\Example',
    ),
  ),
);

当然,这又是一个片段,用来展示需要添加到配置中的内容。现在,我们可以通过执行以下操作轻松地在控制器中检索服务,例如:

// This is an example from within a controller and 
// returns a rot13 encoded string
echo $this->getServiceLocator()
          ->get('ExampleService')
          ->encodeMyString("Service? Easily created!");

在控制器中获取服务

这个例子表明,从控制器中检索服务非常简单。在服务内部,我们也可以通过执行以下操作轻松地获取我们的主要应用程序配置:

// This is executed from within a service class and will 
// return the configuration of the application
$config = $this->getServiceLocator()->get('config');

它是如何工作的...

我们创建了一个非常基本的服务并将其添加到我们的 Application 模块的配置中。其背后的想法是,我们可以展示创建服务、激活它以及在应用程序中使用它的简单性。我们将创建一个将由 ServiceManager 管理的服务,它所做的只是对字符串进行 rot13 编码。

要创建一个服务,我们只需要在我们的类中实现 Zend\ServiceManager\ServiceLocatorAwareInterface,它预定义了两个方法,即 getServiceLocatorsetServiceLocatorsetServiceLocator 在实例化期间被调用,大多数时候(至少在我们将服务添加到配置中时)我们不需要手动执行此操作。

然而,getServiceLocator 是一个我们可以用来获取 ServiceLocator 的方法,从它我们可以获取其他服务或可能是应用程序本身的配置等有用的东西。

服务是在模块加载时实例化的,如果它们在模块配置中,或者只是在应用程序的某个地方。然而,当我们实例化服务时,我们知道我们总是可以通过 ServiceLocator 的相同简单 get() 方法在任何其他地方获取它,一旦它被实例化。

第七章. 处理身份验证

在本章中,我们将涵盖:

  • 理解身份验证方法

  • 设置简单的数据库身份验证

  • 编写自定义身份验证方法

简介

在本章中,我们将讨论不同的身份验证方法,并展示一些如何进行身份验证以及如何创建自己的身份验证方法的示例。

理解身份验证方法

在互联网安全如此重要的情况下,对强大身份验证方法的需求是不可或缺的。因此,Zend Framework 2 提供了一系列适合每个人需求的身份验证方法。

准备工作

为了充分利用这个配方,我建议设置一个可工作的 Zend Framework 2 骨架应用程序。

如何实现...

以下是在 Zend Framework 2 中 readily 可用的身份验证方法列表——或称为适配器。我们将提供适配器的小概览以及如何使用它的说明。

DbTable 适配器

构建DbTable适配器相当简单,如果我们看看以下构造函数:

public function __construct(
  // The Zend\Db\Adapter\Adapter
  DbAdapter $zendDb,

  // The table table name to query on
  $tableName = null,

  // The column that serves as 'username'
  $identityColumn = null,

  // The column that serves as 'password'
  $credentialColumn = null,

  // Any optional treatment of the password before 
  // checking, such as MD5(?), SHA1(?), etcetera
  $credentialTreatment = null
);

Http 适配器

在构建对象之后,我们需要定义FileResolver以确保确实解析了用户详情。

根据我们在accept_schemes选项中配置的内容,FileResolver可以设置为BasicResolverDigestResolver或两者兼而有之。

让我们快速了解一下如何将FileResolver设置为DigestResolverBasicResolver(我们在/module/Application/src/Application/Controller/IndexController.php文件中这样做):

<?php

namespace Application;

// Use the FileResolver, and also the Http 
// authentication adapter.
use Zend\Authentication\Adapter\Http\FileResolver;
use Zend\Authentication\Adapter\Http;
use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
  public function indexAction()
  {
    // Create a new FileResolver and read in our file to use 
    // in the Basic authentication
    $basicResolver = new FileResolver();
    $basicResolver->setFile(
      '/some/file/with/credentials.txt'
    );

    // Now create a FileResolver to read in our Digest file
    $digestResolver = new FileResolver();
    $digestResolver->setFile(
      '/some/other/file/with/credentials.txt'
    );

    // Options doesn't really matter at this point, we can 
    // fill them in to anything we like
    $adapter = new Http($options);

    // Now set our DigestResolver/BasicResolver, depending 
    // on our $options set
    $adapter->setBasicResolver($basicResolver);
    $adapter->setDigestResolver($digestResolver);
  }
}

它是如何工作的...

在两个简短的示例之后,让我们看看其他可用的适配器。

DbTable 适配器(再次)

让我们从所有适配器中最常用的一个开始,即DbTable适配器。这个适配器连接到数据库,并从表中提取所需的用户名/密码组合,如果一切顺利,它将返回一个身份,这不过是一个与用户名详情匹配的记录。

要实例化适配器,它需要在构造函数中提供一个Zend\Db\Adapter\Adapter以连接到数据库并使用用户详情;还有一些其他选项可以设置。让我们看看构造函数的定义:

第二个(tableName)选项不言自明,因为它只是表名,我们需要用它来获取我们的用户,第三个和第四个(identityColumncredentialColumn)选项是逻辑上的,它们代表我们表中的用户名和密码(或我们使用的)列。然而,最后一个选项credentialTreatment可能不太容易理解。

credentialTreatment告诉适配器在尝试查询之前用函数处理credentialColumn。如果是在 MySQL 数据库中,这可以是MD5(?)函数、PASSWORD(?)SHA1(?)函数,但显然这也可以根据数据库的不同而有所不同。为了给出一个关于 SQL 如何看起来的小例子(实际的适配器会以不同的方式构建这个查询),请看以下示例:

有凭证处理:

SELECT * FROM `users` WHERE `username` = 'some_user' AND `password` = MD5('some_password');

没有凭证处理:

SELECT * FROM `users` WHERE `username` = 'some_user' AND `password` = 'some_password';

当定义处理时,我们应该始终包括一个问号,表示密码需要出现的位置,例如,MD5(?)将创建MD5('some_password'),但没有问号则不会插入密码。

最后,我们不仅可以通过构造函数提供选项,还可以使用属性设置器方法:setTableName()setIdentityColumn()setCredentialColumn()setCredentialTreatment()

Http 适配器(再次)

HTTP 认证适配器是我们可能在互联网生活中至少遇到过一次的适配器。当我们访问一个网站并且弹出一个窗口显示我们可以填写用户名和密码以继续时,我们可以识别出这种认证。

这种认证方式非常基础,但在某些实现中仍然非常有效,因此它是 Zend Framework 2 的一部分。然而,这种认证有一个很大的问题,那就是(在使用基本认证时)它可以通过浏览器发送用户名和密码的明文(ouch!)。

然而,有一个解决方案,那就是使用摘要认证,这种认证也由这个适配器支持。

如果我们查看这个适配器的构造函数,我们会看到以下代码行:

public function __construct(array $config);

构造函数接受其config参数中的大量键,如下所示:

  • accept_schemes:这指的是我们想要接受的认证方式;这可以是basicdigestbasic digest

  • realm:这是我们对所在领域的描述,例如“会员区域”。这是针对用户的,仅用于描述用户登录的目的。

  • digest_domains:这是认证正在工作的 URLs。所以如果用户在任何定义的 URL 上使用他的详细信息登录,它们将有效。URLs 应该定义在一个空格分隔的(奇怪,对吧?)列表中,例如 /members/area /members/login

  • nonce_timeout:这将设置 nonce(当我们使用摘要认证时,用户登录时使用的哈希)有效的秒数。请注意,然而,nonce 跟踪和过时支持在 2.2 版本中尚未实现,这意味着每次 nonce 超时时都会重新认证。

  • use_opaque:这是一个布尔值(默认为 true),告诉我们的适配器向客户端发送不透明头。不透明头是服务器发送的字符串,需要在身份验证时返回。不过,在 Microsoft Internet Explorer 浏览器上有时不起作用,因为它们似乎忽略了该头。理想情况下,不透明头应该是一个不断变化的字符串,以减少可预测性,但 ZF 2 不随机化字符串,总是返回相同的哈希。

  • algorithm:这包括用于身份验证的算法,它需要是一个在 supportedAlgos 属性中定义的支持算法。目前只有 MD5。

  • proxy_auth:这个布尔值(默认为 false)告诉我们所使用的身份验证是否是代理身份验证。

应该注意的是,在使用 Digest 或 Basic 时,文件之间略有差异。尽管这两个文件具有相同的布局,但它们不能互换使用,因为 Digest 需要将凭证进行 MD5 哈希,而 Basic 需要将凭证作为纯文本。在每条凭证之后都应该有一个新行,这意味着凭证文件中的最后一行应该是空的。

凭证文件的布局如下:

username:realm:credentials

例如:

some_user:My Awesome Realm:clear text password

除了 FileResolver,还可以使用 ApacheResolver,它可以用来读取 htpasswd 生成的文件,这在已经有此类文件的情况下非常有用。

Digest 适配器

Digest 适配器基本上是没有任何基本身份验证的 Http 适配器。由于它的理念与 Http 适配器相同,我们只需继续讨论构造函数,因为它的实现略有不同:

public function __construct($filename = null, $realm = null, $identity = null, $credential = null);

如我们所见,在构建对象时可以设置以下选项:

  • filename:这是用于 Digest 凭证的直接文件名,因此无需使用 FileResolver

  • realm:这用于向用户标识他/她正在登录的系统,例如 My Awesome RealmThe Dragonborn's lair。由于我们在构建此文件时立即尝试登录,因此它确实需要与凭证文件(参见 Http 适配器 中的凭证文件布局)相匹配。

  • identity:这是我们尝试登录的用户名,并且它需要与凭证文件中定义的用户相似才能正常工作。

  • credential:这是我们尝试登录的 Digest 密码,并且它需要与凭证文件中的密码完全匹配。

然后,例如,我们可以运行 $digestAdapter->getIdentity() 来找出我们是否成功进行了身份验证,如果没有成功,则返回 NULL,如果成功,则返回身份列的值。

LDAP 适配器

使用 LDAP 认证显然要难解释一些,所以我们不会全面介绍,因为这会花费相当长的时间。我们将展示 LDAP 适配器的构造函数并解释其各种选项。然而,如果我们想了解更多关于设置 LDAP 连接的信息,我们应该查看 ZF2 的文档,因为那里解释得很好:

public function __construct(array $options = array(), $identity = null, $credential = null);

构造函数中的选项参数指的是一个与 Zend\Ldap\Ldap 配置兼容的配置选项数组。这里可以设置的实际选项有数十种,所以我们建议查看 ZF2 的 LDAP 文档以了解更多信息。接下来的两个参数 identity 和 credential 分别是用户名和密码,这一点很容易理解。

一旦与 LDAP 建立了连接,就没有太多的事情要做,只剩下获取身份并查看我们是否成功验证。

关于认证

在 Zend Framework 2 中,认证通过特定的适配器实现,这些适配器始终是 Zend\Authentication\Adapter\AdapterInterface 的实现,因此始终提供了那里定义的方法。然而,认证的方法各不相同,对之前显示的方法有深入了解始终是必需的。有些方法通过浏览器工作,如 HttpDigest 适配器,而有些则要求我们创建整个实现,如 LDAPDbTable 适配器。

设置简单的数据库认证

在看到所有可用的认证方法后,是时候看看当设置了数据库认证时它实际上会如何工作了。这个菜谱将解释这个特定方法的方方面面。

准备工作

一个带有 PHP sqlite 扩展已加载和启用的有效 Zend Framework 2 框架应用骨架。

如何操作…

数据库认证可能是最广泛使用的认证方法。在这个菜谱中,我们将设置自己的数据库认证。

设置模块初始化

我们将在模块初始化后尽快创建数据库,因此我们将它附加到名为 route 或 MvcEvent::EVENT_ROUTE 的事件。作为 Module.php 的模板,我们只需复制 Application/Module.php 文件并更改命名空间;我们无论如何都会在 onBootstrap 方法中工作,Module 类的其余部分可以保持不变(但别忘了更改命名空间!)。

让我们看看我们的 /module/Authentication/Module.php 文件的代码:

// We can assume the rest of the Module class file is 
// exactly the same as the default 
// Application/Module.php file, except of course the 
// namespace.
public function onBootstrap(MvcEvent $e)
{
  // This is also default    
  $eventManager = $e->getApplication()->getEventManager();
  $moduleRouteListener = new ModuleRouteListener();
  $moduleRouteListener->attach($eventManager);
  // And now we let the magic happen (this is the bit we 
  // will insert)
  $eventManager->attach(
    // We want to attach to the route event, which means   
    // it happens before our controllers are initialized 
    // (because that would mean we already found the 
    // route)
    MvcEvent::EVENT_ROUTE,

    // We are using this function as our callback 
    function (MvcEvent $event) 
    {
     // Get the database adapter from the configuration
     $dbAdapter = $event->getApplication()
                         ->getServiceManager()
                         ->get('db');

     // Our example is an in memory database, so the 
     // table never exists, but better sure than sorry
     $result = $dbAdapter->query("
             SELECT name 
            FROM sqlite_master 
           WHERE type='table' AND name='users'
     ")->execute();

      // If we couldn't find a users table, we will 
      // create one now (with an in memory db this is 
      // always the case)
     if ($result->current() === false) {
       try {
         // The user table doesn't exist yet, so let's 
         // just create some sample data
         $result = $dbAdapter->query("
            CREATE TABLE `users` (
              `id` INT(10) NOT NULL,
              `username` VARCHAR(20) NOT NULL,
              `password` CHAR(32) NOT NULL,
            PRIMARY KEY (`id`)
            )
           ")->execute();

         // Now insert some users
         $dbAdapter->query("
         INSERT INTO `users` VALUES 
           (1, 'admin', '". md5("adminpassword"). "')
          ")->execute();

         $dbAdapter->query("
           INSERT INTO `users` VALUES 
             (2, 'test', '". md5("testpassword"). "')
             ")->execute();		
        } catch (\Exception $e) {
        \Zend\Debug\Debug::dump($e->getMessage());
      }
    }
  });
}

我们现在创建了一个事件,当开始路由时会触发。如果我们仔细观察,我们可以找到一个肯定会导致代码崩溃的大错误。问题当然在于 ServiceManager 中的 db key,因为我们引用了一个尚未创建的服务。所以让我们开始动手,创建那个 /module/Authentication/config/module.config.php 文件…

<?php

return array(
  // Let's initialize the ServiceManager
  'service_manager' => array(
    'factories' => array(
      // Create a Db Adapter on initialization of the 
      // ServiceManager
      'Zend\Db\Adapter\Adapter' =>
          'Zend\Db\Adapter\AdapterServiceFactory',
    ),

    // Let's give this Db Adapter the alias db
    'aliases' => array(
      'db' => 'Zend\Db\Adapter\Adapter',
    ),
  ),

  // We will now configure our Sqlite database, for 
  // which we only need these two lines
  'db' => array(
    'driver' => 'Pdo_Sqlite',
    'database' => ':memory:',
  ),
);

就这样;我们的基本配置已经完成,以便数据库开始运行,如果我们现在运行代码,我们可以确信数据库已经创建。

创建认证服务

我们接下来要做的事情是创建我们的认证服务,这个服务将帮助我们的应用程序完成所有的认证功能。让我们在Authentication\Service命名空间中创建这个服务,并将其命名为Authentication(文件位于/module/Authentication/src/Authentication/Service/Authentication.php)。

<?php

// Set the namespace
namespace Authentication\Service;

use Zend\ServiceManager\ServiceLocatorAwareInterface;

// We give this one an alias, because otherwise 
// DbTable might confuse us in thinking that it is  
// an actual db table
use Zend\Authentication\Adapter\DbTable as AuthDbTable;
use Zend\Authentication\Storage\Session;

// We want to make a service, so we implement the 
// ServiceLocatorAwareInterface for that as well
class Authentication implements ServiceLocatorAwareInterface
{
  // Storage for our service locator
  private $servicelocator;

  // Get the ServiceManager
  public function getServiceLocator() 
  {
    return $this->servicelocator;
  }

  // Set the ServiceManager
  public function setServiceLocator(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator) 
  {
    $this->servicelocator = $serviceLocator;
  }

好的,这很简单;我们只是创建了一个服务……但目前它实际上什么都没做。让我们首先创建一个检查我们是否已经认证的方法。我们通过检查认证会话,看它是否为空来完成这个操作。假设在这种情况下,我们只有当实际上已经认证时才有一个(认证!)会话,我们可以安全地同意我们将登录;

  /**
   * Lets us know if we are authenticated or not.
   * 
   * @return boolean
   */
  public function isAuthenticated()
  {
    // Check if the authentication session is empty, if 
    // not we assume we are authenticated
    $session = new Session();

    // Return false if the session IS empty, and true if 
    // the session ISN'T empty
    return !$session->isEmpty();
  }

我们可以轻松地打开一个会话,因为会话的命名空间仅用于认证目的。

让我们现在创建我们的认证服务,它将验证用户名和密码,并返回一个布尔值,表示我们是否成功认证:

  /**
   * Authenticates the user against the Authentication 
   * adapter.
   * 
   * @param string $username
   * @param string $password
   * @return boolean
   */
  public function authenticate($username, $password)
  {
    // Create our authentication adapter, and set our 
    // DbAdapter (the one we created before) by getting 
    // it from the ServiceManager. Also tell the adapter 
    // to use table 'users', where 'username' is the 
    // identity and 'password' is the credential column
    $authentication = new AuthDbTable(
      $this->getServiceLocator()->get('db'),
      'users',
      'username',
      'password'
    );

    // We use md5 in here because SQLite doesn't have 
    // any functionality to encrypt strings
    $result = $authentication->setIdentity($username)
                             ->setCredential(md5($password))
                             ->authenticate();

    // Check if we are successfully authenticated or not
    if ($result->isValid() === true) {
      // Now save the identity to the session
      $session = new Session();
      $session->write($result->getIdentity());
    }

    return $result->isValid();
  }

正如我们在之前的代码片段中看到的,我们创建了一个简单的认证方法,它返回 true 或 false,这取决于我们是否已经认证。它还做的一件事是将身份保存到认证会话中,这样我们就可以在我们的上一个方法中看到我们是否已经认证。当我们想要从登录用户那里获取用户名时,我们也需要在会话中获取身份,这可以通过以下方法实现:

  /**
   * Gets the identity of the user, if available, 
   * otherwise returns false.
   * @return array
   */
  public function getIdentity()
  {
    // Clear out the session, we are done here
    $session = new Session();

    // Check if the session is empty, if not return the 
    // identity of the logged in user
    if ($session->isEmpty() === false) {
      return $session->read();
    } else {
      return false;
    }
  }

现在我们已经获取了我们的身份,我们能够注销也同样重要。在我们的情况下,这很简单,只需清除会话即可,因为我们为什么要让它比这更复杂呢?

  /**
   * Logs the user out by clearing the session.
   */
  public function logout()
  {
    // Clear out the session, we are done here
    $session = new Session();
    $session->clear();
  }

  // This is our last method, close the bracket for the 
  // class as well!
}

我们现在已经创建了一个简单的认证服务,现在剩下的唯一部分就是将其注册到服务管理器中,以便在启动时自动实例化。我们可以像往常一样在/module/Authentication/config/module.config.php文件中完成这项操作,因为我们已经有一个service_manager配置在那里,我们只需将可调用的实例放入其中:

<?php
return array(
  'service_manager' => array(
    // [The rest of the service manager configuration 
    // comes here]

    // And our new invokable can be put here
    'invokables' => array(
    'AuthService' => 'Authentication\Service\Authentication',
    ),
  ),
);

对于服务来说这就结束了!现在我们唯一要做的就是创建login/logout操作,然后检查我们是否已经登录。让我们从login/logout操作开始,这样我们实际上能够登录!

设置控制器和操作

让我们在那里修改/module/Authentication/config/module.config.php文件,这样我们就可以访问我们的login/logout操作,这对我们来说非常重要:

<?php
return array(
  // [The configuration that we have now resides here..]

  // And our route configuration comes here..
  'router' => array(
    'routes' => array(
      'authentication' => array(
        'type'    => 'Literal',
        'options' => array(
          'route'    => '/authentication',
          'defaults' => array(
          '__NAMESPACE__' => 
                  'Authentication\Controller',
          'controller'    => 'Index',
          'action'        => 'login',
        ),
      ),
      'may_terminate' => true,
      'child_routes' => array(
        'default' => array(
          'type'    => 'Segment',
          'options' => array(
            'route'    => '[/:action]',
            'constraints' => array(
              'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
            ),
            'defaults' => array(),
          ),
        ),
      ),
    ),
  ),
),

// Make our controller invokable
'controllers' => array(
  'invokables' => array(
    'Authentication\Controller\Index' =>   
          'Authentication\Controller\IndexController'
    ),
  ),

  // Make sure our template path is set correctly
  'view_manager' => array(
    'template_path_stack' => array(
      __DIR__ . '/../view',
    ),
  ),

);

这个基本的路由只是将 /authentication 重定向到我们的 loginAction,由于段路由,我们可以简单地通过 /authentication/logout 重定向到我们的 logoutAction;如果需要更多关于路由的解释,我们可以回顾第一章,Zend Framework 2 基础,并查看 Handling routines 菜单。

让我们继续在 Authentication\Controller 命名空间中创建我们的 /module/Authentication/src/Authentication/Controller/IndexController

<?php

namespace Authentication\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
}

我们已经简单地声明了我们的控制器;现在让我们添加 logoutAction(我们将从它开始,因为它非常简单)和 loginAction

public function logoutAction()
{
  // Log out the user
  $this->getServiceLocator()
       ->get('AuthService')
       ->logout();

  // Redirect the user back to the login screen
  $this->redirect()
       ->toRoute('authentication');
}

如我们所见,这几乎是太简单了,但如果它能工作,我们不会抱怨。现在让我们创建我们的 loginAction,它基本上检查是否有帖子,如果有,就尝试登录,否则显示登录表单。登录成功后,我们将被重定向到 /application 路由,如果没有成功,我们只显示一个错误消息:

public function loginAction()
{
  // See if we are trying to authenticate
  if ($this->params()->fromPost('username') !== null) {
    // Try to authenticate with our post variables from 
    // the form we just send
    $done = $this->getServiceLocator()
                 ->get('AuthService')
                 ->authenticate(
        $this->params()->fromPost('username'),
        $this->params()->fromPost('password')
    );

    if ($done === true) {
      $this->redirect()
           ->toRoute('application');
    } else {
      \Zend\Debug\Debug::dump(
        "Username/password unknown!"
      );
    }
  }

  // On an unsuccessful attempt or just a get request 
  // show the form.
  return new ViewModel();
}

如我们所见,loginAction 只是在检查是否有任何帖子,如果有,它就允许 AuthService 处理它。这种方式并不完美,因为它不检查恶意参数或任何东西,但它确实展示了控制器应该是多么干净,除了必要的变量解析之外,没有登录。

logoutAction 不包含视图脚本,因为这个动作只是重定向用户,并且永远不会有自己的响应。然而,loginAction 确实包含视图脚本,因为它需要显示一个表单。现在让我们快速为 loginAction 建立一个视图脚本(文件位于 /module/Authentication/view/authentication/index/login.phtml):

<form action="/authentication" method="post">
  <label for="username">Username:</label>
  <input type="text" name="username" />

  <label for="password">Password:</label>
  <input type="password" name="password" />

  <button type="submit">Login</button>
</form>

一个简单的登录表单,在我看来不需要任何解释。

我们最后想要确保的是,如果用户未登录,他们不能访问应用程序中的任何内容,除了认证。我们可以通过在 Authentication 模块的 Module(文件位于 /module/Authentication/Module.php)类中添加一个新的事件来实现这一点,该事件将检查我们是否已登录,如果没有,则在屏幕输出任何内容之前将我们重定向:

public function onBootstrap(MvcEvent $e)
{
  // Get the event manager from the event
  $eventManager = $e->getApplication()->getEventManager();

  // Attach the module route listeners
  $moduleRouteListener = new ModuleRouteListener();
  $moduleRouteListener->attach($eventManager);

  // Do this event when dispatch is triggered, on the 
  // highest priority (1)
  $eventManager->attach(
      MvcEvent::EVENT_DISPATCH, 
      function (MvcEvent $event) {
      // We don't have to redirect if we are in a 
      // 'public' area, so don't even try
      if ($event->getRouteMatch()->getMatchedRouteName() 
                  === 'authentication') return;

      // See if we are authenticated, if not lets 
      // redirect to our login page
      if ($event->getApplication()->getServiceManager()
                ->get('AuthService')->isAuthenticated() === 
          false) 
      {
        // Get the response from the event
        $response = $event->getResponse();

        // Clear current headers and add our Location 
        // redirection
        $response->getHeaders()
                 ->clearHeaders()
                 ->addHeaderLine(
             'Location', '/authentication'
        );

        // Set the status code to redirect
        $response->setStatusCode(302)
                 ->sendHeaders();

        // Don't forget to exit the application, as we 
        // don't want anything to overrule at this point
        exit;
      }
  }, 

  // Give this event priority 1
  1);
}

需要的事件就是这样的,发生的情况是,每次我们尝试访问非认证路由时,我们都会被重定向到登录页面。

它是如何工作的…

我们将要创建的是一个简单的数据库认证,它通过内存中的 SQLite 数据库工作。这意味着数据库不会被存储,每次我们请求页面时,所有表和记录都需要重新构建。显然,这在生产环境中使用非常不方便,但它确实可以很好地展示它是如何工作的,并且对于快速启动非常有用。

假设我们正在使用默认的 Zend Skeleton 应用程序,让我们创建一个新的认证模块。这个新模块将包含数据库连接、认证本身以及登录和注销操作。当我们为新的模块创建目录时,我们还应该小心地将新模块添加到application.config.php文件中,否则我们可能会遇到麻烦,不知道为什么它不起作用(哦,是的,我是在从经验中说的)。

首先,我们在Module.php中为认证构建了我们的内存数据库。然后我们创建了一个名为users的表,包含一个唯一的 ID、用户名和密码。ID 由一个整数组成,用户名是一个 20 位的可变字符,密码将是一个 32 个字符,因为这是 MD5 加密字符串的大小。

因为我们已经设置了一个用户表,并将其连接到认证适配器,所以我们能够简单地验证用户名和密码。作为额外的措施,我们确保用户在没有登录的情况下不能访问除登录页面之外的任何页面,这是通过使用在输出发送给用户之前发生的事件来实现的。

编写自定义认证方法

有时候,标准方法可能不够用,这是完全可以接受的。这就是为什么这个食谱清楚地说明了如何创建我们自己的认证方法。

准备工作

对于这个食谱,如果有一个启用了 SSL 的 Web 环境会更好。配置这样的环境超出了范围,但它对执行这个食谱会有好处。

这样的一个环境示例将是正确配置了mod_ssl的 Apache 2 Web 服务器。要在 Apache2 上启用证书验证,需要在他们的public/.htaccess文件中放置以下代码:

# Only execute the following code when mod_ssl is 
# enabled
<IfModule mod_ssl.c>
  # This means the client can present their 
  # certificate, but it doesn't need to be verifiable 
  # by the server
  SSLVerifyClient optional_no_ca

  # This depth means the certificate can only be self-
  # signed otherwise it will be denied
  SSLVerifyDepth 0

  # We want to export the standard variables but also 
  # the certificate data as well to use in PHP
  SSLOptions +StdEnvVars +ExportCertData 
</IfModule>

另一个需要提到的重要事情是,PHP 应该配置(和编译)带有--with-openssl参数,否则解析证书的代码将不存在,因此我们无法使用该代码。有关如何做到这一点的更多信息,请参阅www.php.net/manual/en/book.openssl.php

如何做到这一点...

证书认证可能很少见,但有时当安全性略高于普通 Web 应用时,会使用它。在这个食谱中,我们将展示一个基于证书的认证示例。

创建我们的适配器

让我们开始创建我们的新适配器,这将是我们想要尽可能与当前认证适配器集成的Zend\Authentication\Adapter\AdapterInterface的实现。

由于我们已经有了一个来自上一个食谱的认证模块,我们只需将其作为我们将要工作的命名空间即可;就像之前描述的那样简单。

首先,让我们创建适配器(文件位于/module/Authentication/src/Authentication/Adapter/Certificate.php)。

适配器概述

我们首先从我们的基本类轮廓开始:

// Set the right namespace
namespace Authentication\Adapter;

// We will use this to implement the right methods
use Zend\Authentication\Adapter\AdapterInterface;

// Out class name, not to forget the implementation
class Certificate implements AdapterInterface
{
  // Currently authenticate is the only method required 
  // for the AdapterInterface; lucky us!
  public function authenticate() {}
}

为任何错误消息创建 getter 和 setter

通常我们会在开发过程中逐步想出错误消息。但因为这个代码已经完成,我们已经有定义好的错误消息。没有很好的方法来描述这些 getter 和 setter,所以我们将在以下代码片段中展示它们,以便至少可以清楚地了解发生了什么(文件位于/module/Authentication/src/Authentication/Adapter/Certificate.php):

// After coding the adapter we found the following 
// errors that need to be relayed to the user/developer

// Invalid certificate, there is no certificate set
const AUTH_FAIL_INV_CERT = 0;

// Insecure connection, no HTTPS
const AUTH_FAIL_NO_HTTPS = 1;

// Couldn't parse the certificate, invalid certificate
const AUTH_FAIL_PARSE_CERT = 2;

// Certificate is expired
const AUTH_FAIL_EXP_CERT = 3;

// Not all the required fields we need are in the 
// certificate, thus rendering it invalid
const AUTH_FAIL_NOT_ALL_FIELDS = 4;

// No Database adapter was provided
const AUTH_FAIL_NO_DB_ADAPTER = 5;

// An error occurred in the SQL
const AUTH_FAIL_SQL_ERR = 6;

// The user requested couldn't be found
const AUTH_FAIL_NO_USER = 7;

// By default we have no error
private $error = -1;

这些是我们考虑到的错误消息,稍后将在代码的某个地方使用。现在让我们创建这些错误消息的 setter,以便 getter 可以轻松地在以后检索它们:

/**
 * Sets an error.
 * 
 * @param int $error
 */
private function setError($error) 
{
  $this->error = $error;
}

好吧,那真是有趣。现在让我们创建 getter,它稍微复杂一些,但只是稍微复杂一点:

/**
 * Gets the latest error message back.
 * 
 * @return string
 */
public function getErrorMessage() 
{
  switch ($this->error) {
    case self::AUTH_FAIL_SQL_ERR:
      $retval = "SQL error occurred while checking " 
              . "for the user.";
      break;
    case self::AUTH_FAIL_INV_CERT:
      $retval = "Certificate provided is invalid.";
      break;
    case self::AUTH_FAIL_PARSE_CERT:
      $retval = "Certificate provided couldn't be "
              . "parsed.";
      break;
    case self::AUTH_FAIL_EXP_CERT:
      $retval = "Certificate has expired.";
      break;
    case self::AUTH_FAIL_NO_DB_ADAPTER:
      $retval = "No Database adapter set.";
      break;
    case self::AUTH_FAIL_NOT_ALL_FIELDS:
      $retval = "Not all the fields required are " 
              . "available.";
      break;
    case self::AUTH_FAIL_NO_USER:
      $retval = "The user could not be found.";
      break;
    case self::AUTH_FAIL_NO_HTTPS:
      $retval = "Connection is not secure.";
      break;
    case -1:
      $retval = "No error occurred.";
      break;
    default:
      $retval = "Unknown error occurred.";
      break;
  }

  // Reset the error
  $this->error = -1;

  // Return the string with the error message
  return $retval;
}

确保我们有一个安全的连接

虽然证书只有在我们有 SSL 连接时才会发送,但额外的检查并不坏,因为我们想确保用户正在使用安全的连接。

/**
 * Returns true if the current connection is through 
 * HTTPS.
 * 
 * @return boolean
 */
private function isHTTPS()
{
  return isset($_SERVER['HTTPS']) ? true : false;
} 

哇,那肯定是有史以来最好的方法!开个玩笑,它相当简单,因为HTTPS键在$_SERVER变量中给出,每当通过 HTTPS 建立安全连接时。当键存在时,我们可以假设存在一个安全连接。

检查证书是否为实际证书

接下来是检查证书是否有效,但在我们可以这样做之前,我们也应该确保有方法设置证书:

// This property will store our certificate array
private $certificate;

/**
 * Sets (and parses) a certificate, returns false if the 
 * certificate couldn't be parsed.
 * 
 * @param string $certificateContent
 * @return boolean
 */
public function setCertificate($certificateContent) 
{
  // This function is part of the OpenSSL extension in 
  // PHP. This means that if OpenSSL is not installed 
  // into PHP this function will not exist and thus give 
  // a fatal error. This function deciphers the 
  // information received in the certificate to a great 
  // array with variables.
  $certificate = openssl_x509_parse(
               $certificateContent
  );

  // If the certificate can't be parsed (i.e. it is 
  // invalid) the function above will return false
  if ($certificate !== false) {
    // We can be sure the certificate is valid at least 
    // in raw state now
    $this->certificate = $certificate;

    // Done here
    return true;
  } else {
    // Use the failure to parse certificate here to make 
    // sure the developer/user will know what is going 
    // on
    $this->setError(self::AUTH_FAIL_PARSE_CERT);
    return false;
  }
}

此方法进行了基本检查,以查看我们是否真的得到了至少有效的证书,即使它已过期或没有我们的任何字段。

检查我们是否拥有所有证书字段

因为我们想检查证书中的电子邮件地址,我们需要确保我们确实有一个电子邮件地址在那里。同时,我们还将检查几个与我们认证无关但仍然很不错的字段:

/**
 * Checks if all our fields (issuer, issuer[O], 
 * issuer[CN], issuer[emailAddress], serialNumber) are 
 * in the certificate.
 * 
 * @return boolean
 */
private function checkRequiredFields()
{
  // First get our certificate
  $certificate = $this->getCertificate();

  // Check if our certificate at least is valid
  if ($certificate !== false) {
    // We want to check if the following fields (and 
    // subfields) are in the certificate
    $required = array(
      'issuer' => array('O', 'CN', 'emailAddress'), 
      'serialNumber' => null
    );

    // Loop through the primary fields
    foreach ($required as $field=>$value) {
      if (in_array($field, $certificate) === true) {
        // The primary field is in there, check if 
        // there are any secondary fields we need to 
        // check
        if (is_array($value && is_array($certificate[$field) {
          // Loop through the secondary fields
          foreach ($value as $key) {
            // Now check of our values are in there
            if (in_array(
              $key, 
              array_keys(
                $certificate[$field])) === false) 
              {
                return false;
              }
            }
          }
        } else {
          return false;
        }
      }

      // If we reach this point, we are always ok to go
      $retval = true;

      unset($required);
    }

  unset($certificate);

  return isset($retval) ? $retval : false;
}

我们检查字段是否在其中,如果字段不在其中,我们将返回 false。

检查证书尚未过期

现在我们想知道证书在时间上是否仍然有效,因为证书通常在设定的时间后过期(这可能是一月、一周、一年,实际上可以是任何时间):

/**
 * Checks if the current certificate is valid or not.
 * 
 * @return boolean
 */
private function isCertificateValid()
{
  // Get our certificate again
  $certificate = $this->getCertificate();

  // Again make sure it is not false (highly unlikely 
  // here, but hey, never be sure
  if ($certificate !== false) {
    // Check if the valid from and to fields are set, 
    // because if they are not, we won't be able to 
    // check if the certificate is valid or not
    if (isset($certificate['validFrom_time_t']) === true && isset($certificate['validTo_time_t']) === true)  
    { 
      // Check if the from time is smaller than our 
      // current time and the to time is bigger than the 
      // current time
    if (time() >= $certificate['validFrom_time_t'] && time() < $certificate['validTo_time_t']) 
      {
        $retval = true;
      }
    }
  }

  unset($certificate);

  return isset($retval) ? $retval : false;
}

如果此方法返回 true,我们可以确信我们有一个未过期的证书。

为数据库适配器创建 getter 和 setter

现在我们需要一个简单的 getter 和 setter 来处理我们的数据库适配器,在我们实际上进行认证之前:

/**
 * Our Database adapter property.
 * 
 * @var \Zend\Db\Adapter\Adapter
 */
private $dbAdapter;

/**
 * Sets the Db adapter.
 *	
 * @param \Zend\Db\Adapter\Adapter $db
 */
public function setDbAdapter(\Zend\Db\Adapter\Adapter $db) 
{
  $this->dbAdapter = $db;
}

/**
 * Returns the Db adapter.
 * 
 * @return \Zend\Db\Adapter\Adapter
 */
private function getDbAdapter()
{
  return $this->dbAdapter;
}

当然,这又很简单,因为它根本不需要任何逻辑。现在我们已经设置了数据库适配器,我们实际上可以开始认证用户了。

创建认证方法

此方法将实现我们之前定义的所有方法,如果它们都成功了,它将通过数据库进行身份验证,看看我们的用户是否在那里(或者不在)。但是首先,我们需要另一种方法来从我们的证书中获取字段,这是一种更整洁的方法,以及一种在认证后获取我们身份的方法:

// We will store our identity in here, once 
// authenticated
private $identity;

/**
 * Retrieves a variable from the certificate, returns 
 * null if not found.
 * 
 * @param string $variable
 * @return string
 */
private function getCertificateVariable($variable)
{
  if (is_array($this->certificate) === true && isset($this->certificate[$variable]) === true) 
  {
    return $this->certificate[$variable];
  } else if (is_array($this->certificate) === true && isset($this->certificate['issuer'][$variable) 
  {
    return $this->certificate['issuer'][$variable];
  } else {
    return null;
  }
}

/**
 * Retrieves the identity of the user.
 * 
 * @return array
 */
public function getIdentity() 
{
  return $this->identity;
}

现在是最高潮的时刻,经过漫长的等待,终于到了authenticate方法!

/**
 * Tries to authenticate the user through the 
 * certificate.
 * 
 * @return boolean
 */
public function authenticate() 
{
  $continue = true;

  if ($this->getDbAdapter() !== null) {
    // Check if we are on a secure connection
    if ($this->isHTTPS() === true) {
      // Check if the certificate is valid
      if ($this->getCertificate() !== false) {
        // Check if the fields we require are available
        if ($this->checkRequiredFields() === true) {
          // Check if the certificate isn't expired
          if ($this->isCertificateValid() === false) {
            // Certificate is expired!
            $this->setError(self::AUTH_FAIL_EXP_CERT);
            $continue = false;
          }
        } else {
          // Not all the fields are available
          $this->setError(
              self::AUTH_FAIL_NOT_ALL_FIELDS
          );
          $continue = false;
        }
      } else {
        // This is an invalid certificate
        $this->setError(self::AUTH_FAIL_INV_CERT);
        $continue = false;
      }
    } else {
      // Oh, oh, no secure connection
      $this->setError(self::AUTH_FAIL_NO_HTTPS);
      $continue = false;
    }
  } else {
    // We don't have a db adapter
    $this->setError(self::AUTH_FAIL_NO_DB_ADAPTER);
    $continue = false;
  }

  if ($continue === true) {
    // Now we are going to check with the database if 
    // the email address is in there
    $statement = $this->getDbAdapter()->createStatement(
      "SELECT * FROM users WHERE email = :email"
    );

    try { 
      // Input the email address in the statement and 
      // execute it on the database adapter
      $result = $statement->execute(array(
        'email' => $this->getCertificateVariable(
            'emailAddress'
        )
      ));

      // Check if we have one result
      if ($result->count() === 1) {
        // One result found, put it in the identity kit
        $this->identity = $result->current();

        // Because we are super-cool add some of our 
        // certificate variables as well
        $this->identity['serialNumber'] = 
          $this->getCertificateVariable('serialNumber');

        $this->identity['organization'] = 
          $this->getCertificateVariable('O');

        $this->identity['commonName'] = 
          $this->getCertificateVariable('CN');

        // We successfully found our user
        $retval = true;
      } else {
        $this->setError(self::AUTH_FAIL_NO_USER);
      }
    } catch (\Exception $e) {
      $this->setError(self::AUTH_FAIL_SQL_ERR);
      error_log($e->getMessage());
    }
  }

  // Return the retval is we have one, otherwise just 
  // false
  return isset($retval) ? $retval : false;
}

就这样!authenticate方法将在认证成功时返回 true,或者在失败时返回 false,同时设置一个错误,这样我们就可以看到到底出了什么问题!

它是如何工作的…

现在我们已经创建了我们的authentication适配器,是时候坐下来回顾我们刚才所做的一切了。

我们试图实现什么

在某些网站上,访问被禁止到了这种程度,以至于用户名和密码已成为过去式。在我们希望检查每个进入的顾客而不需要他们自己输入用户名和密码的环境中,我们可能会使用证书认证。

证书认证之所以有效,是因为客户端将在每次向服务器发送请求时发送一个证书。这个证书随后会向服务器显示用户是谁,谁正在尝试浏览他们的页面。通常,证书中的一个或多个字段被用来识别用户。在我们的例子中,我们将使用电子邮件地址,这是一个常见的用于身份验证的字段。

我们首先要做的是创建一个适配器,它将从手动输入(这样测试起来更简单)或浏览器中获取证书,哪个可行就用哪个。然后我们将检查电子邮件地址是否存在于我们的数据库中,如果是的话,我们就认为用户已经登录。显然,我们的服务器不会配置得那么严格,以至于不允许任何证书,因为在这一阶段,基本上所有带有正确电子邮件地址的证书都能获得访问权限。如果我们想知道如何防止用户使用任何证书,我们可以查看“更多内容…”部分,在那里我们将进一步探讨如何保护您的服务器,并限制证书的使用。

然而,从应用程序的角度来看,我们只是假设我们得到的所有证书都是有效的。

AdapterInterface只要求我们有一个认证方法。但在我们继续之前,我们想要确保以下项目已被检查:

  • 我们想确保用户是通过一个安全的连接(HTTPS)来的

  • 我们还想要确保证书是有效的(显然)

  • 在检查的过程中,我们将确保我们的证书具有我们用于认证所需的字段

  • 我们还需要知道证书是否仍然有效且未过期

  • 最后但同样重要的是,我们还需要确保我们有一个数据库适配器来检查值

关于证书

通常,在证书到达应用程序之前,服务器会对证书进行验证。验证通常是对某种 CA,即证书颁发机构进行的,这基本上是服务器端的一个实体,它颁发证书,因此可以为带有其签名的任何证书作证。当然,在现实生活中,这比描述的要复杂得多,但基本思想是相同的。所以当服务器上进行某种程度的检查以验证证书的身份,并且如果它与服务器提供的 CA 有效时,它将解析它并通过到我们的应用程序。

当它到达我们的应用程序时,我们通常假设用户已经从我们这里获得了证书,因此应该被允许进入,因为他知道门上的密码。但尽管他知道密码,这并不意味着我们知道他是谁!这就是为什么第二次认证(我们刚刚进行的认证)验证用户实际上是否属于我们的应用程序,也就是说,证书是否有效!

还有更多...

保护服务器是这种验证最重要的部分,因为我们真的(真的,真的)需要确保携带证书的用户是有效的。通常,构建这样的复杂服务器是由服务器工程师完成的,而不是开发者的任务,但如果这样的话,先了解一下这个主题会是个好主意。

个人来说,我是 Apache 的粉丝,并建议任何人都去了解mod_ssl配置,因为它在保护服务器方面非常全面,并且有很多资源可以帮助你正确配置它。

但最终,如果没有适当的了解,配置 SSL 是一个非常繁琐且容易出错的流程,而且很可能正确配置服务器超出了开发者的能力范围。在这种情况下,让服务器工程师为我们完成这项工作是最好的,也是最偷懒的方法,这样我们就可以专注于我们的工作了!

第八章. 优化性能

在本章中,我们将涵盖:

  • 缓存,以及何时缓存

  • 理解和使用存储插件

  • 设置缓存系统

简介

在一个我们希望立即获取数据的社会中,确保我们的网站和应用能够尽快提供数据是非常重要的。当我们排除了任何明显的减速原因,例如网络基础设施或服务器配置后,我们就可以开始考虑缓存。这一章全部关于应该缓存什么以及如何缓存,从而使我们的生活变得更加快捷。

缓存和何时缓存

缓存,每个人都知道它,每个人都在谈论它,但什么是缓存呢?在它的最纯粹的本质上,缓存就是尽可能快地向用户提供应用。这就是我们在本食谱中将要讨论的,何时以及如何进行缓存。

准备工作

我们将再次使用 Zend Framework 骨架应用,因此明智的做法是先设置好它。

如何操作…

在开发应用时,缓存可能不是设计阶段立即考虑的事情,而且很可能是当应用上线后一段时间,你发现应用的响应速度比你最初上线时要慢。

这正是考虑实现缓存的理想时间(虽然不是完美的,因为显然应该在设计阶段进行),以加快应用的响应速度。

当我们谈论缓存时,一个常见的误解是我们仅仅在谈论缓存 HTML 输出。这完全不是事实,因为我们有几种强大的 PHP 缓存方法。

以下列表是我们可用于缓存应用不同部分的一些方法的集合:

  • 缓存 ZF2 配置

  • 缓存渲染后的输出

  • 缓存类映射

我们现在将更深入地探讨前面列表中提到的各种方法。

缓存配置

在你的应用中,最静态的代码部分可能就是配置。哦,但我们需要配置来正确加载我们的应用,但与此同时,我们可能因为所有需要合并的操作而讨厌它,直到我们得到配置的最终版本。

但不必担心,因为我们只需缓存合并后的配置,这样你的应用就不必再解析所有内容了!实际上,这个过程如此简单,以至于给出示例几乎令人捧腹(/config/application.config.php):

<?php
return array(
  // Look for this ke y in the configuration array.
  'module_listener_options' => array(

    // Enable the config cache.
    'config_cache_enabled' => true,

    // If we want to give the cache a special filename
    // we can just type a name here.
    'config_cache_key' => 'configuration'

    // The directory where we want to write the cache 
    // to. Don't forget that we need read/write access 
    // to this directory by the process running the app, which in 
    // most cases is the web server process!
    'cache_dir' => 'data/cache/',
  ),
);

就这样。不需要任何花哨的东西来使这个工作正常,因为使这个工作正常所需的一切都已经内置在 Zend Framework 2 中。

这是一个非常有效的方法来开始缓存所有静态内容,尽管它可能不会给应用带来巨大的速度提升(除非我们真的有成百上千个模块),但它将是一个不应该被遗忘的方法。

缓存输出

缓存输出在当我们有很多静态文件通常不会或很少改变时很有用。当我们谈论变化不大的内容时,我们可以想到博客文章或新闻条目,因为它们通常只生成一次并无限期地发布。显然还有更多有用的输出类型可以缓存,但我们将只给出一个例子来展示我们认为静态的输出是如何容易缓存的。

首先,我们需要在我们的模块中创建配置,以确保在ServiceManager中启用缓存(/module/SomeModule/config/module.config.php):

<?php
return array(
  // We need to define the ServiceManager
  'service_manager' => array(
    // We will call it cache-service
    'cache-service' => function () {
      // Return a new cache adapter
      return \Zend\Cache\StorageFactory::factory(array(
        'adapter' => array(
          // We want to use the cache that is being 
          // stored on the filesystem
          'name' => 'filesystem',
          'options' => array(
            'cache_dir' => 'data/cache/',

            // This is the amount in minutes the cache is valid
            'ttl' => 100
          ),
        ),
      ));
    },
  },
);

现在我们创建了配置,让我们继续在我们的/module/SomeModule/Module.php文件的onBootstrap方法中控制缓存:

<?php
// Don't forget the namespace (obviously)
namespace SomeModule;

// We need this event for the onBootstrap event
use Zend\Mvc\MvcEvent;

// Begin our module class
class Module
{
  // This is going to be run at bootstrap, and will thus 
  // create our events that will create our cached 
  // output
  public function onBootstrap(MvcEvent $e)
  {
    // We will need a list of routes that we deem 
    // cacheable
    $routes = array('blog/pages', 'blog/archives');

    $eventManager = $e->getApplication()->getEventManager();
    $serviceManager = $e->getApplication()->getServiceManager();

    $eventManager->attach(
         MvcEvent::EVENT_ROUTE, 
         function($e) use ($serviceManager)
    {
      $route = $e->getRouteMatch()
                 ->getMatchedRouteName();

      // Check if this is a page that we want to cache, 
      // if not then just exit this method
      if (!in_array($route, $routes)) {
        return;
      }

      // Get the cache-service from the configuration
      $cache = $serviceManager->get('cache-service');

      // Define a unique key that we use for the route
      $key = 'route-'. $route;

      // Check if our cache has the key with our route 
      // content 
      if ($cache->hasItem($key)) {
        // Handle response
        $response = $e->getResponse();

        // Set the content to our cached content
        $response->setContent($cache->getItem($key));

        // Return the response, because when we return 
        // the response from a route event, the 
        // application will output that response.
        return $response;
      }
    }, 
    // Make this priority super low to make sure this 
    // route has already happened
    -1000); 

    // Now we create an trigger for the render event 
    // which will come after the route event. This means 
    // that we didn't have a valid cache, and we will 
    // now use this opportunity to create a cache of our 
    // rendered content.
    $eventManager->attach(
      MvcEvent::EVENT_RENDER, 
      function($e) use ($serviceManager, $routes) 
    {
      // Get the current route name 
      $route = $e->getRouteMatch()
                 ->getMatchedRouteName();

      // Check if this is a page that we want to cache, 
      // if not then just exit this method
      if (!in_array($route, $routes))
        return;

      // Apparently we want to cache the content, so 
      // here we go!
      $response = $e->getResponse(); 

      // Get the cache service from the ServiceManager
      $cache = $serviceManager->get('cache-service');

      // Build up our unique cache key
      $key = 'route-'. $route;

      // And now set the cache item
      $cache->setItem($key, $response->getContent());
    }, 
    // Again the lowest priority to make sure rendering 
    // already has happened.
    -1000);
  }
}

现在,每次我们访问我们的应用时,路由事件都会检查我们是否可能有一个特定路由的缓存,如果有,它将返回缓存(当然,如果没有过期的话)。如果路由还没有被缓存,一旦渲染事件被触发,它将根据需要执行缓存。

这个例子的功劳归功于Jurian Sluiman (jurian-sluiman),他是stackoverflow.com网站的用户,也是 Zend Framework 2 的重要贡献者。

缓存类映射

类映射文件是那些一旦应用完成合并后就会变得很大的文件之一,基本上是静态的。它之所以是静态的,这显然为我们提供了一个很好的机会,我们可以缓存它,并从应用的合并中减轻一些负担。至于我们缓存的第一个方法,这也只需要我们在配置文件中添加几个属性。

让我们从这个例子开始(/config/application.config.php):

<?php
return array(
  // Look for this key in the configuration array.
  'module_listener_options' => array(

    // Enable the module map cache.
    'module_map_cache_enabled' => true,

    // If we want to give the cache a special filename
    // we can just type a name here.
    'module_map_cache_key' => 'classmap

    // The directory where we want to write the cache 
    // to. Don't forget that we need read/write access  
    // to this directory!
    'cache_dir' => 'data/cache/',
  ),
);

再次强调,尽管这可能在整体性能上可能没有显著提升,但我们确信每一丝帮助都是有益的,它肯定会帮助减轻自动加载过程的工作负担。

它是如何工作的…

所有这些缓存所做的只是通过在特定时间(ttl,也称为生存时间)内保持一切准备就绪来加快应用的速度。它通过在应用需要时提供所需数据,而不需要应用连接到数据库或重新编译模板等,从而加快了应用的速度。

缓存通常是在文件系统中进行的,因为它被认为是一个非常快的选项,而不是通过数据库等。然而,从技术上讲,缓存的最快选项是在内存中(这是因为内存或 RAM 是 CPU 最近的数据存储,因此是最快的)。尽管内存缓存是一种很好的缓存方法,但如果缓存的数据太多,它也可能变成最糟糕的一种。

因此,在仅仅使用一种方法之前,考虑不同的缓存方法(例如,仅对博客文章和应用配置使用文件系统缓存,例如,内存缓存)是明智的。

理解和使用存储插件

与自定义一切不同,Zend Framework 2 提供了一个出色的接口,可以通过存储插件来操作存储、删除和检索缓存数据。

如何做到这一点……

存储插件用于在开发者觉得需要向适配器添加更多功能时补充存储适配器,而不必 necessarily 制作一个自定义适配器。因此,当我们要修改我们的存储适配器处理缓存的方式时,插件是最方便的工具。

在 Zend Framework 2 中,有几个存储插件可供使用,因此让我们开始进一步解释它们。

使用 ClearExpiredByFactor 插件

ClearExpiredByFactor 插件会偶尔清除过期的缓存项,这些项由一个设置的因子决定。因子整数越高,缓存清除过期项的可能性就越小。但别忘了;这是一个(伪)随机过程,所有机会都可能是它每次都会被调用。我们理解这非常不符合直觉,所以也许这个从插件中提取的代码片段可以澄清一些问题。

if ($factor && mt_rand(1, $factor) == 1) {
     $storage->clearExpired();
}

我们还应该注意,此插件仅在需要写入缓存时才会触发,当读取缓存时不会触发。

可以设置的 PluginOptionssetClearingFactor,它设置清除因子。

小贴士

此插件要求存储适配器必须是 ClearExpiredInterface 的实例,否则它将不会做任何事情(而且我们永远不会知道,因为它不会记录这个错误)。只有文件系统和内存存储适配器支持此接口。

使用 ExceptionHandler 插件

ExceptionHandler 插件会捕获在获取/设置缓存时抛出的任何异常,并将其转发到开发者定义的回调函数。

可以设置的 PluginOptions 包括:

  • setExceptionCallback:这是一个在发生异常时调用的回调函数

  • setThrowExceptions:这是一个布尔值(默认 true),告诉插件重新抛出它捕获的异常

使用 IgnoreUserAbort 插件

IgnoreUserAbort 插件确保脚本在写入缓存完成之前不会被中止。这样我们就可以确保我们的缓存中不会有任何损坏的数据。

可以设置的 PluginOptionssetExitOnAbort,它是一个布尔值(默认 true),告诉我们我们是否可以随时中止脚本,或者如果我们需要等待我们完成写入。

使用 OptimizeByFactor 插件

你想要按因子清除吗?我敢肯定你也不想按因子优化!此插件(伪)随机优化缓存。因子决定了它实际优化的机会,数字越低(在 1 和较大数字之间),机会越大,数字越高,机会越小。我们理解这非常不符合直觉,所以也许这个从插件中提取的代码片段可以澄清一些问题:

if ($factor && mt_rand(1, $factor) == 1) {
     $storage->clearExpired();
}

我们还应该注意,此插件仅在需要移除缓存时才会触发,当缓存被读取或写入时不会触发。

可以设置的PluginOptionssetOptimizingFactor,它设置优化因子。

小贴士

此插件仅在具有OptimizableInterface实例的存储适配器上工作。如果此接口不可用,它不会抛出错误,所以我们永远不会知道。目前支持此接口的适配器是 Dba 和 Filesystem。

使用序列化器插件

Serializer插件将在设置和从缓存获取数据时序列化和反序列化数据。

可以设置的PluginOptions

  • setSerializer: 这将设置我们想要使用的序列化器,它需要是一个实现了Zend\Serializer\Adapter\AdapterInterface类的类

  • setSerializerOptions: 如果在setSerializer选项(作为字符串的全类名)中给出了字符串,则需要在选项中设置实例化选项

使用任何插件

幸运的是,插件很容易使用,我们只需要将它们添加到存储适配器即可使其工作。

我们知道有几种方法可以实例化插件,但我们将只显示一种方法来展示它基本上是如何工作的:

<?php

// Use the following libraries for our example
use Zend\Cache\Storage\Plugin\Serializer;
use Zend\Cache\Storage\Adapter\FileSystem;

// Initialize our Serializer plugin
$plugin = new Serializer();

// Initialize our FileSystem adapter
$adapter = new FileSystem();

// Now bind the two together
$adapter->addPlugin($plugin);

这就是所有需要配置以使其协同工作的内容。在一个 MVC 应用程序(我们可能将使用 Zend Framework 2)中,插件可以位于非常不同的位置。通常,我们希望在配置或引导事件中配置它,如果我们打算在整个应用程序中持续使用它,因为这样可以节省时间,与多次实例化相比。

它是如何工作的…

插件附加到存储适配器上,并且因为它们将自己附加到存储适配器的事件上,所以它们才会工作。当这些事件被触发时,功能也会被触发。这真的很简单,而且对此没有真正的进一步解释所需的。

设置缓存系统

学习新技术的最佳方式总是最好的例子。这就是为什么我们将向您展示如何在应用程序的不同部分实现缓存系统。

准备工作

在这个菜谱中,我们将展示一个简单的系统,该系统利用了缓存。我们还将展示一些基准测试,以便我们可以清楚地看到没有缓存和有缓存的系统之间的差异。此项目的代码也可以在书中找到,其中包含一些示例类,以便我们可以更好地测量性能。我们不会讨论任何示例类(所有这些类都可以在/module/Application/src/Application目录中找到),但我们将参考它们在示例中。

如何做…

设置一个简单的缓存系统很容易,但大多数时候的问题是,从哪里开始。

在缓存之前基准测试我们的应用程序

对于基准测试,我们将使用一个名为ab的应用程序,它是 ApacheBench 的缩写。这是一个标准工具,包含在 Apache 网络服务器中,无论是 Microsoft Windows 版本还是 Linux 版本;在我们的食谱中,我们将使用基准测试工具的 Linux 版本,不用担心,因为两个版本都完全一样。

对于我们的基准测试,我们将不使用任何缓存,并在Application\Controller\IndexController/module/Application/src/Application/Controller/IndexController.php)中使用以下代码来生成我们的荒谬长输出:

<?php

// Don't forget to set our namespace
namespace Application\Controller;

// Use the following classes 
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

// Define our class name and extend
class IndexController extends AbstractActionController
{
  // We will just use the index for this
  public function indexAction()
  {
    // Initialize our LongOutput class
    $output = new \Application\Model\LongOutput();

    // echo our stupidly long output 
    echo '<!-- '. $output->run(1500). ' -->';

    // Just return a view model, it doesn't affect us
    return new ViewModel();
  }
}

这个操作将输出一个非常长的字符串,非常复杂,但我们并不真的关心这一点,因为我们只想测量创建这样一个字符串需要多长时间。现在我们可以开始第一个基准测试。

以下命令将用于进行基准测试:

$ ab -c 4 -n 10 http://localhost/

命令表示并发数为四(-c 4),我们想在localhost上运行测试十次(-n 10),作为我们的网站。这意味着我们的页面总共将被访问 40 次,这将给我们一个相当清晰的平均响应时间视图。

以下是对基准测试最重要的结果的回顾。显然,其余的结果也有一定的兴趣,但我们目前只对响应时间感兴趣。

Time taken for tests:   18.111 seconds

我们将使用 18.111 秒作为比较所有其他结果的基础。

实现配置/类映射缓存

首先,我们将实现配置缓存,因为这是所有缓存的基础(至少我喜欢这样想)。

我们可以通过向/config/application.config.php文件添加以下配置来实现这一点:

<?php
// Lets add our options to the configuration array, 
// please be aware that we don't show any other options 
// here that could very well be in the configuration 
// already.
return array(
  // We should add our options inside this array key
  'module_listener_options' => array(
    // Enable the config cache
    'config_cache_enabled' => true,

    // Give the config cache a file name like module-
    // config-cache.config.php
    'config_cache_key' => 'config',

    // Enable the class map caching
    'module_map_cache_enabled' => true,

    // Give the class map cache a file name like module-
    // classmap-cache.classmap.php
    'module_map_cache_key' => 'classmap',

    // Use our data/cache as the cache directory 
    // (remember this directory need to be writeable for 
    // the web server).
    'cache_dir' => 'data/cache',
    // We don't want to check the module dependencies as 
    // that is the job of the developer, it just takes 
    // time to do this and is pretty much useless.
    'check_dependencies' => false,
  ),
);

现在,我们已经启用了配置/类映射缓存,这应该会给我们带来一个非常(非常)小的响应时间增加。当然,当我们的应用程序更大,有更多模块时,这个差异会更大。

让我们再次进行基准测试,看看有什么区别:

Time taken for tests:   15.428 seconds

如我们所见,我们的结果已经显著不同,实际上快了 14.2%,令人震惊。然而,我们不应忘记,我们的应用程序非常小,如果未来应用程序规模扩大,这个百分比可能实际上会更小。尽管如此,这仍然是一个明显的迹象,表明缓存配置和类映射是一个好的实践。

小贴士

我们应该注意配置缓存系统中的一个小错误是,我们不能使用闭包(也称为匿名函数)。如果我们这样做,我们会得到一个 PHP 致命错误,如下所示:

Call to undefined method Closure::__set_state() in your_configuration_cache.php on line XX

实现类缓存

由于我们有这个非常长的输出,使用ClassCache适配器来缓存生成此输出的单个方法输出是很有趣的。我们还知道我们的LongOutput模型没有改变输出的内容,我们可以安全地缓存输出。

为了使这种缓存方法工作,我们需要确保配置缓存已被关闭,否则它将导致 PHP 错误。

我们首先将更改Application模块中的module.config.php,以初始化我们的缓存存储适配器。之后,我们将更改Application\Controller\IndexController,以便我们可以使用我们的模式。我们只需将以下代码添加到/module/Application/config/module.config.php

<?php
return array(
  // We are configuring the service manager
  'service_manager' => array(
    'factories' => array(
      // Initialize our file system storage
      'Zend\Cache\StorageFactory' => function() {
        return Zend\Cache\StorageFactory::factory(
          array(
            'adapter' => array(
              'name' => 'filesystem',
              'options' => array(
                // Define the directory to store the 
                // cache in 
                'cacheDir' => 'data/cache',
              ),
            ),
            // For the file system storage we need to 
            // have the serializer plugin enabled, 
            // otherwise thing just go wrong when we  
            // want to storage a class or so
            'plugins' => array('serializer'),
          ),
        );
      }
    ),
    // We want to call our cache with the 'cache' key
    'aliases' => array(
      'cache' => 'Zend\Cache\StorageFactory',
    ),
  ),
);

现在我们已经初始化了缓存,我们需要确保我们的输出也被缓存。这将在我们的Application模块的IndexController/module/Application/src/Application/Controller/IndexController.php)中完成:

<?php

// Set the namespace
namespace Application\Controller;

// Define the imports
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

// Define the class name and extend
class IndexController extends AbstractActionController
{

  // Begin our index action again
  public function indexAction()
  {
    // This time we want to make sure our class is 
    // loaded in to the ClassCache pattern so that we 
    // can eventually cache the output of our class 
    // method    
    $pattern = \Zend\Cache\PatternFactory::factory(
           'class', array(
      'storage' => $this->getServiceLocator()->get('cache'),
      'class' => '\Application\Model\LongOutput'
    ));

    // Now call our method through the ClassCache 
    // pattern with the same arguments as the previous 
    // test
    echo '<!-- '. $pattern->call('run', array(1500)). '-->';

    // Return the view model again because we don't 
    // actually do anything with it
    return new ViewModel();
  }
}

如果我们现在查看基准测试,我们可以看到以下缓存导致了以下性能提升:

Time taken for tests:   14.956 seconds

如我们所见,这几乎是在原始基准测试上提高了 17.4%,这显然是一个巨大的改进。它也比配置/类映射缓存提高了 3.2%的响应速度。我们知道这听起来并不太令人印象深刻,我们也理解你的失望。然而,请理解,在现实生活中,数据库调用或服务调用可能比这长得多,因此改进的百分比会更大!

这个方法只有一个小问题;那就是我们不会以这种方式缓存配置/类映射。因为我们想尽可能优化我们的应用程序,这显然不是好的做法。但是,不要慌张,这个问题有一个解决方案,它以StorageCacheFactory的形式出现!

我们没有立即讨论这个问题,因为最好看到不止一种编码方式,至少这是我的个人选择。

我们将要移除在/module/Application/config/module.config.php中添加的配置,并添加以下配置:

<?php
// We need to assume that we have stripped the previous 
// configuration out of here and it is back to the 
// default configuration file
return array(
  'service_manager' => array(
    // Instantiate the cache through our storage cache 
    // factory. It will look for the 'cache' key to 
    // initialize the cache
    'factories' => array(
        'cache' => '\Zend\Cache\Service\StorageCacheFactory',
    ),
  ),

  // And here we go, initializing the cache
  'cache' => array(
    // We want to use the filesystem adapter
    'adapter' => 'Filesystem',
    'options' => array(
      // Of course we need to set the directory to cache 
      // in
      'cache_dir' => 'data/cache'
    ),

    // We also want the serializer otherwise it will 
    // throw an exception
    'plugins' => array('Serializer'),
  ),
);

如果我们现在回过头来对配置和类映射缓存进行基准测试,我们会得到以下结果。

Time taken for tests:   14.303 seconds

如我们所见,这次在两个缓存系统都启用的情况下,与原始版本相比,我们得到了 21%的速度提升。

它是如何工作的...

总是缓存我们经常使用且确信其持久性的东西是很好的。如果我们知道一个类的输出不会改变,但例如仅仅做一些我们知道它会进行的计算,那么它将是一个很好的缓存候选者。不要忘记,依赖于第三方输入的缓存方法,如数据库,更难缓存,因为它们需要一定的存活时间,在这个时间内缓存知道它们缓存的这些数据已经过时。

另一件需要注意的事情是缓存过多,这样实际上你的应用程序会变慢,而不是加快,因为缓存太忙于刷新/获取和设置缓存,而不是实际输出它。然而,设置一个周期性的cron(类似于 Windows 用户的计划任务)过程来进行自动清理和自动优化是一个好方法。

第九章:捕获错误

在本章中,我们将涵盖:

  • 处理异常——你的犯罪伙伴

  • 日志记录以及它如何使你的生活更轻松

  • 单元测试——你为什么要做

  • 设置和使用单元测试

简介

1947 年 9 月 9 日,Grace Hopper 发现了第一个计算机错误。这个计算机错误实际上是一只昆虫,而不是软件错误。从那时起,我们基本上在我们的软件应用程序中追逐错误,而且我们学到的代码越多,我们就越开始欣赏良好的错误处理,并及时捕获错误。

对于一个程序员来说,没有什么比接到客户电话说“它不起作用”,而我们不知道到底发生了什么,更令人烦恼的了。这就是为什么本章的重点是尽早捕获错误,并更容易地找到错误的原因。

处理异常——你的犯罪伙伴

为了找到错误源,应该实现良好的错误处理。在本食谱中,我们将讨论在 Zend Framework 2 中的异常处理以及如何最佳地使用它。

准备工作

我们可以安全地假设我们所有人都知道关于 try-catch 和异常的知识,但为了确保没有人被遗漏,请查看本节“参见”子节中的 PHP 手册链接。

如何做到...

异常处理并不难使用,但如果使用得当,它是一个非常实用的工具。

Zend Framework 2 中的异常类

让我们看看以下示例:

<?php 

// This non existing method throws a couple of Exception, which  
// is a PDOException, BadMethodCallException and probably more.
try {
  $object->executeMe();
} catch(PDOException $e) {
  // We catch the most specific Exception first, as this is an 
  // Exception that has to do with a database query that went wrong
} catch(BadMethodCallException $e) {
  // Next up this one, as this tells us that we have done 
  // something wrong when calling this method, maybe we forgot 
  // some arguments, or the method might not exist?
} catch(Exception $e) {
  // We don't know what is going wrong, but we know something did 
  // go wrong. Perhaps we just want to log this, or handle it on 
  // another way?
}

这种try-catch的实现也被称为级联异常。

在分发或渲染时处理异常

要在这些事件之一上实现触发器,我们应该在我们的模块中的一个模块的/module/Application/Module.php文件中添加一些代码(具体是哪一个并不重要)。

<?php
  use Zend\Mvc\Application;
  use Zend\Mvc\MvcEvent;

  // We'll skip the beginning of the file as it has no 
  // effect on us
class Module
{
  // We want to add/create the onBootstrap method to put 
  // our event attachment in
  public function onBootstrap(MvcEvent $e)
  {
    // Get the event manager from the application
    $eventManager = $e->getApplication()
                      ->getEventManager();

    // Make sure our module router listens to our event 
    // manager as well
    $moduleRouteListener = new ModuleRouteListener();
    $moduleRouteListener->attach($eventManager);

    // Get the service manager for later use
    $serviceManager = $e->getApplication()
                        ->getServiceManager();

    // Attach our handler to the events  
    $eventManager->attach(
        // What events do we want to attach to
        array(
          MvcEvent::EVENT_DISPATCH_ERROR,
          MvcEvent::EVENT_RENDER_ERROR,
        ),  

        // What class and method do we want to trigger
        array($this, 'handleException')
    ); 
  }

  // This is the method we use to handle the exception
  public function handleException(MvcEvent $event) 
  {
    // Make sure the error is an exception, otherwise 
    // it might be some other parameter in the event
       if ($event->getError() === Application::ERROR_EXCEPTION) {
      // Now get the exception from the event
      $exception = $event->getParam('exception');

      // Do whatever with this exception 
    }
  }

  // Again, we are not bothered by the rest of the 
  // Module class
}

它是如何工作的...

现在我们已经看到了如何做到这一点,让我们看看它在 Zend Framework 2 中实际上是如何工作的。

Zend Framework 2 中的异常类

Zend Framework 2 几乎为框架的每个组件抛出不同的异常,尽管名称不同,但它们在功能上都是相同的。

首先,这里是一个列表,列出了 PHP 中默认的异常,但被 Zend Framework 2 覆盖,因为 Zend Framework 2 喜欢使用在 Zend 命名空间中的异常,而不是在全局命名空间中:

  • BadMethodCallException

  • DomainException

  • ExtensionNotLoadedException

  • InvalidArgumentException

  • InvalidCallbackException

  • LogicException

  • RuntimeException

幸运的是,我们可以在捕获异常时使用全局的\DomainException以及\Zend\Stdlib\Exception\DomainException(它太长了),因为异常已经从原始的异常中覆盖了。

然而,如果我们使用一系列的捕获来了解特定的异常来源,这可能会很有用;例如,当我们捕获RuntimeException并且我们知道Zend\CacheZend\Authentication可能会抛出一个异常时。然而,通常情况下,它可能很清楚是什么,或者对异常的反应可能因实例而异。

然而,Zend Framework 2 为每个类和方法都提供了 docblocks,并且幸运的是,它还为我们提供了文档化的 @throws。这意味着我们可以轻松地查看文档,并了解特定功能抛出了什么,这样我们就可以轻松地将代码包裹在 try-catch 块中,并处理异常。

我们也可以捕获任何抛出的 \Exception,而不是专门针对命名异常,但我们通常不会这样做,因为这不会给我们对错误的有效控制。一般来说,当我们处理异常时,我们希望尽可能具体,规则是从最具体到最不具体地捕获它们。

在调度或渲染时处理异常

如果我们在调度或渲染时没有处理异常,我们将会遇到麻烦。可能出现的问题之一是白屏问题,因为我们不会在屏幕上看到任何东西,因为发生了错误。在开发阶段,这只会让开发者感到轻微的挫败感,但想想看,当用户在实时环境中看到这种情况时,他们想告诉你他们的侄子/表亲/叔叔的编码能力比我们强。我们不能有这种情况。

正因如此,我们需要确保我们监听 Zend\Mvc\MvcEvent::EVENT_DISPATCH_ERRORZend\Mvc\MvcEvent::EVENT_RENDER_ERROR 事件。当控制器或路由未找到或模板渲染期间发生错误时,这些事件将被触发。

如我们从示例中可以看到,此事件仅在发生错误、调度或渲染时触发。然后可以检索到的异常可以用来记录或显示在屏幕上,无论哪种方式都感觉合适。这里的想法是我们即使没有看到错误发生,也能有效地进行调试。

例如,如果这项技术在实时应用程序中实现,它可以将所有异常记录到日志(或发送到支持部门的电子邮件)中,这样我们就可以看到当我们“不在场”时发生的错误。

关于 try-catch

PHP 中的 try-catch 块是贸易中一个极其有用的工具,我们需要尽可能多地使用它,因为异常链比方法返回 falsenull 更容易解决。特别是结合事件,我们能够及时捕获任何异常,或者至少确保我们能够以合理的方式对其进行调试。

参见

异常手册和 try-catch 介绍:php.net/manual/en/language.exceptions.php

记录和它如何使你的生活更轻松

除了良好的错误处理外,记录是确保你从系统中获得最多知识的好方法。大多数时候,我们甚至可以构建它,以便记录导致错误的事件,然后可以追溯到原始问题。

准备工作

因为我们要在 Zend Framework 2 中尝试所有外来的记录方式,所以我们需要在我们的 Web 服务器上安装 FirePHP 核心。我们可以通过 Composer 工具安装这个库(我们假设我们已经在服务器上使用了它,否则事情会变得有些复杂)。

我们可以通过在composer.json文件的 require 部分添加以下行来安装 FirePHP 库:

"firephp/firephp-core" : "dev-master"

如果我们现在在命令行中执行'php composer.phar update',它将安装库以便稍后在我们的代码中使用。为了充分利用记录器功能,使用一个能够理解 FirePHP 头部的浏览器也是明智的。使用 Mozilla Firefox 浏览器,我们需要安装 Firebug 和 FirePHP 插件来使其工作。如果我们想在 Google 的 Chrome 浏览器或 Microsoft Internet Explorer 中使用 FirePHP 记录,我们也需要安装相应的扩展/插件,因为这些浏览器默认都不支持。

如何做到...

在这个菜谱中,我们将展示如何在我们的应用程序中实现记录器系统的示例。

实现一个真正简单的文件记录器

让我们先实现一个简单的文件记录器,这可以在我们的配置文件之一中完成。我们将把我们的记录器添加到我们的/config/autoload/global.php文件中,因为我们希望它在我们的应用程序的任何地方都可以使用:

return array( 
  // We want to put our logger in the service manager
  'service_manager' => array(
    'factories' => array(
      // We will call our logger 'log' so we can find it 
      // easily back in our application
      'log' => function () {
        // Instantiate our logger
        $log = new Zend\Log\Logger();

        // Add the writer to our logger (don't forget to  
        // make the data directory writable)
        $log->addWriter(new Zend\Log\Writer\Stream(
            getcwd(). '/data/application.log'
        ));

        // Return our logger now
        return $log;
      },
    ),
  ),
);

如我们所见,这很简单,现在我们可以在任何地方使用ServiceManager对象来获取记录器,如下面的Controller(文件:/module/Application/src/Application/Controller/IndexController.php)代码所示:

<?php

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController 
{
  public function indexAction() 
  {
    $this->getServiceLocator()
         ->get('log')
         ->debug("A Debug Log Message");
  }
}

实现 FirePHP 记录器

FirePHP 记录器与之前在/config/autoload/global.php中显示的记录器初始化相同,只有一个区别,那就是Zend\Log\Writer附加到Zend\Log\Logger

// As we can see we can just change (or add if 
// we want more loggers) the log writer to FirePHP.
$log->addWriter(new Zend\Log\Writer\FirePhp());

它是如何工作的...

记录是代码中最被低估的部分之一,我们往往忘记实现它。而且当我们实现它时,我们忘记定期使用它。

我们都知道这是很重要的,但出于某种原因,我们犹豫不决是否要定期实现它。

我们将要做的就是在我们的基本 Zend Framework 2 应用程序中安装一个记录器,以及使用 FirePHP 的更特殊记录方式。

实现一个真正简单的文件记录器

正如我们在先前的indexAction方法中看到的,我们只是简单地在我们的application.log文件中放入了一个调试语句,它看起来可能如下所示:

2013-03-04T13:58:38+02:00 DEBUG (7): A Debug Log Message

我们可以使用的记录方法有log()info()warn()err()debug(),如果我们使用log(),我们需要先给出优先级,然后传递消息作为参数。正如我们也可以看到的,分配给DEBUG的值是7,这指的是优先级的级别。在我们的情况下,DEBUG的优先级是7,但还有更多的优先级:

/**
 * @const int defined from the BSD Syslog message severities
 * @link http://tools.ietf.org/html/rfc3164
 */
const EMERG  = 0;
const ALERT  = 1;
const CRIT   = 2;
const ERR    = 3;
const WARN   = 4;
const NOTICE = 5;
const INFO   = 6;
const DEBUG  = 7;

实现 FirePHP 记录器

如果我们现在开始使用 FirePHP 写入器进行记录,我们将在我们的浏览器控制台中看到以下条目。(在 Mozilla Firefox、Chrome 和 Microsoft Internet Explorer 中按F12键。)

实现 FirePHP 记录器

如我们所见,这为我们通过浏览器发送的日志项提供了一个相当清晰的视图。

小贴士

请注意,在 Zend Framework 2.2.4 中使用debug()日志方法仍然会执行并输出一个trace(),而不是我们使用 FirePHP writer 时想要显示的消息。这目前被报告为一个错误,但尚未得到确认,所以我们不能确定它是否会被解决。

然而,使用这个debug()方法会导致一个非常(非常)大的返回头,并且会通过实际分钟来减慢大型应用程序的响应时间。

在我们继续之前,还有一件事,请勿在生产环境中使用 FirePHP 的log()方法,因为每个人(字面上)都能看到你何时登录和注销,而这并不是你想要的。

关于记录器

Log\Logger包含了一组可以用来以标准化方式记录的方法。Logger有一个或多个Zend\Log\Writer对象附加到它上,Logger将写入这些对象。Writer是唯一一个实际将内容写入请求的日志方法。

使用Writer\FirePhp,这是通过响应向客户端浏览器发送头信息来实现的,而使用Writer\Stream则是一个物理文件(有趣的是,我们在这里使用物理,不是吗?)。

单元测试 – 你为什么要做它

单元测试是编程世界中广泛接受的一种测试形式。不幸的是,许多 PHP 开发者仍然缺乏如何利用它来获得利益的知识,或者他们根本不知道如何开始。这个食谱将尝试改变这一点。

准备工作

要开始使用 PHPUnit 进行单元测试 Zend Framework 2 应用程序,我们需要安装 PHPUnit 3.7.x。我们可以通过几种不同的方式来做这件事,但最简单、最推荐的方式是通过 Composer 安装,它随 Zend Framework 2 应用程序一起提供。

要通过 Composer 安装 PHPUnit,我们只需将以下行添加到composer.json中:

{
  "require-dev": {
    "phpunit/phpunit": "3.7.*"
  }
}

保存composer.json文件后,运行 Composer 来更新新的需求。

$ php composer.phar update

经过一段时间的安装,Composer 安装器将完成,我们就可以开始创建我们的单元测试了。我们可以看到,我们现在在 vendor 目录中有一个额外的目录,名为 phpunit。

如何做...

在我们展示如何真正进行单元测试我们的应用程序之前,最好先展示其背后的概念。

伪代码示例

我们现在将检查几个伪代码示例,这些示例展示了一种根据(某种)TDD 原则(技术上将是 PHP,但我们不会太认真,因为我们只想展示一些示例)进行有效编码的方法。

在这个例子中,我们将有一个名为Person的类,其中只包含isAdult()方法。在我们定义了该方法之后,我们应该编写我们的第一个测试,这个测试应该让我们的初始结果失败。

public function testIsAdult()
{
  // Initialize our Person
  $person = new Person();

  // Our first fail test that makes sure that when no 
  // parameters are given the test will result in false
  assertFalse($person->isAdult());
}

由于我们的方法中还没有代码,结果将始终为 null,因此这个测试将立即失败,正如我们预期的那样,此时应该返回false

当我们现在执行 PHPUnit 时,它将(假设性地)产生以下结果:

PHPUnit 3.7.9 by Sebastian Bergmann.

F

FAILURES!
Tests: 1, Assertions: 1, Failures: 3.

通常看到失败会被认为是错误的,然而在这个例子中,我们会知道我们的方法正在做我们期望它做的事情:失败!下一步是让测试通过,所以让我们在我们的isAdult定义中添加一个简单的返回false

public function isAdult()
{
  // If the return value is set, return that, otherwise
  // return false; which will always happen at this 
  // point
  return isset($retval) ? $retval : false;
}

如果我们现在再次运行测试,我们会看到测试已经通过了:

PHPUnit 3.7.9 by Sebastian Bergmann.

.

OK (1 test, 1 assertion)

现在是时候继续进行测试,确保测试再次失败,这次我们想要确保我们接受一个参数,$age,并且我们希望这个值始终是一个大于或等于 18 的整数,如果不是,我们希望返回false作为结果。

所以让我们继续并编辑test脚本,让它再次失败(从未有失败如此有趣)。

public function testIsAdult()
{
  // Initialize our Person
  $person = new Person();

  // Our first fail test that makes sure that when no 
  // parameters are given the test will result in false
  assertFalse($person->isAdult());

  // Ok, that works now, let's now parse in an integer 
  // parameter so that we get result true back
  assertTrue($person->isAdult(21)); 
}

如果我们现在再次运行测试,我们会看到测试失败了,在这种情况下,这会触发我们重写以下代码,以便测试再次通过:

public function isAdult($age)
{
  // Check if $age is an integer, and if so,  
  // make sure the person is above 18
  if (isset($age) && is_int($age) && $age >= 18) {
      $retval = true;
  }

  // If the return value is set, return that, otherwise   
  // return false; which will always happen at this 
  // point
  return isset($retval) ? $retval : false;

如果我们现在运行测试,测试将再次通过,这意味着我们可以(如果需要的话)再次运行测试失败的循环,更改代码让它再次通过,等等!这个循环将继续,直到我们对方法的输出结果满意,并且它确实做了我们计划让它做的事情。

它是如何工作的...

当我们谈论单元测试时,许多开发者对它有以下几种看法:

他们根本不知道它是什么,或者它的用途是什么;或者他们知道他们应该做,但他们往往不做。

当然,偶尔也会有“我看不出它有任何积极的一面”这样的开发者,但我们现在就忽略这个评论。

单元测试是什么

单元测试是测试应用程序中最小可测试部分的技艺。单元测试被划分为测试用例,这些是隔离的测试,应该只测试你代码的一个特定部分。

这个单元测试可以通过使用模拟对象、伪造和存根方法来使用其他对象,但主要部分是,在任何给定时间,单元测试中只应该测试一段特定的代码。这个想法的背后的理念是我们有一个小的单元测试,它只测试代码的一小部分,所以当问题发生时,我们不需要四处寻找问题所在。

在参考 Zend Framework 2 时,我们通常会单元测试模型、服务和控制器,但不会测试 HTML 输出(除非我们在测试ViewRenderer)。

我们应该在什么时候进行测试?——在编写代码之前还是之后

从纯 TDD测试驱动开发)的角度来看,答案很简单:在开发之前。TDD 整个理念是,在开发开始之前编写测试,因此它总是失败的。测试之所以重要,是因为我们知道我们编写的测试至少是失败的。如果我们编写了一个从未失败的测试,我们怎么知道它会在实际应该失败时失败呢?

显然,也有人在编写测试之后进行争论,其中之一是,我们无法测试尚未设计的代码。虽然这个论点有一定道理,但就我个人而言,我认为这不是一个有效的论点。我们可以在事先编写测试,但这并不意味着我们应该在编写代码之前就编写完整的测试。理念是这样的:编写一个测试,让它失败,编写代码让它通过,然后从开始再次重复这个过程。这也迫使你在开始编写应用程序之前就考虑应用程序的架构。

这是一项纪律问题

单元测试是一项严格的纪律问题,因为它要求我们停止急切地编写代码,并先编写测试。对于许多开发者来说,这意味着我们应该摒弃当前的“肌肉记忆”编码,真正思考我们想要编写的内容,然后再开始编写代码。

当然,当我们开始编写新的代码时,我们有一个关于我们想要的功能实现的想法,例如从数据库中获取记录。然而,考虑我们想要从该功能中获得什么作为返回值是很重要的。是数组吗?还是 boolean?它是否抛出异常?如果我们没有得到有效的参数怎么办?所有这些问题都与架构相关,但通常在事先并没有定义。

单元测试之所以有效,是因为团队中有着严格的纪律。如果我们是我们团队中唯一编写测试代码的人,我们肯定无法维持代码的维护,因为其他团队成员(可能无意中)在修改代码时可能会破坏我们的单元测试。

然而,不能低估单元测试在软件开发中的价值,即使是在代码编写之后(正如你所见,我非常支持先编写测试)。

设置和使用单元测试

在 Zend Framework 2 中开始使用单元测试可能会有些麻烦。但别担心;随着我们引导你完成 Zend Framework 2 单元测试的正确设置,帮助即将到来。

准备工作

要开始使用 Zend Framework 2 应用程序的单元测试,我们需要安装 PHPUnit 3.7.x。我们可以通过几种不同的方式来完成这项工作,但最简单、最推荐的方式是通过 Composer 来安装,它随 Zend Framework 2 应用程序一起提供。

通过 composer 安装 PHPUnit,我们只需将以下几行添加到 composer.json 文件中。

{
  "require-dev": {
    "phpunit/phpunit": "3.7.*"
  }
}

保存 composer.json 文件后,运行 Composer 来更新新的需求。

$ php composer.phar update

经过一段时间,Composer 安装程序将完成,我们将准备好开始创建我们的单元测试。我们可以看到,现在在我们的 vendor 目录中有一个额外的目录叫做 phpunit

如何操作...

在 Zend Framework 2 中使用 PHPUnit 设置单元测试相当简单,而且幸运的是,它也得到了很好的文档记录。

设置测试框架

为了让一切按顺序工作,我们首先需要设置我们独立的测试框架。为此,我们需要三个新文件:Bootstrap.phpTestConfig.phpphpunit.xml

因为我们基本上想要按模块进行测试(记住,要保持它们彼此分离),我们需要为每个测试的模块设置此配置。

首先,我们应该在 module 目录的根目录下创建一个名为 test 的目录。在那个目录中,我们在 /module/Application/test/ 中创建一个名为 phpunit.xml 的文件,该文件由 PHPUnit 用于确定一些配置。

<?xml version="1.0" encoding="UTF-8"?>

<!-- we want to bootstrap with the Bootstrap.php file, and we want tooutput in pretty colors. -->
<phpunit bootstrap="Bootstrap.php" colors="true">
  <testsuites>
    <!-- we can just give this a name for our own 
         identification -->
    <testsuite name="Application Module Tests">
      <!-- this is the directory we want to use for 
           testing -->
      <directory>./Application</directory>
    </testsuite>
  </testsuites>
</phpunit>

这个第一个文件用于 PHPUnit 的一般配置,比我们在这里展示的选项要多得多,但这些选项对我们当前的设置并不相关。

接下来,我们想要在 /module/Application/test/ 中设置 TestConfig.php 文件,这是一个简单的配置文件,它加载启动应用程序和运行我们的代码所需的最基本配置。它基本上与正常的 application.config.php 相同,但我们需要将其放在单独的文件中,因为我们希望能够在不影响主应用程序的情况下对其进行更改。

<?php

// Just as the normal configuration we simply return the 
// array
return array(
  // These are the modules we need to test our module. 
  // Normally this only the current module, but if 
  // this module has dependencies we need to add them 
  // here as well.
  'modules' => array(
    'Application',
  ),

  // Here we define our default module listener options, 
  // nothing special to note here.
  'module_listener_options' => array(
    'module_paths' => array(
      'module',
      'vendor',
    ),
  ),
);

在我们的测试框架中最后要设置的是 /module/Application/test/ 中的 Bootstrap.php 文件,我们在 phpunit.xml 文件中将其作为引导。这个引导类是由整个 ZF2 模块系统的首席作者 Evan Coury 创建的,但我们添加了注释,使整个过程更加清晰。了解这个引导的工作原理对我们来说很重要,以确保我们可以最大限度地利用它。

<?php
// The namespace needs to reflect the namespace of the 
// module we want to test.
namespace Application; 

// The following imports are needed for our class
use Zend\Loader\AutoloaderFactory;
use Zend\Mvc\Service\ServiceManagerConfig;
use Zend\ServiceManager\ServiceManager;
use Zend\Stdlib\ArrayUtils;
use RuntimeException;

// We want to put the error reporting on, so that we see 
// if there is something going wrong
error_reporting(E_ALL | E_STRICT);

// Our current directory is going to be our root 
// directory
chdir(__DIR__);

// Begin our bootstrap class here
class Bootstrap
{
  // Here we will define our ServiceManager in
  protected static $serviceManager;

  // The merged configuration of our application will be 
  // put in this property
  protected static $config;

  // This property isn't used, but we copied it for 
  // originality sake any way
  protected static $bootstrap;

现在,让我们先创建 init() 方法,这个方法将在稍后用于引导应用程序,以便我们可以用它来进行测试。

  public static function init()
  {
    // Read our created TestConfig file, and if it 
    // doesn't exist try the TestConfig.php.dist, but 
    // that won't exist in our environment
    if (is_readable(__DIR__ . '/TestConfig.php')) {
      $testConfig = include __DIR__ . '/TestConfig.php';
    } else {
      $testConfig = include __DIR__ . '/TestConfig.php.dist';
    }

    $zf2ModulePaths = array();

    // Now we will load in all the module paths from the 
    // configuration (if set). 
    if (isset($testConfig['module_listener_options']['module_paths'])) 
    {
      // Get the module path from the configuration
      $modulePaths = $testConfig['module_listener_options']['module_paths'];

      // Now loop through the module paths and find out 
      // what the parent path is of the module
      foreach ($modulePaths as $modulePath) {
        // This method is defined later in the class
        if ($path = static::findParentPath($modulePath)) {
          $zf2ModulePaths[] = $path;
        }
      }
    }

    // Now make a concatenated string with all the 
    // module paths separated by a colon.
    $zf2ModulePaths = implode(
        PATH_SEPARATOR, $zf2ModulePaths
    ) . PATH_SEPARATOR;

    // See if we defined some module paths outside this 
    // class or configuration and add them to the 
    // existing module paths
    $zf2ModulePaths .= getenv('ZF2_MODULES_TEST_PATHS') 
                ?: (defined('ZF2_MODULES_TEST_PATHS') 
                ? ZF2_MODULES_TEST_PATHS : '');

    // Make sure that we initiate auto loading so we 
    // don't have to worry about that (this method is 
    // defined later in the class)
    static::initAutoloader();

    // Now create a new configuration array so that we 
    // can merge it with the loaded configuration.
    $baseConfig = array(
        'module_listener_options' => array(
        'module_paths' => explode(
            PATH_SEPARATOR, $zf2ModulePaths
        ),
      ),
    );

    // Merge our configuration with the base 
    // configuration that we just generated.
    $config = ArrayUtils::merge(
        $baseConfig, $testConfig
    );

到目前为止,我们已经展示了配置文件的定义,现在它已经合并,供我们的引导使用。接下来是服务管理器的定义。

    // Let's create a new service manager
    $serviceManager = new ServiceManager(
        new ServiceManagerConfig()
    );

    // Set the service manager to load the configuration 
    // so that the ModuleManager can use it to load up 
    // the modules and dependencies
    $serviceManager->setService(
        'ApplicationConfig', $config
    );

    // Now get the module manager, and load up the 
    // modules plus dependencies
    $serviceManager->get('ModuleManager')
                   ->loadModules();

    // Make the service manager and configuration 
    // available as a static in the bootstrap class
    static::$serviceManager = $serviceManager;
    static::$config = $config;
  }

我们的初始化到此结束,正如我们所看到的,所做的工作相当直接。引导初始化首先读取配置,然后创建服务管理器。在创建服务管理器之后,我们使用模块管理器加载我们测试所需的模块(及其依赖项)。现在我们已经定义了类最重要的部分,让我们定义在先前的 init() 方法中使用到的其他方法。

  // Not completely unimportant, this is a getter for 
  // our servicemanager property.
  public static function getServiceManager()
  {
    return static::$serviceManager;
  }

  // A simple getter for our static configuration.
  public static function getConfig()
  {
    return static::$config;
  }

  protected static function initAutoloader()
  {
    // Get the parent path of the ZF2 library (this 
    // method is defined later on)
    $vendorPath = static::findParentPath('vendor');

    // Now make sure the ZF2 path is ready to go
    if (is_readable($vendorPath . '/autoload.php')) {
      $loader = include $vendorPath . '/autoload.php';
    } else {
      // The vendor path isn't in the configuration, try 
      // to find it ourselves.
      $zf2Path = getenv('ZF2_PATH') 
               ?: (defined('ZF2_PATH') ? ZF2_PATH 
               : (is_dir($vendorPath . '/ZF2/library') 
               ? $vendorPath . '/ZF2/library' : false));

      // If the path is not defined, we cannot continue
      if (!$zf2Path) {
        throw new RuntimeException(
            'Unable to load ZF2.'
        );
      }

      // Include our autoloader from ZF2
      include $zf2Path. '/Zend/Loader/AutoloaderFactory.php'; 
    }

    // If we come here that means we have a valid ZF2 
    // path, and can safely initialize our Autoloader.      
    AutoloaderFactory::factory(array(
      'Zend\Loader\StandardAutoloader' => array(
        'autoregister_zf' => true,
        'namespaces' => array(
          __NAMESPACE__ => __DIR__ . '/' . __NAMESPACE__,
        ),
      ),
    ));
  }

  // This method finds the parent path of a given path. 
  protected static function findParentPath($path)
  {
    $dir = __DIR__;
    $previousDir = '.';

    while (!is_dir($dir . '/' . $path)) {
      $dir = dirname($dir);

    if ($previousDir === $dir) return false;
      $previousDir = $dir;
    }

    return $dir . '/' . $path;
  }
}

// And finally, initialize the application bootstrap
Bootstrap::init();

现在我们终于设置了测试框架,是时候编写一个简单的测试来看看一切是否正常工作。我们首先要做的是创建一个小的模型(文件 Company.php/module/Application/src/Application/Model/),我们将对其进行测试。

<?php

namespace Application\Model;

class Company
{
  public function hasEmployees() {}
}

到此为止,我们不需要再进行编码,因为我们首先需要创建我们的单元测试(文件 CompanyTest.php/module/Application/test/Application/Model/)。

<?php
// Define the namespace like a boss
namespace ApplicationTest\Model;

// We want to use this model for testing
use Application\Model\Company;

// Begin our test class, which needs to be extended from 
// the PHPUnit framework test case.
class CompanyTest extends \PHPUnit_Framework_TestCase
{
  /**
   * Test some method.
   * @covers Application\Model\Company::hasEmployees
   */
  public function testHasEmployees()
  {
    $this->markTestIncomplete();
  }
}

然后,我们就得到了一个简单的测试,它除了在终端打印一个 I(表示一个不完整的测试)之外,没有做任何事情。如果我们执行它,我们可以看到我们还定义了一个 @covers PHPDoc 标签,这对于良好的文档来说总是一个好主意,实际上记录了你正在测试的方法。

小贴士

要执行 PHPUnit 测试,只需进入 test 目录并输入 phpunit,这将触发 PHPUnit 测试以 Test.php 结尾的每个文件,如 SomeModelTest.php,并查找以 test 开头的方法,如 testSomeMethod

现在我们来做一个小测试,测试我们的方法返回值是否为真(位于 /module/Application/test/Application/Model/ 目录下的 CompanyTest.php 文件)。

public function testHasEmployees()
{
  // Instantiate our model (remember the use statement 
  // in the top of the file).
  $object = new Company();

  // Make sure the method returns true
  $this->assertTrue($object->hasEmployees());
}

如果我们现在再次运行 PHPUnit,我们会在终端看到它打印了一个漂亮的红色大写字母 F(表示测试失败)。现在我们知道单元测试失败了,我们将再次修改我们的模型(位于 /module/Application/src/Application/Model/ 目录下的 Company.php 文件),以确保它再次通过。

public function hasEmployees()
{
  return true;
}

如果我们现在再次运行 PHPUnit,终端中会简单地出现一个 .(表示测试通过)。我们现在知道测试是有效的,我们可以信任单元测试的结果。现在我们可以反复使用这个测试框架来测试我们编写的每个其他方法和模块。

它是如何工作的...

我们首先做的事情是设置一个小型测试框架,它会加载我们为要测试的模块所需的所有内容。然后我们为一些我们想要的代码编写了一些简单的测试。

我们设置的测试框架是一个可以单独用于每个模块的测试框架,因为为整个应用程序创建一个测试框架是不明智的。我们试图实现的是,我们的模块尽可能地独立(当然,考虑到一些模块将具有依赖关系),并且我们可以单独测试它们。

还有更多...

我们设置的框架也在官方文档中可用,这意味着如果我们遇到困难,总有支持可用。

参见

附录 A. 设置基本要素

在附录中,我们将涵盖:

  • 确保你拥有所有你需要的东西

  • 下载 Zend Framework 2 并查找其文档

  • Composer 及其在 Zend Framework 2 中的用途

  • 基本 Zend Framework 2 结构

  • 关于存储适配器和模式

确保你拥有所有你需要的东西

Zend Server 是一款优秀的软件,通过安装我们需要的所有东西(或者至少提供一个良好的平台)来从我们的手中解脱出来,以便编码 Zend Framework 2(以及 Zend Framework 1!)应用程序。尽管 Zend Server 的付费版本可能对于生产应用程序不是必需的,但在 Zend Server 的开发版本中进行开发是一种纯粹的乐趣,因为它将提供系统的适当概述、日志、配置以及我们需要了解的一切。

我们选择通过安装 Zend Server 社区版来走便宜的路,它安装了我们使用 Zend Framework 和 Zend Framework 2 所需的所有东西。关于 Zend Server 的好处不仅在于安装的简便性,还在于服务器本身提供的庞大工具集。这是一款很好的产品,可以了解任何 PHP 相关的配置,并且还能够监控性能和跟踪系统中的事件。

为了安装 Zend Server,我们首先需要从 Zend 网站(www.zend.com)下载它,目前 Zend Server 6.2.0 是该应用程序的最新版本,尽管我们使用它,但安装过程对于任何后续版本都应该是相同的。

小贴士

Zend Server 不是运行 Zend Framework 2 所必需的,但它确实提供了一个优秀的平台,只需要进行最小配置就可以开始使用。

在 Linux 环境中安装 Zend Server 社区版

当我们下载了 Linux 版本的 Zend Server(你需要一个免费的 Zend 账户来下载他们的任何软件)时,我们将有一个名为ZendServer-6.2.0-RepositoryInstaller-linux.tar.gz的文件。

接下来,为了安装 Zend Server,我们需要执行以下命令序列:

$ tar -xf ZendServer-6.2.0-RepositoryInstaller-linux.tar.gz

这将解压 Gzipped Tarball(这是一种压缩方法)包,并将其提取到ZendServer-RepositoryInstaller-linux目录中。现在让我们安装 Zend Server:

$ cd ZendServer-RepositoryInstaller-linux/
$ sudo ./install_zs.sh 5.4

我们选择安装 PHP 5.4,如果没有合理的解释说明为什么我们需要 PHP 5.3,我们建议保持在这个版本。如果我们需要 PHP 5.3,我们可以轻松地将 5.4 改为 5.3,并且它会安装较低的 PHP 版本。一旦我们以 root 用户身份执行install_zs.sh命令(因此,使用sudo,它告诉系统我们想要以超级用户身份执行命令),我们将得到一个简短的确认窗口,询问我们是否真的想要安装 Zend Server。只需按Enter键继续安装。

在安装过程中某个时刻,脚本会询问你是否想要安装 X 数量的新包。你想要回答Yyes,否则安装将在这里结束。

安装本身只需要几分钟,安装成功后,脚本将显示以下信息:

注意

********************************************************
* Zend Server was successfully installed.              *
*                                                      *
* To access the Zend Server UI open your browser at:   *
* https://<hostname>:10082/ZendServer (secure)         *
* or                                                   *
* http://<hostname>:10081/ZendServer                   *
********************************************************

在安全方面,最好始终使用 Zend Server 的安全版本,因为您想确保密码被安全地存储。然而,在本地工作的时候,这并不是特别重要。

在 Windows 环境下安装 Zend Server 社区版

当我们下载了针对 Microsoft Windows 的 Zend Server(您需要免费的 Zend 账户来下载他们的任何软件)并启动了ZendServer-6.2.0-php-5.4.21-Windows_x86.exe文件后,我们发现自己又遇到了一个非常简单的安装。如果我们选择自定义安装,我们可以更改一些选项,但通常默认选项对我们来说已经足够好了。

Windows 环境下安装 Zend Server 的另一个优点是,安装程序会询问我们是否想使用现有的 IIS 网络服务器或安装 Apache 服务器。

您选择哪个选项完全取决于整个项目的配置要求,假设我们有更多的要求;否则,我们真的需要重新考虑是否使用 Windows 作为我们的 PHP 环境。

在即将开始的安装摘要屏幕之后,安装将继续并配置系统。如果安装成功完成,我们将有选项开始使用 Zend Server 并将 Zend Server 添加到桌面图标。

首次运行 Zend Server

如果我们第一次在浏览器中访问 Zend Server 界面(请注意,Windows 版本的 Zend Server 没有内置安全连接,就像 Linux 版本那样),我们将看到许可协议,我们需要接受它才能继续。

在下一屏幕中,根据 Zend Server 的目的,我们需要在开发、单服务器或集群许可之间进行选择。单服务器和集群许可都附带 30 天的试用版,如果我们是 Zend Server 的新用户,那么这是查看服务器全部功能的最优选项。

接下来是设置管理员和开发者密码。如果我们不是唯一一个在服务器环境中工作的人,最好使用单独的账户,因为这会在组织中创建更好的维护结构;如果只有一个人(或账户)能够更改系统设置,那么我们可以直接跳过填写开发者详细信息,因为它们实际上并没有什么用处。

一旦我们完成了所有这些,我们就可以准备在我们的全新系统中首次登录。

默认情况下,登录管理面板的 URL 是非安全面板的http://localhost:10081/ZendServer,而对于安全管理面板则是https://localhost:10082/ZendServer

我们首先看到的是服务器健康状况概述,它还显示了当前发生的事件,如高内存使用、异常和缓慢的执行时间。

我们现在想查看的主要部分是 PHP 配置,它可以在 配置 屏幕下的 PHP 下找到。为 PHP 设置时区非常重要,否则 PHP 会通过警告来打扰我们(原因:因为一些应用程序开发者错误地认为机器运行在他们的本地时区,并将许多日期和时间代码基于此),告诉我们应该设置它。如果我们搜索屏幕右上角的搜索栏中的 date.timezone,它将立即带我们去(并突出显示)我们需要更改的设置。我们可以在互联网上轻松搜索我们特定时区的相关值;例如,可以是 Europe/LondonAmerica/New_York

参见

下载 Zend Framework 2 和查找其文档

让我们来了解一下获取所有关于 Zend Framework 的关键文献的地方。

查找 Zend Framework 2

Zend Framework 2 的官方网站是 framework.zend.com,并且始终包含有关 Zend Framework 2 的最新信息。我们可以轻松地从那里下载框架,以及一些包,例如包含 Zend Server 的框架,或者 Zend Framework 的最小包。

仅下载框架本身,而不包含任何上下文,如骨架应用程序,是从头开始构建应用程序的好方法,无需携带默认骨架带来的任何杂乱。

在 phpcloud 中编码

由 Zend 制造的新玩具目前仍处于测试阶段,是 phpcloud,它允许开发者创建一个快速可靠的开发环境,以便开发者进行开发。使用 phpcloud 的一个特性是它不仅包含 Zend Framework 2,而且运行在 Zend Server 上,这允许出色的调试能力和应用程序部署。目前注册 phpcloud 是免费的,但我们预计这将在未来发生变化。然而,结果如何,我们目前还不知道。

文档和入门指南

Zend Framework 2 的文档幸运地比原始的 Zend Framework 文档(这确实是个好事,请相信我)要可靠得多。Zend 真正致力于创建一个文档详尽且拥有强大社区和如 Github(而不是原始框架中的 Subversion)支持的开放贡献的框架。文档和入门指南都可以在主 Zend Framework 2 网站的 学习 菜单选项下找到。

参见

Composer 和其在 Zend Framework 2 中的用途

Composer 是一个 PHP 依赖管理工具,自 2011 年春季以来一直活跃,在轻松设置项目时非常方便。

Composer 从名为 composer.json 的文件中读取其配置,这是一个由 composer.phar(PHP 归档)读取的 JSON 文件。

当我们使用 Zend Framework 2 骨架应用时,可以使用 Composer 初始化 Zend Framework 2 库。Zend Framework 2 中的其他功能包括安装新的模块或库,我们可以使用这些来扩展我们的应用。

composer.json 文件

如果我们打开 composer.json 文件,我们可以看到文件定义了一些键,这些键告诉 Composer 需要加载什么,以及需要哪些版本。默认情况下,Zend Framework 2 骨架应用的 composer.json 将类似于以下内容:

{
    "name": "zendframework/skeleton-application",
    "description": "Skeleton Application for ZF2",
    "license": "BSD-3-Clause",
    "keywords": [
        "framework",
        "zf2"
    ],
    "homepage": "http://framework.zend.com/",
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": ">2.2.0rc1",
    }
}

如我们所见,这个文件很容易理解,键也很容易解释,但为了确保我们理解正在发生的事情,我们将快速浏览它们。

  • name: 这是带有供应商名称作为前缀的包名,在这种情况下,供应商是 zendframework,而 skeleton-application 是包名。

  • description: 这段简短描述告诉我们这个包的功能。

  • license: 这是软件的许可协议,通常这是众多开源/软件许可协议之一,如 BSD、GPL 和 MIT 许可协议。然而,在 'proprietary' 键下也有可用的闭源软件许可协议。

  • keywords: 这是一个关键词数组,用于在 getcomposer.org 网站上搜索此包时使用。

  • homepage: 这一点非常清楚,不是吗?

  • require: 现在变得有趣了,因为它将告诉 Composer 我们需要运行我们的包的确切内容。在这种情况下,它是一个包含 PHP 的数组,我们需要版本 5.3.3 或更高版本,以及 Zend Framework 2 版本 2.2.0rc1 或更高版本。请注意,然而,在生产环境中,我们应该始终避免使用 dev 版本或带有大于符号的包,因为这可能会破坏我们的应用。请记住,在将应用上线时,始终获取所需的精确版本。

虽然这里没有明确说明,但 Composer 总是将 Zend Framework 2 安装到 vendor 目录,因为 composer.json 中所需的部分说明我们需要 zendframework/zendframework 来运行我们的应用程序。Composer 知道它需要安装到 vendor 目录,因为 zendframework/zendframework 包的类型是库,而这种类型总是被 Composer 复制到 vendor 目录。

升级包

有时候我们只想更新我们的库,例如,当我们知道 Zend Framework 2 的库中已经解决了某个错误,并且我们真的想要它。幸运的是,Composer 提供了一个出色的自更新和更新命令,我们可以使用。

要通过 Composer 自动更新我们的库,我们应该在终端中执行以下命令(这不能通过网页浏览器正确完成):

$ php composer.phar self-update

首先,我们想要确保我们使用的是最新的 Composer,因为使用过时的 Composer 可能会引发不必要的错误。

$ php composer.phar update

这将更新我们放在 composer.jsonrequire 部分中的所有包,以更新到最新(兼容)版本。然而,我们应该小心,当我们想要安装新包,但不更新其他包时,我们应该使用以下命令:

$ php composer.phar update vendor-name/package-name

在这里,vendor-namepackage-name 是我们想要安装的包的名称。

Composer 之所以能工作,是因为所有包都在其网站上注册了 getcomposer.org。在网站上,他们把所有包放在一起,每次我们尝试更新或安装时,composer.phar 都会连接到网站并检索最新的包。

当我们创建自己的模块或库时,我们也可以将其提交到 composer 网站。提交到 composer 的网站将创建一个更好的社区,并在我们开始开发某些应用程序时,更好地理解所需的依赖关系。

参见

基本 Zend Framework 2 结构

当我们考虑 Zend Framework 2 结构时,我们必须意识到,只要我们在配置中告诉 Zend Framework 2 所有路径的位置,Zend Framework 2 实际上并不关心我们的目录结构看起来如何。

在骨架应用程序中,我们看到我们的配置可以在 config/application.config.php 文件中找到。但那个文件仅仅存在于那里,因为 public/index.php 中正在加载它。如果我们,例如,想要将配置文件的存储位置更改为其他地方,我们(在这种情况下)只需要在 public/index.php 文件中更改它。同样,对于模块和 vendor 目录也是如此,因为它们可以放在我们喜欢的任何地方,只要我们告诉 application.config.php 文件确切的位置即可。

如果我们想更改公共目录,我们可以安全地将其更改为我们想要的任何名称,只要我们告诉我们的 Web 服务器新的DocumentRoot在哪里。显然,构建一个好的结构当然是成功应用的关键,因此骨架应用被创建出来。但这并不意味着不同的结构要求必须让我们停止使用 Zend Framework 2,因为框架可以被完全配置以满足这些要求。

然而,我们可以假设,因为我们正在使用由 Zend 提供的骨架,它为我们提供了一个非常优化的结构,以便我们开发。

当我们列出我们骨架应用的初始文件夹时,我们注意到一些以下重要对象:

  • config

  • module

  • public

  • vendor

  • init_autoloader.php

正如我们所见,我们的文件夹中有许多对象,但这些对我们基本应用没有显著的重要性。

文件夹 – config

config文件夹默认包含以下对象:

  • autoload/

  • global.php

  • local.php.dist

  • application.config.php

在这个文件夹中,最关键的文件可能是application.config.php,因为它包含了我们所有的主要配置选项。如果我们打开这个文件,我们可以看到它设置了一些选项,使我们的应用能够工作。

该文件包含,例如,modules键,它告诉框架我们需要为我们的应用加载哪些模块。它还包含module_listener_options - module_paths,它告诉我们的框架在哪里可以找到我们的模块和库,默认情况下是哪些模块和供应商。

config文件夹还包含一个autoload文件夹,该文件夹本身包含两个文件,一个是全局配置覆盖文件,另一个是本地配置覆盖文件。这两个文件默认都是空的。

文件夹 – module

默认的module文件夹包含以下重要对象:

  • Application/config/module.config.php

  • Application/language/src/Application/Controller/IndexController.php

  • Application/src/Application/Controller/IndexController.php

  • Application/view/Application/index/index.phtml

  • Application/view/Application/error/404.phtml

  • Application/view/Application/error/index.phtml

  • Application/view/Application/layout/layout.phtml

  • Application/Module.php

应用模块提供了我们创建新模块时希望看到的基本结构。我们在这里看到的最重要文件是Module.php,它告诉框架我们的模块是如何构建的,它可以在哪里找到我们的控制器,以及更多。

根据我们的应用程序是如何构建的,我们也希望为每个模块有一个配置文件,因为我们希望尽可能保持应用程序的动态性。在骨架应用程序中,我们可以看到我们的Module.php包含一个名为Module::getConfig()的方法;它所做的只是简单地包含到config/module.config.php文件中。虽然理论上我们可以在Module.php中直接定义配置,但如果我们将实际的配置文件与代码分开,会更好一些,因为这样也带来了更多的可维护性,如果我们不需要更改代码来简单地更改配置。

我们还可以在这个文件夹中看到一个language文件夹,它包含翻译我们的应用程序所需的全部 i18n(国际化的缩写,因为它在 I 和 N 之间有 18 个字符)文件。尽管可能被许多开发者使用,但并非我们的所有应用程序都需要翻译,所以我们可能根本不需要在我们的项目中使用这个文件夹。

但如果我们确实需要i18nl10n(本地化),那么按模块而不是按应用程序来做会更有益,这同样是为了可维护性,因为我们不希望整个应用程序(即整个应用程序)为所有模块定义i18n/l10n,因为理论上并非所有模块都必须存在。这就是为什么以模块为导向的工作可以使代码更加动态,同时也更加可维护,因为我们可以安全地假设,如果我们的模块中发生错误,问题也出在那个模块中。

下一个文件夹src可能是我们模块中最有趣的文件夹之一,因为它包含——正如我们可能猜测的那样——我们模块的源代码。src文件夹只包含另一个名为Application的文件夹,这是其中类定义的命名空间。

确保您在src中的子目录命名与它们使用的命名空间相匹配。否则,这不仅可能导致冲突,还可能导致混淆和不一致。例如,如果您的模块名为Winter,那么我们的目录应该被称为src/Winter,以确保所有我们的Winter命名空间都在该目录中。这样我们就可以安全地假设所有针对该命名空间的代码都整洁地放在该目录及其子目录中。

Application子文件夹中,我们可以在我们的骨架应用程序Controller中找到它,该文件夹只包含IndexController.phpIndexController.phpZend\Mvc\Controller\AbstractActionController的扩展,通常用于我们的日常控制器;然而,在同一个命名空间中,还有一个AbstractRestfulController,如果我们想创建一个 RESTful 服务,我们可以使用它。

接下来是view文件夹,它包含了我们所有的视图脚本。视图脚本基本上是我们用来向请求页面的用户实际显示的模板文件。正如我们在Application模块的默认module.config.php文件中所看到的,我们已经将视图脚本配置为指向view目录,这告诉框架当它需要查找任何视图脚本时,应该查看该文件夹。

文件夹 – 模块

正如我们所看到的,view文件夹的结构与配置文件中的结构相同。Application文件夹指的是使用此视图脚本的命名空间,即Application,然后我们看到还定义了一个布局,它用作我们模块的全球布局——如果没有在其他地方定义,则用于整个项目——以及一个error文件夹,它仅在应用程序发生错误时使用。如果我们想了解更多关于 Zend Framework 2 中布局如何工作的信息,你应该查看第四章,使用视图

小贴士

layout文件夹和error文件夹通常被认为是项目的主体模板文件。但这并不意味着我们只能定义一个布局;我们只需要在我们的模块文件中定义另一个布局配置,这样就可以使特定的模块与其他模块不同。

这就完成了我们的module文件夹的构建,当创建其他模块——使用骨架应用程序时——它要求我们使用相同的文件夹结构。

文件夹 – public

public文件夹包含所有公众可能看到的文件。我们需要确保我们的应用程序是安全的,所以我们只会将图片、样式表和 JavaScript 文件放在这里。这里与框架相关的唯一文件将是index.php文件,因为这是初始化我们的应用程序的文件,并且仅在 HTTP 请求时使用。虽然我们可以在这里放置 PHP 文件,但我们强烈建议不要这样做,因为它可能会使你的项目容易受到漏洞的攻击。

文件夹 – vendor

vendor文件夹包含——正如其名所示——由第三方制作的库。在我们的默认项目中,这只会包含运行项目所需的 Zend Framework 2 库(位于zendframework/library文件夹中)。无论何时我们要使用像SmartyDoctrine这样的第三方库,这些库都将被放置在这里。

小贴士

如果我们有一个非应用程序特定的自定义库(或可以成为),我们建议也将它放在这里,特别是如果库在其他地方维护的话。一旦我们开始在其他文件夹中散布我们的库,几乎不可能保持一致性和可维护性。

文件 – init_autoloader.php

init_autoloader.php文件确保我们的项目可以找到我们试图使用的类和命名空间。它由public/index.php文件调用。

为了让 Zend Framework 2 启动并配置自身,会发生一系列操作。如果我们使用骨架应用程序,可以假设以下信息流:

  • /public/index.php: 这是将要运行的第一个文件,因为它是与应用程序相关的唯一公共脚本文件。当运行时,该脚本会将根目录中的init_autoloader.php包含到脚本中,然后初始化 Zend Framework 2。

  • /init_autoloader.php: 这个文件确实做了它所说的,初始化了自动加载器。Zend Framework 2 最好的特性之一是自动加载器的扩展性。这个文件所做的只是确保在初始化应用程序之前,自动加载器已经知道我们使用的大多数命名空间和类(但尚未加载),这样自动加载器就可以在需要时简单地加载类。尽管骨架应用程序有一个非常懒惰的自动加载器,我们不应该以这种形式使用它,但在生产环境中,它可以是一个非常强大的工具,为您的应用程序创建最佳性能。

接下来是什么?

public/index.php加载了已知类和命名空间的位置后,它就准备好启动 Zend Framework 2 MVC 应用程序。

  1. 获取config/application.config.php文件。实际上,目前这个文件并没有对这个文件做任何事情。

  2. 运行Zend\Mvc\Application::init($configurationArray),其中$configurationArray是包含从步骤 1 读取的配置的变量。

    • ServiceManager的初始化,它处理应用程序中的所有服务。

      调用Zend\EventManager\SharedEventManager

      工厂Zend\ModuleManager\ModuleManager

    • ServiceManager请求ModuleManager并运行其loadModules()方法。

    • 这将解决所有模块并加载模块特定的配置。

    • ServiceManager请求Zend\Mvc\Application

    • 它将运行bootstrap()方法。

  3. public/index.php现在将在完全初始化的Zend\Mvc\Application上执行run()方法,这将确保路由触发引导、路由、调度、渲染和完成事件,确保应用程序完成了所请求的操作。

  4. Zend\Mvc\Application完成其run()方法后,它将执行send()方法,该方法将run()方法生成的输出发送回客户端。

这里有一个流程图来展示这个过程如何更直观地展示:

接下来是什么?

关于存储适配器和模式

不同的存储适配器和模式是我们在缓存适配器中实现不同功能以及在不同平台上存储数据(例如,文件系统或仅内存中)的极好方式。这个配方将告诉我们 Zend Framework 2 中所有默认工具的详细信息。

存储适配器的实现

ZF2 中的存储适配器是用于实际缓存我们数据的适配器,这意味着它们还控制数据的存储方式。存储适配器始终实现Zend\Cache\Storage\StorageInterface,它包含存储适配器需要遵守的基本功能。大多数存储适配器也扩展自Zend\Cache\Storage\Adapter\AbstractAdapter,但无法保证这一点。除了StorageInterface之外,存储适配器通常还实现表示增强功能的额外接口。这些实现显然在适配器的功能中扮演着至关重要的角色,因此我们认为最好列出适配器可以使用的、由框架定义的实现列表。

  • AvailableSpaceCapableInterface:此接口提供了一个检查缓存可用空间的方法。

  • Capabilities:此接口提供了检查存储适配器功能的方法,例如缓存的最小和最大 TTL(生存时间)或支持的数据类型(布尔值、字符串、对象等)。

  • ClearByNamespaceInterface:此接口定义了一个可以通过给定命名空间清除缓存的方法。

  • ClearByPrefixInterface:此接口定义了一个可以通过给定前缀清除缓存的方法。

  • ClearExpiredInterface:此接口提供了一个清除过期缓存项的方法。

  • FlushableInterface:此接口能够刷新整个缓存。

  • IterableInterface:此接口提供了遍历缓存项的功能。对于使用foreach遍历它们来说非常方便!

  • OptimizableInterface:此接口提供了优化缓存的能力。

  • TaggableInterface:此接口提供了获取和设置特定缓存项标签的方法,以及通过某个标签删除所有缓存项的能力。

  • TotalSpaceCapableInterface:此接口有一个返回缓存总空间的方法。

存储适配器

现在我们知道了适配器可能实现的接口,是时候给出Zend\Cache\Storage\Adapter命名空间中可用的存储适配器的完整列表了。

Apc 缓存

Apc 或替代 PHP 缓存是一个广为人知的框架,它大量优化 PHP 输出并将编译后的 PHP 代码存储在共享内存中。这样,一些操作码(操作代码)就不需要重新编译,因为它们已经准备好立即使用。Apc 适配器也扩展自AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByNamespaceInterface

  • ClearByPrefixInterface

  • FlushableInterface

  • IterableInterface

  • TotalSpaceCapableInterface

小贴士

此适配器只能在 PHP 中启用了 APC 扩展的情况下才能工作,请在尝试之前确保它已启用。

Dba 缓存

您想将缓存存储在预关系 dbm 数据库中,那么这就是您的机会!此适配器可以将所有内容整齐地存储在漂亮的数据库中。此适配器还扩展了AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByNamespaceInterface

  • ClearByPrefixInterface

  • FlushableInterface

  • IterableInterface

  • OptimizableInterface

  • TotalSpaceCapableInterface

提示

此适配器在能够工作之前需要在 PHP 中启用 dba 扩展,请确保它已启用。

文件系统缓存

文件系统缓存是个人最喜欢的,将缓存存储在古老的文件系统中,这是一个快速且通常可靠的存储位置。此适配器还扩展了AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByNamespaceInterface

  • ClearByPrefixInterface

  • ClearExpiredInterface

  • FlushableInterface

  • IterableInterface

  • OptimizableInterface

  • TaggableInterface

  • TotalSpaceCapableInterface

提示

这听起来非常明显,但请确保我们对我们想要存储缓存的目录有写权限。

Memcached 缓存

Memcached 适配器将缓存存储在内存中,这对于存储不经常更改的静态文件来说是一个很好的方法,可以被认为是半静态的。此适配器还扩展了AbstractAdapter。请注意,Memcached 不受 PHP 内存限制设置的约束,因为 Memcached 将内存存储在 PHP 进程之外,在其自己的 Memcached 进程中。

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • FlushableInterface

  • TotalSpaceCapableInterface

提示

我们需要memcachedPHP 扩展才能通过此适配器进行缓存。请确保在您的系统上已安装并启用了该扩展。

内存缓存

内存适配器将所有缓存存储在 PHP 进程中,与 Memcached 适配器相比,后者将所有缓存存储在外部 Memcached 进程中。此适配器还扩展了AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByPrefixInterface

  • ClearByNamespaceInterface

  • ClearExpiredInterface

  • FlushableInterface

  • IterableInterface

  • TaggableInterface

  • TotalSpaceCapableInterface

Redis 缓存

Redis 是一个键值数据存储,将数据存储在内存中,这做得非常好,肯定是一个值得使用的缓存方法。此适配器还扩展了AbstractAdapter

此适配器实现了以下接口:

  • FlushableInterface

  • TotalSpaceCapableInterface

提示

如果我们要使用此缓存适配器,我们需要确保已加载redis扩展,否则此存储适配器无法使用。请确保已安装并启用了该扩展。

会话缓存

会话存储适配器使用会话来存储我们的缓存。虽然对于每次只有一个用户来说很方便,但这种方法对于查看相同页面的用户来说并不真正有效,因为它每次用户启动会话时都会构建缓存。此适配器还扩展了 AbstractAdapter

此适配器实现了以下接口:

  • ClearByPrefixInterface

  • FlushableInterface

  • IterableInterface

WinCache 缓存

WinCache 是一个在 Microsoft Windows 服务器上运行 PHP 时非常有用的优秀适配器。WinCache 支持 opcache 缓存、文件系统缓存和相对路径缓存。此适配器还扩展了 AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • FlushableInterface

  • TotalSpaceCapableInterface

提示

对于此方法,需要加载 wincache 扩展,并且如果这还不够,您还需要在 Microsoft Windows 上运行,才能使用此功能。

XCache 缓存

XCache 是一个适配器,它利用 PHP 中的 XCache 模块,类似于 APC 的另一个缓存适配器,它是一个快速的 opcache 缓存器,非常有用。此适配器还扩展了 AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByNamespaceInterface

  • ClearByPrefixInterface

  • FlushableInterface

  • IterableInterface

  • TotalSpaceCapableInterface

提示

此适配器要求在 PHP 中加载并启用 XCache 扩展。在使用适配器之前,请确保这是正确的。

ZendServerDisk 缓存

ZendServerDisk 适配器是 Zend Server 应用程序提供的一个优秀的文件系统缓存适配器。如果我们已经安装了 Zend Server,那么此适配器是存储缓存在文件系统上的一个好方法,因为它与 Zend Server 集成得非常好。此适配器还扩展了 AbstractAdapter

此适配器实现了以下接口:

  • AvailableSpaceCapableInterface

  • ClearByNamespaceInterface

  • FlushableInterface

  • TotalSpaceCapableInterface

提示

要使此适配器工作,您需要安装 Zend Server,否则它将抛出异常。

ZendServerShm 缓存

ZendServerShm 适配器也要求我们安装 Zend Server,但如果我们已经安装并且我们想在共享内存(shm)中缓存项目,那么这是一种非常棒的方法,因为此适配器与 Zend Server 集成得非常好。此适配器还扩展了 AbstractAdapter

此适配器实现了以下接口:

  • ClearByNamespaceInterface

  • FlushableInterface

  • TotalSpaceCapableInterface

提示

要使此适配器工作,您需要安装 Zend Server 以使此适配器工作,否则它将抛出异常。

缓存模式

当我们开始缓存时,我们很快就会发现自己处于与性能相反的情况,而我们只是想让一切更快。这就是为什么在 ZF2 中有一些被称为缓存模式的类,这些类是为了我们在想要克服一些常见问题时使用的。

就像适配器一样,模式也是接口的实现;在这种情况下是PatternInterface。而且因为我们通常也想要一些基本功能,所以大多数模式也扩展自AbstractPattern类。

模式的选项通过PatternOptions类定义,这将在稍后进一步解释。

CallbackCache 模式

我们想要的是回调还是缓存?有时我们确实不知道,所以我们将让模式自己决定!CallbackCache模式首先确保我们的回调结果已经定义在缓存中,如果是的话,就返回它。如果结果尚未在缓存中,它将调用我们的回调函数,将输出放入结果中,然后返回它。无论如何,第二次处理该回调时,我们将得到我们的缓存。所以如果这是一个长时间运行的方法,我们不需要再次执行代码,这将大大加快速度。

此模式还考虑了该回调的参数,这意味着你实际上不必过多担心回调会给你提供错误的结果!

此模式使用AbstractPattern类。

CaptureCache 模式

CaptureCache模式通过启动ob_start()ob_implicit_flush()来捕获我们发送到浏览器的输出。然后我们可以在每次发送输出时检查缓存是否存在,这样我们就可以显示输出而不是生成它。

此模式使用AbstractPattern类。

小贴士

此模式在定义后不会自动输出缓存,开发者在使用 start 方法之前需要首先获取缓存。如果我们希望在生成新内容之前输出已存在的缓存,我们应该使用OutputCache模式。

ClassCache 模式

ClassCache模式缓存类方法调用的输出,并返回该输出而不是实际调用。但当然,这只有在缓存实际可用时才会发生,否则它将只执行方法调用并缓存结果。类名(而不是对象)需要设置在PatternOptions::setClass中以使其工作。

此模式使用AbstractPattern类。

ObjectCache 模式

ObjectCache模式缓存对象,可以在检索时调用其方法,如果我们有需要长时间持久化的对象,这非常方便。对象需要设置在PatternOptions::setObject中以使其工作。

此模式使用AbstractPattern类。

OutputCache 模式

OutputCache 模式如果定义了缓存,则会输出缓存。如果没有定义,则 OutputCache 会缓存输出并在脚本结束(或调用结束方法,以先到者为准)时设置缓存。

此模式使用 AbstractPattern 类。

PatternOptions 模式

PatternOptions 模式可以用来设置或从模式中获取选项(分别对应 setOptionsgetOptions)。对于大多数模式,在使用模式之前通常需要设置某种形式的选项。例如,考虑 setStorage 方法,因为模式在实际上存储东西之前需要知道存储适配器。

解释差异

存储适配器存储和检索缓存数据。我们可以设置选项来确定有效期的长度,或者检查缓存是否已满,但我们不能确定它是如何存储的,因为这是适配器工作描述的一部分。

然而,模式本身并不存储任何内容。它们通过检查缓存是否已经存在,或者缓存是否是我们期望的(例如,当我们使用不同的方法调用或不同的参数时)来确定是否需要存储任何内容。它们确实会告诉适配器它们想要检索和存储的内容,这样适配器就可以找出如何从实际存储中再次检索它。

在开发者的眼中,我们更愿意在使用适配器之前使用模式,因为我们不希望过多地干扰适配器,如果已经有模式为我们做了大部分工作的话。

posted @ 2025-09-09 11:30  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报