10.10. 插件

10.10.1. 简介

控制器(Controller)结构包含一个可以在控制器周期内确定事件发生时调用用户代码的插件系统。 前端控制器(Front controller)使用插件 broker 作为用户插件注册,同时插件 broker 确保前端控制器中注册的每个插件都在事件发生时调用相应的事件方法。

事件方法定义在虚类 Zend_Controller_Plugin_Abstract,用户插件应当从这个类继承:

  • routeStartup()Zend_Controller_Front 向注册的 路由器 发送请求前被调用。

  • routeShutdown()路由器 完成请求的路由后被调用。

  • dispatchLoopStartup()Zend_Controller_Front 进入其分发循环(dispatch loop)前被调用。

  • preDispatch() 在动作由 分发器 分发前被调用。该回调方法允许代理或者过滤行为。通过修改请求和重设分发标志位(利用 Zend_Controller_Request_Abstract::setDispatched(false) )当前动作可以跳过或者被替换。

  • postDispatch() 在动作由 分发器 分发后被调用。该回调方法允许代理或者过滤行为。通过修改请求和重设分发标志位(利用 Zend_Controller_Request_Abstract::setDispatched(false) )可以指定新动作进行分发。

  • dispatchLoopShutdown()Zend_Controller_Front 推出其分发循环后调用。

10.10.2. 编写插件

只需要包含并扩展抽象类 Zend_Controller_Plugin_Abstract 即可编写插件类。

class MyPlugin extends Zend_Controller_Plugin_Abstract
{
    // ...
}


        

Zend_Controller_Plugin_Abstract 的全部方法都不是抽象的, 这意味着插件类并不是一定要去实现前面列出的每一个事件方法。 插件的开发者只要实现需要用到的方法即可。

Zend_Controller_Plugin_Abstract 也可以通过调用 getRequest()getResponse() 方法从控制器中分别获取 request 对象和 response 对象.

10.10.3. 使用插件

可以使用 Zend_Controller_Front::registerPlugin() 在任何时候注册插件类。 下面的代码片段说明了如何在控制器链条中使用插件。

class MyPlugin extends Zend_Controller_Plugin_Abstract
{
    public function routeStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>routeStartup() called</p>\n");
    }

    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>routeShutdown() called</p>\n");
    }

    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>dispatchLoopStartup() called</p>\n");
    }

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>preDispatch() called</p>\n");
    }

    public function postDispatch(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->appendBody("<p>postDispatch() called</p>\n");
    }

    public function dispatchLoopShutdown()
    {
        $this->getResponse()->appendBody("<p>dispatchLoopShutdown() called</p>\n");
    }
}

$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory('/path/to/controllers')
      ->setRouter(new Zend_Controller_Router_Rewrite())
      ->registerPlugin(new MyPlugin());
$front->dispatch();

        

假设没有动作产生任何输出,而只有一个动作被调用,前面演示的插件仍然会产生下面的输出:

<p>routeStartup() called</p>
<p>routeShutdown() called</p>
<p>dispatchLoopStartup() called</p>
<p>preDispatch() called</p>
<p>postDispatch() called</p>
<p>dispatchLoopShutdown() called</p>

        
[注意] 注意

插件可以在前端控制器(Front controller)执行的任何时候被被注册, 如果一个事件在注册时已经完成,则这个事件对应的事件方法不会被触发。

10.10.4. 获取和控制插件

有时,可能需要取消注册或者获取一个插件。下面列出的前端控制器中的方法可以实现这个功能:

  • getPlugin($class) 允许获取指定类名的一个插件。 如果没有插件匹配,将返回 false。如果有多个指定类的插件被注册,则返回一个数组。

  • getPlugins() 返回全部插件。

  • unregisterPlugin($plugin) 允许从插件列表中移除一个插件。 传递一个插件件对象,或者需要移除的插件的类名。如果传递类名,任何该类的插件都将被移除。

10.10.5. 包含在标准发行包中的插件

Zend Framework 在其标准发行包中包含错误处理插件。

10.10.5.1. 动作堆栈

动作堆栈插件可以管理一个请求堆栈,其操作象postDispatch插件。如果一个转发(例如,对另一个动作的调用)在当前请求对象中已经检测到,它不做任何事情。然而,如果没有检测到,它就检查堆栈并把最上面的一个取出,然后转发给由那个请求指定的动作。堆栈按照LIFO(后进先出)顺序处理。

你可以在任何时候用 Zend_Controller_Front::getPlugin('Zend_Controller_Plugin_ActionStack')从前端控制器获取插件。一旦你有插件对象,有很多机制你可以用来操作。

  • getRegistry()setRegistry()。在内部, 动作堆栈 使用一个Zend_Registry 实例来存储堆栈。你可以用不同的注册表实例来代替或在这些访问器里获取。

  • getRegistryKey()setRegistryKey()。当弹出堆栈时,这些可以用来识别使用哪个注册表键。缺省地值是'Zend_Controller_Plugin_ActionStack'。

  • getStack() 允许你全面地获取动作堆栈。

  • pushStack()popStack() 分别允许你弹出和压栈。pushStack() 接受请求对象。

