Issues

ZF-3698: Error adding rows when overriding insert() method of extended Table Abstract class

Description

Greetings to the Hard Working Team!

I've unearthed a vexing bug in 1.5.2 that occurs when one extends Zend/Db/Table/Abstract and overrides the insert() function. Upon save, the following error is provided (note that this line number is slightly off due to my having inserted various debugging prints tracking this one down): {{Zend_Db_Table_Row_Exception: Cannot refresh row as parent is missing in . . . /library/Zend/Db/Table/Row/Abstract.php on line 725}}

I've marked this as "Major" because all forward processing is halted by the error, because clicking refresh offers to resubmit the form (and doing so causes a key collision), and in order to "get around it" one must either go back a few pages on the browser or reload the URL from scratch. OH - or one could simply not extend the Table Abstract class... but that's just silly talk.

NOTE: This bug ONLY happens when the insert() function is implemented in the extended abstract class; If the function is NOT implemented but the class is extended, no problems occur.

It turns out the the insert actually does succeed, but the row refresh fails due to the Zend/Db/Table/Row/Abstract.php::_doInsert() function being unable to detect the newly entered primary key value. Unfortunately, I'm pressed for time at the moment and did not track down the actual cause of this bug, but I did implement the following fix which seems to fix the problem:


. . .
/**
 * @category   Zend
 * @package    Zend_Db
 * @subpackage Table
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Db_Table_Row_Abstract
{
. . .
    /**
     * @return mixed The primary key value(s), as an associative array if the
     *     key is compound, or a scalar if the key is single-column.
     */
    protected function _doInsert()
    {
        /**
         * A read-only row cannot be saved.
         */
        if ($this->_readOnly === true) {
            require_once 'Zend/Db/Table/Row/Exception.php';
            throw new Zend_Db_Table_Row_Exception('This row has been marked read-only');
        }

. . .

        /**
         * Normalize the result to an array indexed by primary key column(s).
         * The table insert() method may return a scalar.
         */
        if (is_array($primaryKey)) {
            $newPrimaryKey = $primaryKey;
        } else {
            /* ORIGINAL CODE: 
             * $newPrimaryKey = array(current((array) $this->_primary) => $primaryKey);
            */

            /* NEW CODE: */
            $primaryKeyFieldName = current((array) $this->_primary);
            $primaryKey = $primaryKey ? $primaryKey : $data[$primaryKeyFieldName];
            $newPrimaryKey = array($primaryKeyFieldName => $primaryKey);
        }

        /**
         * Save the new primary key value in _data.  The primary key may have
         * been generated by a sequence or auto-increment mechanism, and this
         * merge should be done before the _postInsert() method is run, so the
         * new values are available for logging, etc.
         */

. . .

        return $primaryKey;
    }
. . .
}

For reference, here is my extended table class:


  <?php
  require_once 'Zend/Db/Table/Abstract.php';
  
  abstract class Mns_Db_Table_Abstract extends Zend_Db_Table_Abstract
  {
    protected function _setupTableName()
    {
      // if no name is provided then convert class name from camel case to underscore breaks. e.g. ThisClassName become this_class_name.
      if (!$this->_name) {
        $this->_name = trim(strtolower(preg_replace('/([A-Z])/', '_$1', get_class($this))), '_');
      }
      parent::_setupTableName();
    }
  
  
    public function insert(array $data)
    {
      if (empty($data['created_by'])) {$data['created_by'] = 'getcurrentuser';}
      if (empty($data['created_on'])) {$data['created_on'] = new Zend_Db_Expr('NOW()');}
      parent::insert($data);
    }
  
    public function update(array $data, $where)
    {
      if (empty($data['altered_by'])) {$data['altered_by'] = 'getcurrentuser';}
      if (empty($data['altered_on'])) {$data['altered_on'] = new Zend_Db_Expr('NOW()');}
      parent::update($data, $where);
    }
  
  }

And finally, my bootstrap - just in case it makes a difference in reproducing this error.


<?php

/* Report all errors directly to the screen for simple diagnostics in the dev environment */
error_reporting(E_ALL | E_STRICT);
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
date_default_timezone_set('America/Detroit');

/* Add the Zend Framework library to the include path so that we can access the ZF classes */
set_include_path(
  '.'
  . PATH_SEPARATOR . '/usr/share/php/libzend-framework-php/'
  . PATH_SEPARATOR . '../library'               // Zend Framework
  //. PATH_SEPARATOR . '/usr/share/php/PEAR'      //included here for using Auth; todo: switch app to use Zend_Auth instead?
  . PATH_SEPARATOR . '../application/models/db'
  . PATH_SEPARATOR . '../application/models/form'
  . PATH_SEPARATOR . get_include_path()
);

