Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Split rules up into ORM and non-ORM flavours.
Having a RulesChecker independent of the ORM will be very helpful when
implementing rules on the elastic search ODM.

This should be fully backwards compatible as the old classes and methods
are still around.
  • Loading branch information
markstory committed May 29, 2015
1 parent 7397ba3 commit 19c0c7c
Show file tree
Hide file tree
Showing 4 changed files with 408 additions and 347 deletions.
98 changes: 98 additions & 0 deletions src/Datasource/RulesAwareTrait.php
@@ -0,0 +1,98 @@
<?php
namespace Cake\Datasource;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\RulesChecker;

/**
* A trait that allows a class to build and apply application.
* rules.
*
* If the implementing class also implements EventAwareTrait, then
* events will be emitted when rules are checked.
*
* The implementing class is expected to define the `RULES_CLASS` constant
* if they need to customize which class is used for rules objects.
*/
trait RulesAwareTrait
{
/**
* The domain rules to be applied to entities saved by this table
*
* @var \Cake\Datasource\RulesChecker
*/
protected $_rulesChecker;

/**
* Returns whether or not the passed entity complies with all the rules stored in
* the rules checker.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param string $operation The operation being run. Either 'create', 'update' or 'delete'.
* @param \ArrayObject|array $options The options To be passed to the rules.
* @return bool
*/
public function checkRules(EntityInterface $entity, $operation = RulesChecker::CREATE, $options = null)
{
$rules = $this->rulesChecker();
$options = $options ?: new ArrayObject;
$options = is_array($options) ? new ArrayObject($options) : $options;
$hasEvents = method_exists($this, 'dispatchEvent');

if ($hasEvents) {
$event = $this->dispatchEvent(
'Model.beforeRules',
compact('entity', 'options', 'operation')
);
if ($event->isStopped()) {
return $event->result;
}
}

$result = $rules->check($entity, $operation, $options->getArrayCopy());

if ($hasEvents) {
$event = $this->dispatchEvent(
'Model.afterRules',
compact('entity', 'options', 'result', 'operation')
);

if ($event->isStopped()) {
return $event->result;
}
}
return $result;
}

/**
* Returns the rule checker for this table. A rules checker object is used to
* test an entity for validity on rules that may involve complex logic or data that
* needs to be fetched from the database or other sources.
*
* @return \Cake\Datasource\RulesChecker
*/
public function rulesChecker()
{
if ($this->_rulesChecker !== null) {
return $this->_rulesChecker;
}
$class = defined('static::RULES_CLASS') ? static::RULES_CLASS : 'Cake\Datasource\RulesChecker';
$this->_rulesChecker = $this->buildRules(new $class(['repository' => $this]));
$this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]);
return $this->_rulesChecker;
}

