Passing an exception as the message to Zend_Log_Writer_Db raises an exception #23

Closed
zfbot opened this Issue Apr 5, 2013 · 6 comments

3 participants

@zfbot

Jira Information

Original Issue: ZF-12435
Issue Type: Bug
Reporter: Andrew Ballard
Created: 10/04/12
Assignee: froschdesign
Components: Zend_Log

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:

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\}

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?)

@zfbot

(Originally posted by: Andrew Ballard on 10/04/12)

Fixed code block.

@zfbot

(Originally posted by: froschdesign on 02/15/13)

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|http://framework.zend.com/code/diff.php?repname=Zend+Framework&path=%2Fbranches%2Frelease-1.12%2Flibrary%2FZend%2FLog%2FWriter%2FDb.php&rev=25247&peg=25247])

Thanks for your help! :)

@zfbot

(Originally posted by: Andrew Ballard on 02/15/13)

{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.

@zfbot

This issue was ported from the ZF2 Jira Issue Tracker at
http://framework.zend.com/issues/browse/ZF-12435

Known GitHub users mentioned in the original message or comment:
@froschdesign

@fusioned

I've recently had to extend the DB writer _write() method to fix issues with omitted event items. However as this touches on the private _columnMap, this has meant every method which uses this property needed to be overridden.

If the intention was to encourage downstream DB writer classes to be a direct offshoot of the abstract class with entire methods copied from Zend_Log_Writer_Db, _columnMap being private is doing the job.

@froschdesign
Zend Framework member

I think, it was fixed with 2882bc7 and released with 1.12.5.

If I've missed something, please feel free to reopen this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment