Index: library/Zend/Db/Table/Abstract.php
===================================================================
--- library/Zend/Db/Table/Abstract.php (revision 24822)
+++ library/Zend/Db/Table/Abstract.php (working copy)
@@ -70,6 +70,7 @@
const ON_UPDATE = 'onUpdate';
const CASCADE = 'cascade';
+ const CASCADE_RECURSE = 'cascadeRecurse';
const RESTRICT = 'restrict';
const SET_NULL = 'setNull';
@@ -1193,27 +1194,56 @@
*/
public function _cascadeDelete($parentTableClassname, array $primaryKey)
{
+ // setup metadata
$this->_setupMetadata();
+
+ // get this class name
+ $thisClass = get_class($this);
+ if ($thisClass === 'Zend_Db_Table') {
+ $thisClass = $this->_definitionConfigName;
+ }
+
$rowsAffected = 0;
+
foreach ($this->_getReferenceMapNormalized() as $map) {
if ($map[self::REF_TABLE_CLASS] == $parentTableClassname && isset($map[self::ON_DELETE])) {
- switch ($map[self::ON_DELETE]) {
- case self::CASCADE:
- $where = array();
- for ($i = 0; $i < count($map[self::COLUMNS]); ++$i) {
- $col = $this->_db->foldCase($map[self::COLUMNS][$i]);
- $refCol = $this->_db->foldCase($map[self::REF_COLUMNS][$i]);
- $type = $this->_metadata[$col]['DATA_TYPE'];
- $where[] = $this->_db->quoteInto(
- $this->_db->quoteIdentifier($col, true) . ' = ?',
- $primaryKey[$refCol], $type);
+
+ $where = array();
+
+ // CASCADE or CASCADE_RECURSE
+ if (in_array($map[self::ON_DELETE], array(self::CASCADE, self::CASCADE_RECURSE))) {
+ for ($i = 0; $i < count($map[self::COLUMNS]); ++$i) {
+ $col = $this->_db->foldCase($map[self::COLUMNS][$i]);
+ $refCol = $this->_db->foldCase($map[self::REF_COLUMNS][$i]);
+ $type = $this->_metadata[$col]['DATA_TYPE'];
+ $where[] = $this->_db->quoteInto(
+ $this->_db->quoteIdentifier($col, true) . ' = ?',
+ $primaryKey[$refCol], $type);
+ }
+ }
+
+ // CASCADE_RECURSE
+ if ($map[self::ON_DELETE] == self::CASCADE_RECURSE) {
+
+ /**
+ * Execute cascading deletes against dependent tables
+ */
+ $depTables = $this->getDependentTables();
+ if (!empty($depTables)) {
+ foreach ($depTables as $tableClass) {
+ $t = self::getTableFromString($tableClass, $this);
+ foreach ($this->fetchAll($where) as $depRow) {
+ $rowsAffected += $t->_cascadeDelete($thisClass, $depRow->getPrimaryKey());
+ }
}
- $rowsAffected += $this->delete($where);
- break;
- default:
- // no action
- break;
+ }
}
+
+ // CASCADE or CASCADE_RECURSE
+ if (in_array($map[self::ON_DELETE], array(self::CASCADE, self::CASCADE_RECURSE))) {
+ $rowsAffected += $this->delete($where);
+ }
+
}
}
return $rowsAffected;
@@ -1531,4 +1561,38 @@
return $data;
}
+ public static function getTableFromString($tableName, Zend_Db_Table_Abstract $referenceTable = null)
+ {
+ if ($referenceTable instanceof Zend_Db_Table_Abstract) {
+ $tableDefinition = $referenceTable->getDefinition();
+
+ if ($tableDefinition !== null && $tableDefinition->hasTableConfig($tableName)) {
+ return new Zend_Db_Table($tableName, $tableDefinition);
+ }
+ }
+
+ // assume the tableName is the class name
+ if (!class_exists($tableName)) {
+ try {
+ require_once 'Zend/Loader.php';
+ Zend_Loader::loadClass($tableName);
+ } catch (Zend_Exception $e) {
+ require_once 'Zend/Db/Table/Row/Exception.php';
+ throw new Zend_Db_Table_Row_Exception($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ $options = array();
+
+ if ($referenceTable instanceof Zend_Db_Table_Abstract) {
+ $options['db'] = $referenceTable->getAdapter();
+ }
+
+ if (isset($tableDefinition) && $tableDefinition !== null) {
+ $options[Zend_Db_Table_Abstract::DEFINITION] = $tableDefinition;
+ }
+
+ return new $tableName($options);
+ }
+
}
Index: library/Zend/Db/Table/Row/Abstract.php
===================================================================
--- library/Zend/Db/Table/Row/Abstract.php (revision 24822)
+++ library/Zend/Db/Table/Row/Abstract.php (working copy)
@@ -725,6 +725,17 @@
}
/**
+ * Retrieves an associative array of primary keys.
+ *
+ * @param bool $useDirty
+ * @return array
+ */
+ public function getPrimaryKey($useDirty = true)
+ {
+ return $this->_getPrimaryKey($useDirty);
+ }
+
+ /**
* Constructs where statement for retrieving row(s).
*
* @param bool $useDirty
@@ -1167,37 +1178,7 @@
*/
protected function _getTableFromString($tableName)
{
-
- if ($this->_table instanceof Zend_Db_Table_Abstract) {
- $tableDefinition = $this->_table->getDefinition();
-
- if ($tableDefinition !== null && $tableDefinition->hasTableConfig($tableName)) {
- return new Zend_Db_Table($tableName, $tableDefinition);
- }
- }
-
- // assume the tableName is the class name
- if (!class_exists($tableName)) {
- try {
- require_once 'Zend/Loader.php';
- Zend_Loader::loadClass($tableName);
- } catch (Zend_Exception $e) {
- require_once 'Zend/Db/Table/Row/Exception.php';
- throw new Zend_Db_Table_Row_Exception($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- $options = array();
-
- if (($table = $this->_getTable())) {
- $options['db'] = $table->getAdapter();
- }
-
- if (isset($tableDefinition) && $tableDefinition !== null) {
- $options[Zend_Db_Table_Abstract::DEFINITION] = $tableDefinition;
- }
-
- return new $tableName($options);
+ return Zend_Db_Table_Abstract::getTableFromString($tableName, $this->_table);
}
}
Index: tests/Zend/Db/Table/TestCommon.php
===================================================================
--- tests/Zend/Db/Table/TestCommon.php (revision 24822)
+++ tests/Zend/Db/Table/TestCommon.php (working copy)
@@ -1613,6 +1613,39 @@
$this->assertEquals(0, count($rows));
}
+ /**
+ * @group ZF-1103
+ */
+ public function testTableCascadeRecurseDelete()
+ {
+ $tblRecursive = $this->_getTable('My_ZendDbTable_TableCascadeRecursive');
+
+ // Enforce initial table structure
+ $parentRow = $tblRecursive->find(1)->current();
+ $this->assertType('Zend_Db_Table_Row', $parentRow);
+ $childRows = $parentRow->findDependentRowset('My_ZendDbTable_TableCascadeRecursive', 'Children');
+ $this->assertType('Zend_Db_Table_Rowset', $childRows);
+ $this->assertEquals(2, count($childRows));
+ foreach ( $childRows as $childRow ) {
+ $this->assertType('Zend_Db_Table_Row', $childRow);
+ $subChildRows = $childRow->findDependentRowset('My_ZendDbTable_TableCascadeRecursive', 'Children');
+ $this->assertType('Zend_Db_Table_Rowset', $subChildRows);
+ $this->assertEquals( $childRow['item_id'] == 3 ? 2 : 0 , count($subChildRows));
+ }
+
+ // Perform the delete
+ $parentRow->delete();
+
+ // Assert that all children of #1 (2,3,4,5) are removed recursively
+ $this->assertNull($tblRecursive->find(1)->current());
+ $this->assertNull($tblRecursive->find(2)->current());
+ $this->assertNull($tblRecursive->find(3)->current());
+ $this->assertNull($tblRecursive->find(4)->current());
+ $this->assertNull($tblRecursive->find(5)->current());
+ //... but #6 remains
+ $this->assertType('Zend_Db_Table_Row', $tblRecursive->find(6)->current());
+ }
+
public function testSerialiseTable()
{
$table = $this->_table['products'];
@@ -1793,3 +1826,4 @@
Zend_Db_Table_Abstract::setDefaultMetadataCache(null);
}
}
+
Index: tests/Zend/Db/TestUtil/Common.php
===================================================================
--- tests/Zend/Db/TestUtil/Common.php (revision 24822)
+++ tests/Zend/Db/TestUtil/Common.php (working copy)
@@ -217,7 +217,8 @@
'noprimarykey' => 'zfnoprimarykey',
'Documents' => 'zfdocuments',
'Price' => 'zfprice',
- 'AltBugsProducts' => 'zfalt_bugs_products'
+ 'AltBugsProducts' => 'zfalt_bugs_products',
+ 'CascadeRecursive' => 'zfalt_cascade_recursive'
);
public function getTableName($tableId)
@@ -291,6 +292,16 @@
);
}
+ protected function _getColumnsCascadeRecursive()
+ {
+ return array(
+ 'item_id' => 'INTEGER NOT NULL',
+ 'item_parent' => 'INTEGER NULL',
+ 'item_data' => 'VARCHAR(100)',
+ 'PRIMARY KEY' => 'item_id'
+ );
+ }
+
protected function _getDataAccounts()
{
return array(
@@ -407,6 +418,18 @@
);
}
+ protected function _getDataCascadeRecursive()
+ {
+ return array(
+ array('item_id' => '1', 'item_parent' => NULL, 'item_data' => '1'),
+ array('item_id' => '2', 'item_parent' => '1', 'item_data' => '1.2'),
+ array('item_id' => '3', 'item_parent' => '1', 'item_data' => '1.3'),
+ array('item_id' => '4', 'item_parent' => '3', 'item_data' => '1.3.4'),
+ array('item_id' => '5', 'item_parent' => '3', 'item_data' => '1.3.5'),
+ array('item_id' => '6', 'item_parent' => NULL, 'item_data' => '6')
+ );
+ }
+
public function populateTable($tableId)
{
$tableName = $this->getTableName($tableId);
@@ -487,6 +510,9 @@
$this->createTable('Price');
$this->populateTable('Price');
+ $this->createTable('CascadeRecursive');
+ $this->populateTable('CascadeRecursive');
+
$this->createView();
}
Index: tests/Zend/Db/Table/_files/My/ZendDbTable/TableCascadeRecursive.php
===================================================================
--- tests/Zend/Db/Table/_files/My/ZendDbTable/TableCascadeRecursive.php (revision 0)
+++ tests/Zend/Db/Table/_files/My/ZendDbTable/TableCascadeRecursive.php (revision 0)
@@ -0,0 +1,53 @@
+ array(
+ 'columns' => array('item_parent'),
+ 'refTableClass' => 'My_ZendDbTable_TableCascadeRecursive',
+ 'refColumns' => array('item_id'),
+ 'onDelete' => self::CASCADE_RECURSE
+ )
+ );
+
+}
Index: documentation/manual/en/module_specs/Zend_Db_Table-Relationships.xml
===================================================================
--- documentation/manual/en/module_specs/Zend_Db_Table-Relationships.xml (revision 24822)
+++ documentation/manual/en/module_specs/Zend_Db_Table-Relationships.xml (working copy)
@@ -752,12 +752,36 @@
To declare a cascading relationship in the Zend_Db_Table, edit
the rules in the $_referenceMap. Set the associative array keys
- 'onDelete' and 'onUpdate' to the string 'cascade'
- (or the constant self::CASCADE). Before a row is deleted from the
- parent table, or its primary key values updated, any rows in the dependent table that
- refer to the parent's row are deleted or updated first.
+ 'onDelete' and 'onUpdate' to one of these options:
+
+
+
+
+ Cascade: This option configures a single-level cascade (parent table plus all
+ directly-dependent tables). To enable this option set the appropriate key in
+ $_referenceMap to string 'cascade' or use the constant
+ self::CASCADE.
+
+
+
+
+
+ Recursive Cascade: This option configures a full recursive cascade starting
+ with the parent table. To enable this option set the appropriate key in
+ $_referenceMap to string 'cascadeRecurse' or use the constant
+ self::CASCADE_RECURSE.
+
+
+
+
+
+
+ Before a row is deleted from the parent table, or its primary key values updated, any
+ rows in the dependent table that refer to the parent's row are deleted or updated first.
+
+
Example Declaration of Cascading Operations