/**
* Returns rules checker object after modifying the one that was passed. Subclasses
* can override this method in order to initialize the rules to be applied to
* entities saved by this table.
*
* @param \Cake\Datasource\RulesChecker $rules The rules object to be modified.
* @return \Cake\Datasource\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
return $rules;
}
}
296 changes: 296 additions & 0 deletions src/Datasource/RulesChecker.php
@@ -0,0 +1,296 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.0.7
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;

use ArrayObject;
use BadMethodCallException;
use InvalidArgumentException;

/**
* Contains logic for storing and checking rules on entities
*
* RulesCheckers are used by Table classes to ensure that the
* current entity state satisfies the application logic and business rules.
*
* RulesCheckers afford different rules to be applied in the create and update
* scenario.
*
* ### Adding rules
*
* Rules must be callable objects that return true/false depending on whether or
* not the rule has been satisfied. 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(), RulesChecker::checkUpdate() or
* RulesChecker::checkDelete().
*/
class RulesChecker
{
/**
* 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
*/
protected $_rules = [];

/**
* The list of rules to check during create operations
*
* @var array
*/
protected $_createRules = [];

/**
* The list of rules to check during update operations
*
* @var array
*/
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
*/
protected $_options = [];

/**
* Whether or not to use I18n functions for translating default error messages
*
* @var bool
*/
protected $_useI18n = false;

/**
* Constructor. Takes the options to be passed to all rules.
*
* @param array $options The options to pass to every rule
*/
public function __construct(array $options)
{
$this->_options = $options;
$this->_useI18n = function_exists('__d');
}

/**
* Adds a rule that will be applied to the entity both on create and update
* 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 string $name The alias for a rule.
* @param array $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
*/
public function add(callable $rule, $name = null, array $options = [])
{
$this->_rules[] = $this->_addError($rule, $name, $options);
return $this;
}

/**
* Adds a rule that will be applied to the entity on create 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 string $name The alias for a rule.
* @param array $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
*/
public function addCreate(callable $rule, $name = null, array $options = [])
{
$this->_createRules[] = $this->_addError($rule, $name, $options);
return $this;
}

/**
* Adds a rule that will be applied to the entity on update 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 string $name The alias for a rule.
* @param array $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
*/
public function addUpdate(callable $rule, $name = null, array $options = [])
{
$this->_updateRules[] = $this->_addError($rule, $name, $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 string $name The alias for a rule.
* @param array $options List of extra options to pass to the rule callable as
* second argument.
* @return $this
*/
public function addDelete(callable $rule, $name = null, array $options = [])
{
$this->_deleteRules[] = $this->_addError($rule, $name, $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.
* @param string $mode Either 'create, 'update' or 'delete'.
* @param array $options Extra options to pass to checker functions.
* @return bool
* @throws \InvalidArgumentException if an invalid mode is passed.
*/
public function check(EntityInterface $entity, $mode, array $options = [])
{
if ($mode === self::CREATE) {
return $this->checkCreate($entity, $options);
}

if ($mode === self::UPDATE) {
return $this->checkUpdate($entity, $options);
}

if ($mode === self::DELETE) {
return $this->checkDelete($entity, $options);
}

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'
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array $options Extra options to pass to checker functions.
* @return bool
*/
public function checkCreate(EntityInterface $entity, array $options = [])
{
$success = true;
$options = $options + $this->_options;
foreach (array_merge($this->_rules, $this->_createRules) as $rule) {
$success = $rule($entity, $options) && $success;
}
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 'update'
*
* @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
* @param array $options Extra options to pass to checker functions.
* @return bool
*/
public function checkUpdate(EntityInterface $entity, array $options = [])
{
$success = true;
$options = $options + $this->_options;
foreach (array_merge($this->_rules, $this->_updateRules) as $rule) {
$success = $rule($entity, $options) && $success;
}
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.
* @param array $options Extra options to pass to checker functions.
* @return bool
*/
public function checkDelete(EntityInterface $entity, array $options = [])
{
$success = true;
$options = $options + $this->_options;
foreach ($this->_deleteRules as $rule) {
$success = $rule($entity, $options) && $success;
}
return $success;
}
}

12 comments on commit 19c0c7c

@dereuromark
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markstory This update isn't quite as BC for some: "Declaration of App\Model\Table\AdsTable::buildRules() should be compatible with Cake\ORM\Table::buildRules(Cake\Datasource\RulesChecker $rules)"

Since it was moved to a different class people will have to adjust their use statements accordingly. Should be communicate that in a release note?

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for exemple : I've the error with this code.

public function buildRules(RulesChecker $rules) {

$rules->add($rules->existsIn(['ads_category_id'], 'AdsCategories'));
$rules->add($rules->existsIn(['ads_type_id'], 'AdsTypes'));
$rules->add($rules->existsIn(['country_id'], 'Countries'));
$rules->add($rules->existsIn(['region_id'], 'Regions'));
$rules->add($rules->existsIn(['departement_id'], 'Departements'));

return $rules;
}

@ADmad
Copy link
Member

@ADmad ADmad commented on 19c0c7c May 31, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to generate error in every table that has buildRules() method.

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we don't need buildrules() method ?

@markstory
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you do, I'll fix it up so the code is actually backwards compatible.

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I left :
public function buildRules(RulesChecker $rules) {

 $rules->add($rules->isUnique(['email', 'username']));

 return $rules;

}
I ve de same error after a composer update.

@markstory
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LeFuret17 Yes, right now master is not backwards compatible. I will have it fixed today.

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so now we dont use this?
When i make a bake model it's wrong too ?

@markstory
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LeFuret17 The code in master is broken. The code you pasted should work, but doesn't right now because of the defect.

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So i diseabled all buildrules in Models and i wait for the correction

@markstory
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LeFuret17 It is fixed now.

@LeFuret17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for me it's good

Please sign in to comment.