Issues

ZF-12435: Passing an exception as the message to Zend_Log_Writer_Db raises an exception

Description

The other log writers allow you to pass an instance of an exception as the message for logging. In particular, Zend_Log_Writer_Firebug even has a formatter that renders in a way that is easy to read when debugging XmlHttpRequests. However, I get an exception in Zend_Log_Writer_Db because the message is an object:

{panel:title=Exception} exception 'Zend_Db_Statement_Sqlsrv_Exception' with message 'An invalid PHP type for parameter 4 was specified.' in ..\library\Zend\Db\Statement\Sqlsrv.php:206 Stack trace:

0 ..\library\Zend\Db\Statement.php(320): Zend_Db_Statement_Sqlsrv->_execute(Array)

1 ..\library\Zend\Db\Adapter\Abstract.php(479): Zend_Db_Statement->execute(Array)

2 ..\library\Zend\Db\Adapter\Sqlsrv.php(380): Zend_Db_Adapter_Abstract->query('INSERT INTO "Ap...', Array)

3 ..\library\Zend\Log\Writer\Db.php(143): Zend_Db_Adapter_Sqlsrv->insert('ApplicationLog', Array)

4 ..\library\Zend\Log\Writer\Abstract.php(85): Zend_Log_Writer_Db->_write(Array)

5 ..\library\Zend\Log.php(428): Zend_Log_Writer_Abstract->write(Array)

6 [internal function]: Zend_Log->log(Object(Exception), 3)

7 ..\library\My\Controller\Plugin\Log.php(212): call_user_func_array(Array, Array)

8 [internal function]: My_Controller_Plugin_Log->__call('log', Array)

9 [internal function]: My_Controller_Plugin_Log->log(Object(Exception), 3)

10 ..\library\My\Controller\Action\Helper\Log.php(27): call_user_func_array(Array, Array)

11 ..\application\controllers\ErrorController.php(52): My_Controller_Action_Helper_Log->__call('log', Array)

12 ..\application\controllers\ErrorController.php(52): My_Controller_Action_Helper_Log->log(Object(Exception), 3)

13 ..\library\Zend\Controller\Action.php(516): ErrorController->errorAction()

14 ..\library\Zend\Controller\Dispatcher\Standard.php(295): Zend_Controller_Action->dispatch('errorAction')

15 ..\library\Zend\Controller\Front.php(954): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))

16 ..\library\Zend\Application\Bootstrap\Bootstrap.php(97): Zend_Controller_Front->dispatch()

17 ..\library\Zend\Application.php(366): Zend_Application_Bootstrap_Bootstrap->run()

18 ..\public\index.php(126): Zend_Application->run()

19 {main}

{panel}

I can pass the exception in as a string, but then it is more difficult to read in Firebug. I'm also not sure if this problem is specific to the database adapter I am using (Zend_Db_Adapter_Sqlsrv) since I don't have another database/adapter I can test right now.

I think a change in Zend_Log_Writer_Db::_write() should fix it but I don't know if it might raise other issues:


    /**
     * Write a message to the log.
     *
     * @param  array  $event  event data
     * @return void
     * @throws Zend_Log_Exception
     */
    protected function _write($event)
    {
        if ($this->_db === null) {
            // require_once 'Zend/Log/Exception.php';
            throw new Zend_Log_Exception('Database adapter is null');
        }

        if ($this->_columnMap === null) {
            $dataToInsert = $event;
        } else {
            $dataToInsert = array();
            foreach ($this->_columnMap as $columnName => $fieldKey) {
-                $dataToInsert[$columnName] = $event[$fieldKey];

+               if ('info' == $fieldKey && (is_object($event[$fieldKey]) || is_array($event[$fieldKey]))) {
+                   $dataToInsert[$columnName] = Zend_Debug::dump($event[$fieldKey], gettype($event[$fieldKey]), true);
+               } else {
+                   $dataToInsert[$columnName] = $event[$fieldKey];
+               }
            }
        }

        $this->_db->insert($this->_table, $dataToInsert);
    }

I tried to resolve this by extending Zend_Log_Writer_Db to override the _write method, but all of the properties are marked private instead of protected for some reason. As a result, getting it to work would have required duplicating the entire class. (If they truly need to be private, perhaps the class should be final?)

