The Developer Day | Staying Curious

Mar/10

10

Zend Framework Advanced Error Controller

The default Zend Framework Error Controller generated by Zend_Tool is quite simple. It displays a simple error message, sets a response status and if exception display is enabled in the current environment, an exception message, stack trace and request variables are displayed.

While such a standard error controller may work well for many web applications it may not be suitable for everyone. The main disadvantage of the default error controller is that it does not notify developers of the errors that occurred and instead silently logs them. Many enterprise web applications will find this unacceptable and will try to implement their means of solving the issue. In this post I’ll try to show how a more advanced Zend Framework error controller could be implemented to help developers tackle errors quickly.

class ErrorController extends Zend_Controller_Action
{
    private $_notifier;
    private $_error;
    private $_environment;
    public function init()
    {
        parent::init();
        $bootstrap = $this->getInvokeArg('bootstrap');
        $environment = $bootstrap->getEnvironment();
        $error = $this->_getParam('error_handler');
        $mailer = new Zend_Mail();
        $session = new Zend_Session_Namespace();
        $database = $bootstrap->getResource('Database');
        $profiler = $database->getProfiler();
        $this->_notifier = new Application_Service_Notifier_Error(
            $environment,
            $error,
            $mailer,
            $session,
            $profiler,
            $_SERVER
        );
        $this->_error = $error;
        $this->_environment = $environment;
   }
    public function errorAction()
    {
        switch ($this->_error->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Page not found';
                break;
            default:
                $this->getResponse()->setHttpResponseCode(500);
                $this->_applicationError();
                break;
        }
        // Log exception, if logger available
        if ($log = $this->_getLog()) {
            $log->crit($this->view->message, $this->_error->exception);
        }
    }
    private function _applicationError()
    {
        $fullMessage = $this->_notifier->getFullErrorMessage();
        $shortMessage = $this->_notifier->getShortErrorMessage();
        switch ($this->_environment) {
            case 'live':
                $this->view->message = $shortMessage;
                break;
            case 'test':
                $this->_helper->layout->setLayout('blank');
                $this->_helper->viewRenderer->setNoRender();
                $this->getResponse()->appendBody($shortMessage);
                break;
            default:
                $this->view->message = nl2br($fullMessage);
        }
        $this->_notifier->notify();
    }
    private function _getLog()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        if (!$bootstrap->hasPluginResource('Log')) {
            return false;
        }
        $log = $bootstrap->getResource('Log');
        return $log;
    }
}

The modified error controller is aware of the environment it is running in. It’s likely that depending on the environment you would want to display different layouts with different information. For example while debugging Zend Controller Tests you may want to reduce the amount of HTML appearing in your terminal screen by disabling the layout while running in the test environment. You’ll also notice the Application_Service_Notifier_Error class dependency. This class is responsible for deciding whether to send an email to the developers and gathers potentially helpful information from different sources. You’ll also notice how the dependencies for the notifier are instantiated. It can be done in different ways using dependency injection frameworks, using bootstrap resources and so on. It’s up to you to decide what fits your application better.

