I was asked yesterday if I could elaborate on my OwnedByCurrentUser rule class. I’ll post it here, but also post on my process for developing rules.

Organization

First off, I hate having anonymous functions:

They are harder to test in isolation of the enclosing scope.

They make it more difficult to reason about classes because of the implicit extra scope/binding of the callable.

I think they look silly.

I definitely think they have their place - configuring the CRUD Plugin is one - but normally I try to stay away from them if possible. Instead, I use invokable callable classes.

For rules, I normally place my callable classes in src/Form/Rule . Here is what our initial OwnedByCurrentUser rule looks like:

<?php namespace App\Form\Rule; class OwnedByCurrentUser { /** * Performs the check * * @param mixed $value The data to validate * @param array $context A key value list of data that could be used as context * during validation. Recognized keys are: * - newRecord: (boolean) whether or not the data to be validated belongs to a * new record * - data: The full data that was passed to the validation process * - field: The name of the field that is being processed * - providers: associative array with objects or class names that will * be passed as the last argument for the validation method * @return bool */ public function __invoke($value, array $context = null) { } } ?>

Filling it in

When I write a rule, I’ll first write it to handle one very specific case. In this particular application, I had to ensure that a particular Battle was owned by a participant in the battle before allowing them to perform certain actions. My invoke looked like so:

public function __invoke($value, array $context = null) { $table = \Cake\ORM\TableRegistry::get('Battles'); return !!$table->find()->where([ 'id' => (int)$value, 'user_id' => $userId, ])->firstOrFail(); }

The above sort of works:

It actually throws a Cake\Datasource\Exception\RecordNotFoundException exception, which is incorrect for my use case, since I don’t want validation rules to throw exceptions

exception, which is incorrect for my use case, since I don’t want validation rules to throw exceptions I wasn’t sure where I was passing in the $userId . The $context maybe?

. The maybe? I’m offloading a lot of logic into the database. What if I don’t have compound index on id/user_id ? That would slow down this part of the app (maybe not a concern).

? That would slow down this part of the app (maybe not a concern). There was a table where I was thinking of re-using this in the near future that used creator_id instead of user_id to denote who owned the record (legacy applications, am I right?). This was hardcoded to the one field, which would mean more copy-pasting. I also couldn’t modify the table that was being checked. Boo.

Once I had a few tests going that brought up the above issues, I knew I had to refactor it.

Fixing issues

I took a step back and realized I wanted to instantiate rules and then invoke them several times. This meant modifying the rule instance state, as well as passing in an initial state. First, lets add a constructor:

protected $_alias; protected $_userId; protected $_fieldName; /** * Performs the check * * @param string $alias Table alias * @param mixed $userId A string or integer denoting a user's id * @param string $fieldName A name to use when checking an entity's association * @return void */ public function __construct($alias, $userId, $fieldName = 'user_id') { $this->_alias = $alias; $this->_userId = $userId; $this->_fieldName = $fieldName; } public function setTable($alias) { $this->_alias = $alias; } public function setUserId($userId) { $this->_userId = $userId; } public function setFieldName($fieldName) { $this->_fieldName = $fieldName; }

Each field is a protected field - meaning I can extend this easily by subclassing - and all have setters - meaning I can reuse a rule instance if necessary. Next I needed to modify the __invoke() method to use my customizations:

public function __invoke($value, array $context = null) { // handle the case where no userId was // specified or the user is logged out $userId = $this->_userId; if (empty($userId)) { return false; } // use the Table class specified by our configured alias $table = \Cake\ORM\TableRegistry::get($this->_alias); // Don't make the database do the heavy-lifting $entity = $table->find()->where(['id' => (int)$value])->first(); if (empty($entity)) { return false; } // Ensure any customized field matches our userId return $entity->get($this->_fieldName) == $userId; }

Wrapping it up

From yesterday’s post, here is how the rule is invoked:

protected function _buildValidator(Validator $validator) { // use $this->_user in my validation rules $userId = $this->_user->get('id'); $validator->add('id', 'custom', [ 'rule' => function ($value, $context) use ($userId) { // reusing an invokable class $rule = new OwnedByCurrentUser('Battles', $userId); return $rule($value, $context); }, 'message' => 'This photo isn\'t yours to battle with' ]); // This should also work $validator->add('id', 'custom', [ 'rule' => new OwnedByCurrentUser('Battles', $userId), 'message' => 'This photo isn\'t yours to battle with' ]); // As should this (and you can now re-use the rule) $rule = new OwnedByCurrentUser('Battles', $userId); $validator->add('id', 'custom', [ 'rule' => $rule, 'message' => 'This photo isn\'t yours to battle with' ]); }

Mopping up

When I first found out I could do this, I was quite delighted by it. Validation rules have always been a pain to test, and this was as good as it got. I now have an easy to understand class that is both easily testable and gives me increased code reuse.