Comments

Fixed code block.

Hi Andrew, I will look into your problem with exceptions. I must write a unit test.

{quote} tried to resolve this by extending Zend_Log_Writer_Db to override the _write method, but all of the properties are marked private instead of protected for some reason.{quote} This problem was fixed with ZF-12514. (Look at the diff)

Thanks for your help! :)

{quote} bq. tried to resolve this by extending Zend_Log_Writer_Db to override the _write method, but all of the properties are marked private instead of protected for some reason.

This problem was fixed with ZF-12514.{quote}

Thank you!

I was ultimately able to extend {{Zend_Log_Writer_Db}} in a way that works with the current version. I'm still not sure if this is the best way of handling things, but this is the class I came up with:


/**
 * @category   My
 * @package    My_Log
 * @subpackage Writer
 */
class My_Log_Writer_Db extends Zend_Log_Writer_Db
{

    /**
     * Relates database columns names to log data field keys.
     * 
     * THIS HAD TO BE COPIED TO THE SUBCLASS BECAUSE IT WAS DECLARED PRIVATE IN
     * THE PARENT CLASS AND I NEED IT IN THE OVERRIDDEN _write METHOD BELOW. 
     * 
     * @var null|array
     */
    private $_columnMap;
    
    /**
     * Class constructor
     * 
     * THIS HAD TO BE INCLUDED BECAUSE $db, $table AND $columnMap ALL  MAP TO 
     * PRIVATE PROPERTIES IN THE PARENT CLASS AND COULD NOT BE SET HERE.
     *
     * @param Zend_Db_Adapter $db   Database adapter instance
     * @param string $table         Log table in database
     * @param array $columnMap
     * @return void
     */
    public function __construct($db, $table, $columnMap = null)
    {
        
        // I NEED $columnMap IN THE OVERRIDDEN VERSION OF _write BELOW.
        $this->_columnMap = $columnMap;
        
        // PASS ALL THE PARAMETERS TO THE PARENT CLASS SO THEY CAN BE SET 
        // CORRECTLY WHERE _write IN THE PARENT CLASS CAN ACCESS THEM.
        parent::__construct($db, $table, $columnMap);
    }
    

    /**
     * Write a message to the log.
     * OVERWRITTEN: Safely converts any values in the $event to strings 
     *
     * @param  array  $event  event data
     * @return void
     * @throws Zend_Log_Exception
     */
    protected function _write($event)
    {

        foreach ($this->_columnMap as $columnName => $fieldKey) {
            
            $var = $event[$fieldKey];
                
            switch (true) {
                
                case ('message' == $fieldKey && ($var instanceof Exception)):
                    // Special handling if an exception was passed to the
                    // message.
                    
                    // Store only the exception message in 'message'.
                    
                    // If no 'info' key exists in the event, move the exception
                    // object (cast as a string) to that key.
                    
                    if (!array_key_exists('info', $event)) {
                        $event['info'] = (string) $var; 
                    }
                    $var = $var->getMessage();
                    break; 
                
                case (is_object($var) && method_exists($var, '__toString()')):
                    // The object can be typecast to a string.
                    // Fall through.
                case is_bool($var):
                case is_numeric($var):
                    $var = (string) $var;
                    break;
                    
                    
                case ($var instanceof DateTime):
                    // DateTime instances cannot be cast to a string.
                    // Format the date as an ISO 8601 string.
                    $var = $var->format('c');
                    break;

                case is_resource($var):
                case is_array($var):
                case is_object($var):
                    // The variable cannot be typecast to a string.
                    $var = Zend_Debug::dump($var, null, true);
                    break;

                case is_string($var):
                    // No action needed; included for completeness.
                    break;
                    
                default:
                    // Not sure what other types might need to be handled.

            }
                    
            $event[$fieldKey] = $var;
        }

        parent::_write($event);
    }
}

Hopefully this is useful to someone else.

This issue has been closed on Jira and moved to GitHub for issue tracking. To continue following the resolution of this issues, please visit: https://github.com/zendframework/zf1/issues/23