class Application_Service_Notifier_Error
{
    protected $_environment;
    protected $_mailer;
    protected $_session;
    protected $_error;
    protected $_profiler;
    public function __construct(
        $environment,
        ArrayObject $error,
        Zend_Mail $mailer,
        Zend_Session_Namespace $session,
        Zend_Db_Profiler $profiler,
        Array $server)
    {
        $this->_environment = $environment;
        $this->_mailer = $mailer;
        $this->_error = $error;
        $this->_session = $session;
        $this->_profiler = $profiler;
        $this->_server = $server;
    }
    public function getFullErrorMessage()
    {
        $message = '';
        if (!empty($this->_server['SERVER_ADDR'])) {
            $message .= "Server IP: " . $this->_server['SERVER_ADDR'] . "\n";
        }
        if (!empty($this->_server['HTTP_USER_AGENT'])) {
            $message .= "User agent: " . $this->_server['HTTP_USER_AGENT'] . "\n";
        }
        if (!empty($this->_server['HTTP_X_REQUESTED_WITH'])) {
            $message .= "Request type: " . $this->_server['HTTP_X_REQUESTED_WITH'] . "\n";
        }
        $message .= "Server time: " . date("Y-m-d H:i:s") . "\n";
        $message .= "RequestURI: " . $this->_error->request->getRequestUri() . "\n";
        if (!empty($this->_server['HTTP_REFERER'])) {
            $message .= "Referer: " . $this->_server['HTTP_REFERER'] . "\n";
        }
        $message .= "Message: " . $this->_error->exception->getMessage() . "\n\n";
        $message .= "Trace:\n" . $this->_error->exception->getTraceAsString() . "\n\n";
        $message .= "Request data: " . var_export($this->_error->request->getParams(), true) . "\n\n";
        $it = $this->_session->getIterator();
        $message .= "Session data:\n\n";
        foreach ($it as $key => $value) {
            $message .= $key . ": " . var_export($value, true) . "\n";
        }
        $message .= "\n";
        $query = $this->_profiler->getLastQueryProfile()->getQuery();
        $queryParams = $this->_profiler->getLastQueryProfile()->getQueryParams();
        $message .= "Last database query: " . $query . "\n\n";
        $message .= "Last database query params: " . var_export($queryParams, true) . "\n\n";
        return $message;
    }
    public function getShortErrorMessage()
    {
        $message = '';
        switch ($this->_environment) {
            case 'live':
                $message .= "It seems you have just encountered an unknown issue.";
                $message .= "Our team has been notified and will deal with the problem as soon as possible.";
                break;
            default:
                $message .= "Message: " . $this->_error->exception->getMessage() . "\n\n";
                $message .= "Trace:\n" . $this->_error->exception->getTraceAsString() . "\n\n";
        }
        return $message;
    }
    public function notify()
    {
        if (!in_array($this->_environment, array('live', 'stage'))) {
            return false;
        }
        $this->_mailer->setFrom('[email protected]');
        $this->_mailer->setSubject("Exception on Application");
        $this->_mailer->setBodyText($this->getFullErrorMessage());
        $this->_mailer->addTo('[email protected]');
        return $this->_mailer->send();
    }
}

This class provides an extensive report providing helpful details in what state the application was when an exception occurred. What’s the IP address of the server (maybe the application is distributed on many servers), what was the time, was it an AJAX request, what was user’s session data, request data.

One of the nice things to have is to be able to tell what was the last database query executed. This is especially useful if some dynamic database query fails or someone is trying to make an SQL injection. The easiest way to achieve this is to use a Zend_Db_Profiler. But the default profiler consumes a lot of server resources and should not be enabled on production environments. To work around this we use a custom dummy profiler that does no profiling at all and just stores the last query information in memory.

class Application_Db_Profiler extends Zend_Db_Profiler
{
    protected $_lastQueryText;
    protected $_lastQueryType;
    public function queryStart($queryText, $queryType = null)
    {
        $this->_lastQueryText = $queryText;
        $this->_lastQueryType = $queryType;
        return null;
    }
    public function queryEnd($queryId)
    {
        return;
    }
    public function getQueryProfile($queryId)
    {
        return null;
    }
    public function getLastQueryProfile()
    {
        $queryId = parent::queryStart($this->_lastQueryText, $this->_lastQueryType);
        return parent::getLastQueryProfile();
    }
}

The custom error controller will only notify developers of errors that occur on production and stage environments to avoid spamming people with exceptions from the unstable development environment. The Application_Service_Notify_Error class is also highly testable. All the dependencies can be mocked, no global variables or constants are used. The class itself could be more refined by employing polymorphism instead of if statements but I believe it’s better to keep the example simple to make it easily understandable.

Depending on which version of the Zend Framework is being used the implementation for the custom error controller may be a little different, but the general idea is the same. In short the advanced error controller provides additional information such as session data, database queries, server variables and also is capable of notifying developers when errors occur on production or stage environments. Please let me know if this is helpful by providing feedback in the comments.

RSS Feed

14 Comments for Zend Framework Advanced Error Controller

Mike A | March 21, 2010 at 9:40 AM

Good feedback, thanks. I shall refer to it in my own writings.