一个附加的方法,forward(),准备一个请求对象,并在前端控制器设置当前请求状态给提供的请求对象的状态,使它不可派遣(强制另一个派遣循环迭代)。

10.10.5.2. Zend_Controller_Plugin_ErrorHandler

Zend_Controller_Plugin_ErrorHandler提供了一个活动的插件,用来处理从程序抛出的异常,包括那些从缺控制器或动作的来的结果;它是一个列在MVC Exceptions section里的方法的一个替代。

插件的基本目标是:

  • 监视由于缺失控制器或动作方法而产生的异常

  • 监视动作控制器里产生的异常

换句话说,ErrorHandler 插件设计用来处理HTTP 404 类型的错误(找不到页面)和 500 类型错误(内部错误)。它不打算抓取有其它插件或路由产生的异常。

缺省地,在缺省模块中,Zend_Controller_Plugin_ErrorHandler将转发给ErrorController::errorAction()。你可以通过使用在插件中不同的访问器给它们设置替代的值:

  • setErrorHandlerModule() 设置控制器模块来使用。

  • setErrorHandlerController() 设置控制器来用。

  • setErrorHandlerAction() 设置控制器动作来用。

  • setErrorHandler()接受联合数组,它可以包含任何键,如'module'、 'controller' 或 'action',以及要给它们设置的合适的值。

另外,你可以传递一个可选的联合数组给可以代理setErrorHandler()的构造函数。

Zend_Controller_Plugin_ErrorHandler注册一个postDispatch()钩子和检查注册在响应对象里的异常。如果发现有异常,它试图转发给注册的错误处理器(handler)动作。

如果在派遣错误处理器时发生异常,这插件将告诉前端控制器抛出异常,并重新抛出和带响应对象注册的最后一个异常。

10.10.5.2.1. 使用 ErrorHandler 作为一个 404 处理器(handler)

因为ErrorHandler插件不仅抓取程序错误,而且也抓取在控制器链里的由于缺失控制器类和/或动作方法而产生的错误,它可以用作一个404处理器。这样做,需要让错误控制器检查异常类型。

异常的抓取被记录在一个对象里,这个对象注册在请求里。使用Zend_Controller_Action::_getParam('error_handler')来读取它:

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');
    }
}

        

一旦有错误对象,可通过$errors->type来获得类型。它将是下面其中之一:

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER,指示控制器没有被发现。

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION,指示请求动作没有被发现。

  • Zend_Controller_Plugin_ErrorHandler::EXCEPTION_OTHER,指示其它异常。

然后可以测试头两个类型中的任意一个,并且,如果这样,显示一个404页面:

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // 404 error -- controller or action not found
                $this->getResponse()
                     ->setRawHeader('HTTP/1.1 404 Not Found');

                // ... get some output to display...
                break;
            default:
                // application error; display error page, but don't 
                // change status code
                break;
        }
    }
}

        

最后,你可以读取异常,这个异常由错误管理器通过抓取error_handler对象的exception属性来触发的:


public function errorAction()
{
        $errors = $this->_getParam('error_handler');


        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // 404 error -- controller or action not found
                $this->getResponse()
                      ->setRawHeader('HTTP/1.1 404 Not Found');

                // ... get some output to display...
                break;
            default:
                // application error; display error page, but don't change
                // status code

                // ...

                // Log the exception:
                $exception = $errors->exception;
                $log = new Zend_Log(
                    new Zend_Log_Writer_Stream(
                        '/tmp/applicationException.log'
                    )
                );
                $log->debug($exception->getMessage() . "\n" .  
                            $exception->getTraceAsString());
                break;
        }
}

        
10.10.5.2.2. 处理以前呈现的(rendered)输出

如果你在一个请求里派遣多个动作,或者你的动作对render()做多次调用,很可能响应对象已经有存储在它里面的内容。这可以导致呈显期望的内容和错误的内容的混合体。

如果你希望呈现错误内嵌到这样的页面,不需要修改。如果你不希望呈现这样的内容,你应该在呈现任何视图之前清除响应体:

$this->getResponse()->clearBody();

        
10.10.5.2.3. 插件用法示例

例 10.16. Standard usage

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Zend_Controller_Plugin_ErrorHandler());

            

例 10.17. Setting a different error handler

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Zend_Controller_Plugin_ErrorHandler(array(
    'module'     => 'mystuff',
    'controller' => 'static',
    'action'     => 'error'
)));

            

例 10.18. Using accessors

$plugin = new Zend_Controller_Plugin_ErrorHandler();
$plugin->setErrorHandlerModule('mystuff')
       ->setErrorHandlerController('static')
       ->setErrorHandlerAction('error');

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin($plugin);

            

10.10.5.2.4. 错误控制器示例

为了使用错误处理器插件,你需要错误控制器。下面是个简单的例子。

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // 404 error -- controller or action not found
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                $content =<<<EOH
<h1>Error!</h1>
<p>The page you requested was not found.</p>
EOH;
                break;
            default:
                // application error
                $content =<<<EOH
<h1>Error!</h1>
<p>An unexpected error occurred. Please try again later.</p>
EOH;
                break;
        }

        // Clear previous content
        $this->getResponse()->clearBody();

        $this->view->content = $content;
    }
}