CakePHP flash error message logging

When your CakePHP application cannot handle a submitted form it shows a flash error message (normally a red bar) and some fields in red (validation errors). By default, CakePHP log exceptions and fatal errors to “app/tmp/logs/error.log”. Unfortunately flash error messages and validation errors are not logged by default. This can be explained since the errors are actually handled and can thus be considered “user errors” and not “application errors”. But when a customer needs assistance, you actually need both types of error logs. Note that the Apache access log will also log these requests, but normally this does not provide enough information. In this post we will show how you can add flash error message logging to your CakePHP application.

First we will show that normal error logging is pretty useful. For example, when you call the PHP function “nltobr()” while this function is actually called “nl2br()” it will be logged as expected:

$ cat app/tmp/logs/error.log
2013-06-09 21:54:44 Error: [FatalErrorException] Call to undefined function nltobr()

For flash error message logging we have created a data structure and a CakePHP component that allows you to make a nice overview like this:

runtime_list

Figure 1: List with all pages served with flash error messages

And when you actually view the details of the flash error message you see the following screen:
runtime
Figure 2: Detail page for the table, showing a flash error message

You can clearly see that somebody was trying to edit user 1. The error message was not very descriptive, while the validation errors were pretty clear about the missing data (password).

Instructions

The data structure used is a MySQL database table that can be created by executing the following SQL:

CREATE TABLE `runtime_errors` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `created` datetime NOT NULL,
 `ip_address` varchar(255) NOT NULL,
 `method` varchar(255) NOT NULL,
 `url` varchar(255) NOT NULL,
 `referer` varchar(255) NOT NULL,
 `redirect_url` varchar(255) NOT NULL,
 `data` text NOT NULL,
 `validation_errors` text NOT NULL,
 `flash_message` text NOT NULL,
 `user_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `user_id` (`user_id`),
 KEY `created` (`created`),
 CONSTRAINT `runtime_errors_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Now create the following model and save it to “app/Model/RuntimeError.php”:

<?php
App::uses('AppModel', 'Model/App');

class RuntimeError extends AppModel {
  public $displayField = 'url';
  public $belongsTo = array('User');
}

Add the following component code to “app/Controller/Component/ErrorLoggerComponent.php” to log the flash messages:

<?php
App::uses('Component','Controller');

class ErrorLoggerComponent extends Component
{
  public function beforeRedirect(&$controller, $url, $status, $exit) {
    $this->log($controller,$url);
    return parent::beforeRedirect($controller, $url, $status, $exit);
  }

  public function beforeRender(&$controller) {
    $this->log($controller);
    parent::beforeRender($controller);
  }

  public function log(&$controller,$redirectUrl=false) {
    $session = $controller->Session->read();
    $flash = isset($session['Message']['flash'])?$session['Message']['flash']:false;
    $success = isset($flash['params']['class'])?$flash['params']['class']=='success':false;
    if ($flash && $success==false) {
      $controller->User->RuntimeError->create();
      $data = array('RuntimeError'=>array());
      $data['RuntimeError']['method'] = $controller->request->method();
      $data['RuntimeError']['url'] = $controller->request->here;
      $data['RuntimeError']['referer'] = $controller->request->referer();
      $data['RuntimeError']['redirect_url'] = $redirectUrl?Router::url($redirectUrl):'';
      $data['RuntimeError']['data'] = Spyc::YAMLDump($controller->params->data,2,80);
      $data['RuntimeError']['validation_errors'] = Spyc::YAMLDump($this->getErrors(),2,80);
      $data['RuntimeError']['flash_message'] = Spyc::YAMLDump($flash,2,80);
      $data['RuntimeError']['user_id'] = $controller->Auth->user('id');
      $data['RuntimeError']['ip_address'] = $controller->request->clientIp(true);
      if (!$controller->User->RuntimeError->save($data)) {
        throw new InternalErrorException('Could not log action');
      }
    }
  }

  private function getErrors()
  { $validationErrors = array();
    $models = ClassRegistry::keys();
    foreach ($models as $currentModel) {
      $currentObject = ClassRegistry::getObject($currentModel);
      if (is_a($currentObject, 'Model')) {
        if ($currentObject->validationErrors) {
          $validationErrors[$currentObject->alias] =& $currentObject->validationErrors;
        }
      }
    }
    return $validationErrors;
  }
}

Note that you may want to fine-tune the business logic that decides which messages to log (only errors), because the above code will log all flash messages without exception. To load the above component you have to add the “ErrorLogger” to the “$components” array in “app/Controller/AppController.php”:

 public $components = array(
   ...
   'ErrorLogger'=>array()
 );

Dependencies

For writing YAML output from CakePHP we have added “Spyc” to composer.json, which makes our full composer.json file look like this:

{
  "minimum-stability": "dev",
  "config": {
    "vendor-dir": "vendors"
  },
  "repositories" : [
    {
      "type": "package",
      "package": {
        "name" : "cakephp/cakephp",
        "version" : "2.3.5",
        "source" : {
          "type" : "git",
          "url" : "git://github.com/cakephp/cakephp.git",
          "reference" : "2.3.5"
        },
        "bin" : ["lib/Cake/Console/cake"]
      }
    }
  ],
  "extra": {
    "installer-paths": {
      "app/Plugin/DebugKit": ["cakephp/debug_kit"]
    }
  },
  "require" : {
    "php": ">=5.2",
    "cakephp/cakephp" : "2.3.5",
    "cakephp/debug_kit": "2.2.*",
    "mustangostang/spyc": "0.5.*"
  }
}

To achieve the nice looking index and view pages from Figure 1 and 2 you will have to “bake” some views and controllers. Good luck!

Share

Leave a Reply

Your email address will not be published. Required fields are marked *