One thing often missing in Zend related feedback/tutorials is [where] things go. The ErrorController is obvious enough - but presumably Application_Service_Notifier_Error belongs in a library. Some developers, particularly those new to ZF, will not find that so obvious.

Fatih | June 16, 2010 at 9:16 PM

Nice Tutorial ,
but there are some syntax error ,
in getfullmessage method,

if (!emptyempty)

should be if(!empty),

and i agree with mike, may be you should also explain your configuration, application.ini file.
so that newbies can understand parameters in swith case

__fabrice | July 9, 2010 at 9:44 PM

Hi,

Can you explain a little bit please. Or give us a zip file with all files ?

I have 2 errors :

- Fatal error: Call to a member function getProfiler() on a non-object in …/…/ErrorController.php on line 20 ==> $profiler = $database->getProfiler();

and

- Fatal error: Class ‘Application_Service_Notifier_Error’ not found in …I create an Error.php file here : /library/Service/Notifier/Error.php. Correct ?

Thanks
F.

awdhesh\ | July 19, 2010 at 1:03 PM

hi sir i need your help i want to make diffrent diffrent layout in zend framework and diffrent module can u help me

Eric Lamb | August 2, 2010 at 6:37 AM

Wonderful post Žilvinas; I was thinking about putting together a similar ErrorController myself but yours worked like a charm for a good base.

Well done :)

Jeff | August 24, 2010 at 10:25 AM

Hi,
When I comment all code about DB from your code.
It work perfect.
When i open,
I got an error at line 20 in file ErrorController.php
$profiler = $database->getProfiler();

Fatal error: Call to a member function getProfiler() on a non-object

I has this code in Application config file to load class Profiler like manual of Zend. http://framework.zend.com/manual/en/zend.db.profiler.html

Code:
;======================
resources.database.profiler.class = “WDS_Db_Profiler”
resources.database.profiler.enabled = true
;======================

I’m looking for all your code. Don’t have any code to load Application_Db_Profiler.

Can you tell me how to fix this error to show notify about db when application error.
Thank you very much for your solution.

Jeff | August 24, 2010 at 11:07 AM

In your code:
$database = $bootstrap->getResource(‘database’);
$profiler = $database->getProfiler();

Can you show me your database config to load customize profiler (Application_Db_Profiler)

My Code:
;======================
resources.database.profiler.class = “WDS_Db_Profiler”
resources.database.profiler.enabled = true
;======================

But it not work.

kebunanggur | December 14, 2010 at 10:07 AM

hi there, im an asp developer for some years and now starting build a php zend framework project. wanna thanks for your great codes and tutorial.

but as i a totally newbee on php and this (OMG), i was wondering, where do i have to attached those codes?

thanks

Jonathan Kushner | December 30, 2010 at 3:02 PM

Very nice! Your missing the class variable server on Application_Service_Notifier_Error.

Web Punk » Zend Framework Advanced Error Controller - The Developer Day | March 29, 2011 at 9:39 AM

[...] via Zend Framework Advanced Error Controller - The Developer Day. [...]

David Weinraub | September 16, 2011 at 8:11 AM

Very nice.

On top of the improved ErrorController idea, your modified Db_Profiler idea seems likely to be very useful for getting the last query. Cool!

Thanks!

Mark van Berkel | March 14, 2012 at 1:23 PM

For the profiler config to work I used:

resources.db.adapter = “Pdo_Mysql”
resources.db.params.username = “etcetc”
resources.db.params.password = “etcetc”
resources.db.params.dbname = “etcetc”
resources.db.params.host = “etcetc”
resources.db.params.port = “etcetc”
resources.db.params.profiler.class = “MyApplicaiton_Db_Profiler”
resources.db.params.profiler.enabled = true

Zend Db + Paginator - Query anzeigen lassen | March 20, 2012 at 9:46 PM

[...] Eine Variante wäre einen eigenen Profile zu verwenden der Dir die letzte Query protokolliert: Zend Framework Advanced Error Controller - The Developer Day zum Ende des Artikels [...]

Leave a comment!

<<

>>

Find it!

Theme Design by devolux.org