Stuffing Complex Logic into Model-less Forms
One new feature of CakePHP 3 is the ability to have Model-less form classes. These are typically useful for stuff like contact forms that might send an email:
<?php
namespace App\Form;
use Cake\Form\Form;
use Cake\Form\Schema;
use Cake\Mailer\Email;
use Cake\Validation\Validator;
class ContactForm extends Form
{
// require some data
protected function _buildSchema(Schema $schema)
{
return $schema->addField('name', 'string')
->addField('email', ['type' => 'string'])
->addField('body', ['type' => 'text']);
}
// validate the incoming data
protected function _buildValidator(Validator $validator)
{
return $validator->add('name', 'length', [
'rule' => ['minLength', 10],
'message' => 'A name is required'
])->add('email', 'format', [
'rule' => 'email',
'message' => 'A valid email address is required',
]);
}
// actually send an email
protected function _execute(array $data)
{
$email = new Email('default');
return $email->from([$data['email'] => $data['name']])
->to('mail@example.com', 'Mail Example')
->subject('Contact Form')
->message($data['body'])
->send();
}
}
?>
Neato burrito. One thing I love about this is the ability to have complex validation rulesets for specific actions. For instance, on a blog, I might have complex edit action that needs to check for editing writes before allowing a user to do anything:
<?php
namespace App\Form;
use Cake\Form\Form;
class PostEditForm
{
protected $_user = null;
public function __construct(array $user = [])
{
$this->_user = $user;
return $this;
}
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
return (new OwnedByCurrentUser($userId))->__invoke($value);
},
'message' => 'This photo isn\'t yours to battle with'
]);
}
}
?>
Nifty, huh? Usually I end up saving new records in my _execute()
method as well. Here is what that looks like in one of my form classes:
protected function _execute(array $data)
{
$battle_id = Hash::get($data, 'id', null);
$photo_id = Hash::get($data, 'photo_id', null);
$battles = TableRegistry::get('Battles');
$photos = TableRegistry::get('Photos');
$battle = $battles->find('Battle', [
'battle_id' => $battle_id,
])->firstOrFail();
if ($battle->confirmed != null) {
throw new MethodNotAllowedException('Battle has already been updated');
}
$photo = $photos->get($photo_id);
$battle->confirmed = true;
$battle->rival->photo = $photo;
if ($battles->save($battle)) {
return $battles->find('Battle', $battle->toFind())->firstOrFail();
}
$exception = new ValidationException('There are errors in the data you submitted');
$exception->errors($battle->errors());
throw $exception;
}
Why? Because it turns certain complex actions into the following:
public function edit()
{
$authedUser = $this->Auth->user();
$post = (new PostEditForm($authedUser))->execute($this->request->data);
$this->set(['post' => $post]);
}
Instead of litering logic across:
- a custom validation method in my model
- a controller action
- some other random model method or protected controller method
I can group it all together into one, logical unit that can be easily unit tested for various types of input. A side-benefit of this is that if I absolutely need to, I can always re-use a given action’s logic with as few as three lines of code within say, idk, a console shell.