Browse Source

Adding support to the RulesChecker to check entities before deleting them

Jose Lorenzo Rodriguez 11 years ago
parent
commit
dc560fa1f1

+ 96 - 4
src/ORM/RulesChecker.php

@@ -17,6 +17,7 @@ namespace Cake\ORM;
 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 @@ use Cake\ORM\Rule\IsUnique;
  * ### 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
  */
@@ -62,6 +85,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
  *
  * @var array
@@ -145,6 +175,53 @@ class RulesChecker {
 	}
 
 /**
+ * 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'
  *
@@ -175,6 +252,21 @@ class RulesChecker {
 	}
 
 /**
+ * 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.
  *

+ 28 - 14
src/ORM/Table.php

@@ -1263,7 +1263,8 @@ class Table implements RepositoryInterface, EventListenerInterface {
 			$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 @@ class Table implements RepositoryInterface, EventListenerInterface {
  * ### 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 @@ class Table implements RepositoryInterface, EventListenerInterface {
  *
  */
 	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 @@ class Table implements RepositoryInterface, EventListenerInterface {
  * @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 @@ class Table implements RepositoryInterface, EventListenerInterface {
 			$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 @@ class Table implements RepositoryInterface, EventListenerInterface {
  * @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;

+ 17 - 0
tests/TestCase/ORM/RulesCheckerIntegrationTest.php

@@ -489,4 +489,21 @@ class RulesCheckerIntegrationTest extends TestCase {
 		$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));
+	}
+
 }

+ 4 - 4
tests/TestCase/ORM/TableTest.php

@@ -1944,7 +1944,7 @@ class TableTest extends TestCase {
  */
 	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 @@ class TableTest extends TestCase {
 
 		$table = TableRegistry::get('users', ['eventManager' => $mock]);
 		$entity->isNew(false);
-		$table->delete($entity);
+		$table->delete($entity, ['checkRules' => false]);
 	}
 
 /**
@@ -1997,7 +1997,7 @@ class TableTest extends TestCase {
 
 		$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 @@ class TableTest extends TestCase {
 
 		$table = TableRegistry::get('users', ['eventManager' => $mock]);
 		$entity->isNew(false);
-		$result = $table->delete($entity);
+		$result = $table->delete($entity, ['checkRules' => false]);
 		$this->assertEquals('got stopped', $result);
 	}