/* Set up autoload so we don't have to explicitly require each Zend Framework class */
require_once "Zend/Loader.php";
Zend_Loader::registerAutoload();

// load configuration
$config = new Zend_Config_Ini('../application/config.ini', 'general');
$registry = Zend_Registry::getInstance();
$registry->set('config', $config);

// setup database
$db = Zend_Db::factory($config->db);
Zend_Db_Table::setDefaultAdapter($db);
Zend_Registry::set('arrayKeyHashFormat', '%s__%s');

/* Set the singleton instance of the front controller */
$frontController = Zend_Controller_Front::getInstance();

$acl = new Zend_Acl();

// todo: cache the result of getControllerDirs
$frontController->setControllerDirectory(getControllerDirs('../application/controllers'));

/* Disable error handler so it doesn't intercept all those errors we enabled above */
$frontController->throwExceptions(true);

// Initialize Zend_Layout's MVC helpers
Zend_Layout::startMvc(array('layoutPath'=>'../application/layouts'));

/* OK, do your stuff, front controller */
$frontController->dispatch();



/**
 * This next bit iterates through the controllers directory and adds each of
 * the "modules" into the Front Controller. Note that this depends on the the
 * "default" directory being named "default" since this is a special keyword
 * for the controller indicating the action to use when no module is specified.
 */
function getControllerDirs($basedir, $_level = 0) {
  $_level++;
  $controllerDirs = array();
  $dir = new DirectoryIterator($basedir);
  foreach ($dir as $file) {
    if ($file->isDot() || !$file->isDir()) {continue;}

    $module = $file->getFilename();

    // Don't use SCCS directories as modules
    if (preg_match('/^[^a-z]/i', $module) || ('CVS' == $module)) {continue;}

    $moduleDir = $basedir.'/'.$module;

    $controllerDirs[$module] = $moduleDir;

    if ($_level == 1) {$controllerDirs = array_merge($controllerDirs, getControllerDirs($moduleDir, $_level));}
  }
  return $controllerDirs;
}

Comments

Greetings,

Just to keep everyone in the loop: I'm now used 1.5.3 and the problem still exists. I do see that the projection is for this to be completed in the NEXT release, but I figured this information might help. I realized I was SHOULD have been extending Zend_Db_Table rather than Zend_Db_Table_Abstract, but there is nothing significant going on in Zend_Db_Table so my code was unaffected by this error on my part, though I have corrected this oversight in my code. As you might expect, this has no effect on the relieving the error.

Sean P. O. MacCath-Moran www.emanaton.com

Greetings,

I've upgraded to version 1.6 and continue to see this problem. Are there any plans to troubleshoot this?

Sean P. O. MacCath-Moran www.emanaton.com

Greetings,

I've just tested this in 1.6.1, and it appears to have been fixed! Woot! Thanks!

Sean P. O. MacCath-Moran www.emanaton.com

Greetings,

ACK! Belay my last; I had forgotten that I'd overwritten the method in an extended Row class. This problem IS NOT FIXED yet.

I keep seeing "Next Mini Release" being listed as the anticipated fix, but then am not seeing the fix on that release. Am I misunderstanding something on this?

Sean P. O. MacCath-Moran www.emanaton.com

I received the following email and upon testing discovered Tim to be quite correct; I was neglecting to RETURN the call to parent while over-writing the insert and update functions. THANK YOU TIM!!!

Needless to say, this issue is now defunct as the error was mine, not ZF. =o/

Sean P. O. MacCath-Moran www.emanaton.com

=======================================================================================================================

Sean,

   Saw your bug report when I had the same issue extending Zend_Db_Table_Abstract.

   I ran into the same issue, then realized I was boneheaded.

   instead of:

   parent::insert($data);

   try:

   return parent::insert($data);

   Works for me with 1.6, anyway.

   I'd have added this to the bug report, but I can't figure out how to create a comment.

Regards,

-Tim

Tim Lieberman Electric Mind Control Industries

This "bug" turned out to be one in my own code, NOT one with ZF.

Was notifed by Sean off-list that this was not an issue in ZF, but a local issue in his project (use-case).

-Ralph

Changing issues in preparation for the 1.7.0 release.

Sorry, if this ticket is not exactly where it should be posted, but the error message I got is the same so maybe it will help someone.

ZF 1.10 Zend/Db/Table/Row/Abstract.php:752 _refresh() throws exception in question if the length of the data you are trying to save is more then DB table field can have. So when _refresh() search this data back it cannot find it because DB silently truncated it.