Skip to content

Commit

Permalink
Adding support to the RulesChecker to check entities before deleting …
Browse files Browse the repository at this point in the history
…them
  • Loading branch information
lorenzo committed Dec 6, 2014
1 parent 3b1041f commit dc560fa
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 22 deletions.
100 changes: 96 additions & 4 deletions src/ORM/RulesChecker.php
Expand Up @@ -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
Expand All @@ -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
*/
Expand All @@ -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
*
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down
42 changes: 28 additions & 14 deletions src/ORM/Table.php
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
*
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions tests/TestCase/ORM/RulesCheckerIntegrationTest.php
Expand Up @@ -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));
}

}
8 changes: 4 additions & 4 deletions tests/TestCase/ORM/TableTest.php
Expand Up @@ -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');

Expand Down Expand Up @@ -1976,7 +1976,7 @@ public function testDeleteCallbacks() {

$table = TableRegistry::get('users', ['eventManager' => $mock]);
$entity->isNew(false);
$table->delete($entity);
$table->delete($entity, ['checkRules' => false]);
}

/**
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down

0 comments on commit dc560fa

Please sign in to comment.