Si han trabajado con Zend Framework usando el patrón MVC, sabrán lo que es el Front Controller y sus «Action Plugins».
A principios de este año me topé con un problema que dejé pasar, hasta ahora.
Al configurar el FrontController, uno de los parámetros usuales a fijar, es registrar y configurar el plugin ErrorHandler. Este plugin permite redirigir a un módulo->controlador->acción cuando se dispara una excepción durante el Dispatch Loop. Esto en conjunto con establecer que el FrontController no dispare excepciones, permite presentar un bonito error manualmente, con los detalles que el desarrollador quiera mostrar.
Sin embargo, hasta hoy, no fui capaz de entender porque cuando otro Plugin disparaba una excepción, esta pasaba de largo, sin hacer caso a mi ErrorHandler.
Afortunadamente, hoy me desperté con el pie izquierdo, por lo cual me di la maña de examinar el código del Framework (bendito sean los kits de desarrollo libres). Y afortunadamente conozco Xdebug, que me facilitó la tarea de encontrarlo.
Es un problema de diseño del ZendFramework, pero lamentablemente, según lo que pude leer, no se me ocurre uno mejor que proponer. En cambio, mi solución es OLVIDAR el plugin ErrorHandler que trae el framework, y hacer uno manualmente, con una maña más mañosa que el original.
El problema de diseño
El dispatch loop, es un proceso en el cual se ejecutan, secuencialmente, los métodos de los Plugins y el controlador de acción que corresponda.
El Zend Framework, usando todas las capacidades de Programación Orientada a Objetos que PHP permite, dispara excepciones cada vez que ocurre algun error en algún procedimiento.
Si se dispara una, entonces el dispatch loop la captura. Si configuramos correctamente el FrontController, el resultado esperado es que nos redirija al controlador de errores que hayamos creado.
Para lograr eso, el Plugin ErrorHandler engaña al dispatch, diciéndole que el ciclo NO HA FINALIZADO, reemplaza el controlador de acción que se estaba ejecutando por el controlador de acción de error y finalmente, continua el ciclo del Dispatch.
Eventualmente, podría ocurrir que el controlador de acción de error también dispare una excepción. Si esto ocurre y se mantiene la misma condición, se crearía un ciclo infinito. Para prevenir esto, el plugin ErrorHandler comprueba «si es que ya se encuentra en un controlador de acción de error «, en tal caso simplemente arroja la excepción, pues no se puede manejar si el manejador está malo.
Pero, en Zend Framework, cuando se redirije de una acción a otra, el dispatch VUELVE a ejecutar los plugins. Por lo tanto, si es un plugin el que dispara la excepción, entonces caerá en el caso anteriormente descrito. Es decir, «ya me encuentro en un controlador de error, por ende, si no se puede manejar la excepción, simplemente hay que lanzarla (throw)».
¡Esto es lo que ocurre!, si es un plugin el erróneo, entonces se volverá a ejecutar cuando se redirija al controlador de errores.
Solución propuesta, sin modificar las clases del Framework.
Consiste en crear un plugin ErrorHandler propio, que haga lo mismo que el del framework, pero además des-registre los plugins. De este modo, al entrar en el loop, no volverá a producirse un error, en cambio se capturará la excepción como era esperado, para finalmente desplegarla por pantalla a través de una vista.
Antes de continuar, es necesario tener la precaución de que nuestro Error Controller tenga la menor cantidad posible de código, que pueda producir excepciones y además no dependa de algunos de los plugins que posiblemente también disparen excepciones. OJalá, capture el error y lo mande por pantalla en una plantilla HTML y nada más.
El ÚNICO plugin que no debería ser des-registrado, es el que maneja la vista (o mejor aún, el Layout de la vista).
¿Como? Así:
/** Zend_Controller_Plugin_ErrorHandler */
require_once 'Zend/Controller/Plugin/ErrorHandler.php';
/**
* Replace of classic Zend Framework ErrorHandler controller plugin
*/
class Rox_Controller_Plugin_ErrorHandler extends Zend_Controller_Plugin_ErrorHandler
{
public function postDispatch(Zend_Controller_Request_Abstract $request)
{
$frontController = Zend_Controller_Front::getInstance();
parent::postDispatch($request);
$response = $this->getResponse();
if (($response->isException()) && (!$this->_isInsideErrorHandlerLoop)) {
foreach( $frontController->getPlugins() as $plugin )
{
// Don't unregister Layout Plugin, to show Error Layout
if( !($plugin instanceof Rox_Controller_Plugin_Layout) )
$frontController->unregisterPlugin($plugin);
}
}
}
}
Acá está el ejemplo implementado en Gonium, funcioná impecable hasta ahora.
¿TO DO?
Se me ocurre que puede ser buena idea extender, además, el Plugin Broker del FrontController, de tal modo que se pueda establecer si un plugin es «sticky» (permanente) o no, de tal modo que el nuevo ErrorHandler des-registre solo aquellos plugins prescindibles.