Skip to content
Permalink
Browse files

Add cascadeCallback option to associations.

This option allows control over how associations are deleted. Allowing
developers to choose the more efficient deleteAll() or the slower load
+ delete option which also includes callbacks. Using the slower mode can
be helpful when your application relies on life-cycle callbacks.
  • Loading branch information...
markstory committed Nov 3, 2013
1 parent 34e4247 commit 2b99452dbce0a06c623b4aa2e8d113c7e2adfc12
@@ -94,6 +94,13 @@ abstract class Association {
*/
protected $_dependent = false;
/**
* Whether or not cascaded deletes should also fire callbacks.
*
* @var string
*/
protected $_cascadeCallbacks = false;
/**
* Source table instance
*
@@ -145,6 +152,7 @@ public function __construct($name, array $options = []) {
'foreignKey',
'conditions',
'dependent',
'cascadeCallbacks',
'sourceTable',
'targetTable',
'joinType',
@@ -235,7 +235,13 @@ public function cascadeDelete(Entity $entity, $options = []) {
$conditions = array_merge($conditions, $this->conditions());
$table = $this->pivot();
return $table->deleteAll($conditions);
if ($this->_cascadeCallbacks) {
foreach ($table->find('all')->where($conditions) as $related) {
$table->delete($related, $options);
}
} else {
return $table->deleteAll($conditions);
}
}
/**
@@ -48,9 +48,12 @@ public function cascadeDelete(Entity $entity, $options = []) {
// TODO fix multi-column primary keys.
$conditions = array_merge($conditions, $this->conditions());
$query = $table->find('all')->where($conditions);
foreach ($query as $related) {
$table->delete($related, $options);
if ($this->_cascadeCallbacks) {
foreach ($table->find('all')->where($conditions) as $related) {
$table->delete($related, $options);
}
} else {
$table->deleteAll($conditions);
}
return true;
}
@@ -461,7 +461,12 @@ public function belongsTo($associated, array $options = []) {
* - foreignKey: The name of the field to use as foreign key, if false none
* will be used
* - dependent: Set to true if you want CakePHP to cascade deletes to the
* associated table when an entity is removed on this table.
* associated table when an entity is removed on this table. Set to false
* if you don't want CakePHP to remove associated data, for when you are using
* database constraints.
* - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
* cascaded deletes. If false the ORM will use deleteAll() to remove data.
* When true records will be loaded and then deleted.
* - conditions: array with a list of conditions to filter the join with
* - joinType: The type of join to be used (e.g. LEFT)
*
@@ -493,7 +498,12 @@ public function hasOne($associated, array $options = []) {
* - foreignKey: The name of the field to use as foreign key, if false none
* will be used
* - dependent: Set to true if you want CakePHP to cascade deletes to the
* associated table when an entity is removed on this table.
* associated table when an entity is removed on this table. Set to false
* if you don't want CakePHP to remove associated data, for when you are using
* database constraints.
* - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
* cascaded deletes. If false the ORM will use deleteAll() to remove data.
* When true records will be loaded and then deleted.
* - conditions: array with a list of conditions to filter the join with
* - sort: The order in which results for this association should be returned
* - strategy: The strategy to be used for selecting results Either 'select'
@@ -531,6 +541,9 @@ public function hasMany($associated, array $options = []) {
* - through: If you choose to use an already instantiated link table, set this
* key to a configured Table instance containing associations to both the source
* and target tables in this association.
* - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
* cascaded deletes. If false the ORM will use deleteAll() to remove data.
* When true pivot table records will be loaded and then deleted.
* - conditions: array with a list of conditions to filter the join with
* - sort: The order in which results for this association should be returned
* - strategy: The strategy to be used for selecting results Either 'select'
@@ -908,16 +921,14 @@ protected function _update($entity, $data) {
* Delete a single entity.
*
* Deletes an entity and possibly related associations from the database
* based on the 'cascade' option. When true, the cascade option will cause
* any associations marked as dependent to be removed. Any
* rows in a BelongsToMany join table will be removed as well.
* based on the 'dependent' option used when defining the association.
* For HasMany and HasOne associations records will be removed based on
* the dependent option. Join table records in BelongsToMany associations
* will always be removed. You can use the `cascadeCallbacks` option
* when defining associations to change how associated data is deleted.
*
* ## Options
*
* - `cascade` Defaults to true. Set to false to disable cascaded deletes.
* Use this when you don't want to cascade or when your foreign keys
* will handle the cascading delete for you. Cascaded deletes
* will occur inside the transaction when atomic is true.
* - `atomic` Defaults to true. When true the deletion happens within a transaction.
*
* ## Events
@@ -936,7 +947,7 @@ protected function _update($entity, $data) {
* @return boolean success
*/
public function delete(Entity $entity, array $options = []) {
$options = new \ArrayObject($options + ['atomic' => true, 'cascade' => true]);
$options = new \ArrayObject($options + ['atomic' => true]);
$process = function() use ($entity, $options) {
return $this->_processDelete($entity, $options);
@@ -954,9 +965,6 @@ public function delete(Entity $entity, array $options = []) {
* Will delete the entity provided. Will remove rows from any
* dependent associations, and clear out join tables for BelongsToMany associations.
*
* Setting $options['cascade'] = false will prevent associated data including
* join tables from being cleared.
*
* @param Entity $entity The entity to delete.
* @param ArrayObject $options The options for the delete.
* @return boolean success
@@ -984,8 +992,7 @@ protected function _processDelete($entity, $options) {
->executeStatement();
$success = $statement->rowCount() > 0;
if (!$success || !$options['cascade']) {
if (!$success) {
return $success;
}
@@ -570,4 +570,55 @@ public function testCascadeDelete() {
$association->cascadeDelete($entity);
}
/**
* Test cascading deletes with callbacks.
*
* @return void
*/
public function testCascadeDeleteWithCallbacks() {
$articleTag = $this->getMock('Cake\ORM\Table', ['find', 'delete'], [], '', false);
$config = [
'sourceTable' => $this->article,
'targetTable' => $this->tag,
'conditions' => ['Tag.name' => 'foo'],
'cascadeCallbacks' => true,
];
$association = new BelongsToMany('Tag', $config);
$association->pivot($articleTag);
$articleTagOne = new Entity(['article_id' => 1, 'tag_id' => 2]);
$articleTagTwo = new Entity(['article_id' => 1, 'tag_id' => 4]);
$iterator = new \ArrayIterator([
$articleTagOne,
$articleTagTwo
]);
$query = $this->getMock('\Cake\ORM\Query', [], [], '', false);
$query->expects($this->once())
->method('where')
->with(['Tag.name' => 'foo', 'article_id' => 1])
->will($this->returnSelf());
$query->expects($this->any())
->method('getIterator')
->will($this->returnValue($iterator));
$articleTag->expects($this->once())
->method('find')
->will($this->returnValue($query));
$articleTag->expects($this->at(1))
->method('delete')
->with($articleTagOne, []);
$articleTag->expects($this->at(2))
->method('delete')
->with($articleTagTwo, []);
$articleTag->expects($this->never())
->method('deleteAll');
$entity = new Entity(['id' => 1, 'name' => 'PHP']);
$association->cascadeDelete($entity);
}
}
@@ -42,7 +42,7 @@ public function setUp() {
]
]);
$this->article = $this->getMock(
'Cake\ORM\Table', ['find', 'delete'], [['alias' => 'Article', 'table' => 'articles']]
'Cake\ORM\Table', ['find', 'deleteAll', 'delete'], [['alias' => 'Article', 'table' => 'articles']]
);
$this->article->schema([
'id' => ['type' => 'integer'],
@@ -434,7 +434,7 @@ public function testAttachToNoFields() {
}
/**
* Test cascading delete with has many.
* Test cascading deletes.
*
* @return void
*/
@@ -443,7 +443,33 @@ public function testCascadeDelete() {
'dependent' => true,
'sourceTable' => $this->author,
'targetTable' => $this->article,
'conditions' => ['Article.is_active' => true]
'conditions' => ['Article.is_active' => true],
];
$association = new HasMany('Article', $config);
$this->article->expects($this->once())
->method('deleteAll')
->with([
'Article.is_active' => true,
'author_id' => 1
]);
$entity = new Entity(['id' => 1, 'name' => 'PHP']);
$association->cascadeDelete($entity);
}
/**
* Test cascading delete with has many.
*
* @return void
*/
public function testCascadeDeleteCallbacks() {
$config = [
'dependent' => true,
'sourceTable' => $this->author,
'targetTable' => $this->article,
'conditions' => ['Article.is_active' => true],
'cascadeCallbacks' => true,
];
$association = new HasMany('Article', $config);
@@ -1603,20 +1603,20 @@ public function testDeleteDependent() {
}
/**
* Test delete with dependent records and cascade = false
* Test delete with dependent = false does not cascade.
*
* @return void
*/
public function testDeleteDependentCascadeFalse() {
public function testDeleteNoDependentNoCascade() {
$table = TableRegistry::get('author');
$table->hasOne('article', [
$table->hasMany('article', [
'foreignKey' => 'author_id',
'dependent' => true,
'dependent' => false,
]);
$query = $table->find('all')->where(['id' => 1]);
$entity = $query->first();
$result = $table->delete($entity, ['cascade' => false]);
$result = $table->delete($entity);
$articles = $table->association('article')->target();
$query = $articles->find('all')->where(['author_id' => $entity->id]);
@@ -1643,34 +1643,14 @@ public function testDeleteBelongsToMany() {
$this->assertNull($query->execute()->one(), 'Should not find any rows.');
}
/**
* Test delete with belongsToMany and cascade = false
*
* @return void
*/
public function testDeleteCascadeFalseBelongsToMany() {
$table = TableRegistry::get('article');
$table->belongsToMany('tag', [
'foreignKey' => 'article_id',
'joinTable' => 'articles_tags'
]);
$query = $table->find('all')->where(['id' => 1]);
$entity = $query->first();
$table->delete($entity, ['cascade' => false]);
$pivot = $table->association('tag')->pivot();
$query = $pivot->find('all')->where(['article_id' => 1]);
$this->assertCount(2, $query->execute(), 'Should find rows.');
}
/**
* Test delete callbacks
*
* @return void
*/
public function testDeleteCallbacks() {
$entity = new \Cake\ORM\Entity(['id' => 1, 'name' => 'mark']);
$options = new \ArrayObject(['atomic' => true, 'cascade' => true]);
$options = new \ArrayObject(['atomic' => true]);
$mock = $this->getMock('Cake\Event\EventManager');
$mock->expects($this->at(0))

0 comments on commit 2b99452

Please sign in to comment.
You can’t perform that action at this time.