From dc560fa1f1e73a51da5efdba488b217817acaaf8 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 7 Dec 2014 00:41:51 +0100 Subject: [PATCH] Adding support to the RulesChecker to check entities before deleting them --- src/ORM/RulesChecker.php | 100 +++++++++++++++++- src/ORM/Table.php | 42 +++++--- .../ORM/RulesCheckerIntegrationTest.php | 17 +++ tests/TestCase/ORM/TableTest.php | 8 +- 4 files changed, 145 insertions(+), 22 deletions(-) diff --git a/src/ORM/RulesChecker.php b/src/ORM/RulesChecker.php index 79b42984d3f..760f73fde29 100644 --- a/src/ORM/RulesChecker.php +++ b/src/ORM/RulesChecker.php @@ -17,6 +17,7 @@ use Cake\Datasource\EntityInterface; use Cake\ORM\Rule\ExistsIn; use Cake\ORM\Rule\IsUnique; +use InvalidArgumentException; /** * Contains logic for storing and checking rules on entities @@ -30,18 +31,40 @@ * ### Adding rules * * Rules must be callable objects that return true/false depending on whether or - * not the rule has been satisified. You can use RulesChecker::add(), RulesChecker::addCreate() - * and RulesChecker::addUpdate() to add rules to a checker. + * not the rule has been satisified. You can use RulesChecker::add(), RulesChecker::addCreate(), + * RulesChecker::addUpdate() and RulesChecker::addDelete to add rules to a checker. * * ### Running checks * * Generally a Table object will invoke the rules objects, but you can manually - * invoke the checks by calling RulesChecker::checkCreate() or RulesChecker::checkUpdate(). + * invoke the checks by calling RulesChecker::checkCreate(), RulesChecker::checkUpdate() or + * RulesChecker::checkDelete(). */ class RulesChecker { /** - * The list of rules to be checked on every case + * Indicates that the checking rules to apply are those used for creating entities + * + * @var string + */ + const CREATE = 'create'; + +/** + * Indicates that the checking rules to apply are those used for updating entities + * + * @var string + */ + const UPDATE = 'update'; + +/** + * Indicates that the checking rules to apply are those used for deleting entities + * + * @var string + */ + const DELETE = 'delete'; + +/** + * The list of rules to be checked on both create and update operations * * @var array */ @@ -61,6 +84,13 @@ class RulesChecker { */ protected $_updateRules = []; +/** + * The list of rules to check during delete operations + * + * @var array + */ + protected $_deleteRules = []; + /** * List of options to pass to every callable rule * @@ -144,6 +174,53 @@ public function addUpdate(callable $rule, array $options = []) { return $this; } +/** + * Adds a rule that will be applied to the entity on delete operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addDelete(callable $rule, array $options = []) { + $this->_deleteRules[] = $this->_addError($rule, $options); + return $this; + } + +/** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules to be applied are depended on the $mode parameter which + * can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @return bool + * @throws \InvalidArgumentException if an invalid mode is passed. + */ + public function check(EntityInterface $entity, $mode) { + if ($mode === self::CREATE) { + return $this->checkCreate($entity); + } + + if ($mode === self::UPDATE) { + return $this->checkUpdate($entity); + } + + if ($mode === self::DELETE) { + return $this->checkDelete($entity); + } + + throw new InvalidArgumentException('Wrong checking mode: ' . $mode); + } + /** * Runs each of the rules by passing the provided entity and returns true if all * of them pass. The rules selected will be only those specified to be run on 'create' @@ -174,6 +251,21 @@ public function checkUpdate(EntityInterface $entity) { return $success; } +/** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'delete' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @return bool + */ + public function checkDelete(EntityInterface $entity) { + $success = true; + foreach ($this->_deleteRules as $rule) { + $success = $rule($entity, $this->_options) && $success; + } + return $success; + } + /** * Returns a callable that can be used as a rule for checking the uniqueness of a value * in the table. diff --git a/src/ORM/Table.php b/src/ORM/Table.php index 87c0f62cf57..e1c56487d63 100644 --- a/src/ORM/Table.php +++ b/src/ORM/Table.php @@ -1263,7 +1263,8 @@ protected function _processSave($entity, $options) { $entity->isNew(!$this->exists($conditions)); } - if ($options['checkRules'] && !$this->checkRules($entity)) { + $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; + if ($options['checkRules'] && !$this->checkRules($entity, $mode)) { return false; } @@ -1448,6 +1449,7 @@ protected function _update($entity, $data) { * ### Options * * - `atomic` Defaults to true. When true the deletion happens within a transaction. + * - `checkRules` Defaults to true. Check deletion rules before deleting the record. * * ### Events * @@ -1462,7 +1464,7 @@ protected function _update($entity, $data) { * */ public function delete(EntityInterface $entity, $options = []) { - $options = new \ArrayObject($options + ['atomic' => true]); + $options = new \ArrayObject($options + ['atomic' => true, 'checkRules' => true]); $process = function () use ($entity, $options) { return $this->_processDelete($entity, $options); @@ -1487,14 +1489,6 @@ public function delete(EntityInterface $entity, $options = []) { * @return bool success */ protected function _processDelete($entity, $options) { - $event = $this->dispatchEvent('Model.beforeDelete', [ - 'entity' => $entity, - 'options' => $options - ]); - if ($event->isStopped()) { - return $event->result; - } - if ($entity->isNew()) { return false; } @@ -1504,6 +1498,20 @@ protected function _processDelete($entity, $options) { $msg = 'Deleting requires all primary key values.'; throw new \InvalidArgumentException($msg); } + + if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE)) { + return false; + } + + $event = $this->dispatchEvent('Model.beforeDelete', [ + 'entity' => $entity, + 'options' => $options + ]); + + if ($event->isStopped()) { + return $event->result; + } + $this->_associations->cascadeDelete($entity, $options->getArrayCopy()); $query = $this->query(); @@ -1894,16 +1902,22 @@ public function validateUnique($value, array $options, array $context = []) { * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. * @return bool */ - public function checkRules(EntityInterface $entity) { + public function checkRules(EntityInterface $entity, $operation = RulesChecker::CREATE) { $rules = $this->rulesChecker(); - $event = $this->dispatchEvent('Model.beforeRules', compact('entity', 'rules')); + $event = $this->dispatchEvent( + 'Model.beforeRules', + compact('entity', 'rules', 'operation') + ); if ($event->isStopped()) { return $event->result; } - $result = $entity->isNew() ? $rules->checkCreate($entity) : $rules->checkUpdate($entity); - $event = $this->dispatchEvent('Model.afterRules', compact('entity', 'rules', 'result')); + $result = $rules->check($entity, $operation); + $event = $this->dispatchEvent( + 'Model.afterRules', + compact('entity', 'rules', 'result', 'operation') + ); if ($event->isStopped()) { return $event->result; diff --git a/tests/TestCase/ORM/RulesCheckerIntegrationTest.php b/tests/TestCase/ORM/RulesCheckerIntegrationTest.php index 264225340ce..8ed3ad3adbb 100644 --- a/tests/TestCase/ORM/RulesCheckerIntegrationTest.php +++ b/tests/TestCase/ORM/RulesCheckerIntegrationTest.php @@ -489,4 +489,21 @@ public function testExistsInWithCleanFields() { $this->assertSame($entity, $table->save($entity)); } +/** + * Tests using rules to prevent delete operations + * + * @group delete + * @return void + */ + public function testDeleteRules() { + $table = TableRegistry::get('Articles'); + $rules = $table->rulesChecker(); + $rules->addDelete(function ($entity) { + return false; + }); + + $entity = $table->get(1); + $this->assertFalse($table->delete($entity)); + } + } diff --git a/tests/TestCase/ORM/TableTest.php b/tests/TestCase/ORM/TableTest.php index 60257bda155..260fc322b6b 100644 --- a/tests/TestCase/ORM/TableTest.php +++ b/tests/TestCase/ORM/TableTest.php @@ -1944,7 +1944,7 @@ public function testDeleteBelongsToMany() { */ public function testDeleteCallbacks() { $entity = new \Cake\ORM\Entity(['id' => 1, 'name' => 'mark']); - $options = new \ArrayObject(['atomic' => true]); + $options = new \ArrayObject(['atomic' => true, 'checkRules' => false]); $mock = $this->getMock('Cake\Event\EventManager'); @@ -1976,7 +1976,7 @@ public function testDeleteCallbacks() { $table = TableRegistry::get('users', ['eventManager' => $mock]); $entity->isNew(false); - $table->delete($entity); + $table->delete($entity, ['checkRules' => false]); } /** @@ -1997,7 +1997,7 @@ public function testDeleteBeforeDeleteAbort() { $table = TableRegistry::get('users', ['eventManager' => $mock]); $entity->isNew(false); - $result = $table->delete($entity); + $result = $table->delete($entity, ['checkRules' => false]); $this->assertNull($result); } @@ -2020,7 +2020,7 @@ public function testDeleteBeforeDeleteReturnResult() { $table = TableRegistry::get('users', ['eventManager' => $mock]); $entity->isNew(false); - $result = $table->delete($entity); + $result = $table->delete($entity, ['checkRules' => false]); $this->assertEquals('got stopped', $result); }