This tutorial uses the deprecated $this→Model→setId($id), use $this→Model→id = $id instead
Complex Model Validation Routines
This tutorial will walk you through the process of implementing complex model validation routines for each field inside your data model. I quickly discovered that simple regular expressions weren’t going to cut it. What if I needed to base my validation on a database lookup or a web service call or comparison against something I had previously stored in my session? If you’ve found yourself asking the same sort of questions then you may find this tutorial useful.
Step 1: Setup SQL
In this tutorial we’ll be using a generic user model. So let’s begin by creating the users table.
CREATE TABLE `users` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `email` varchar(120) NOT NULL, `password` varchar(20) NOT NULL, `firstname` varchar(30) NOT NULL, `lastname` varchar(30) NOT NULL, `phone` varchar(15) DEFAULT NULL, `modified` datetime NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`) );
Step 2: Setup the admin route
This example takes advantage of the built-in administration route. To enable this just uncomment the line in app/config/core.php that looks like this:
/** * Uncomment the define below to use cake built in admin routes. * You can set this value to anything you want. * All methods related to the admin route should be prefixed with the * name you set CAKE_ADMIN to. * For example: admin_index, admin_edit */ define('CAKE_ADMIN', 'admin');
This will effectively do two things:
- Allow your special administration functions to live behind /admin/
- Allow you to have extra control, i.e. separate index and admin_index methods based on the URL used
Step 3: Create the model
Our model is going to slightly differ from that of the standard setup because we won’t be using the standard $validate property. Instead we’ll be using custom validate[Fieldname] methods for each field that we want to validate. Here is the code:
<?php class User extends AppModel { var $name = 'User'; function validateFirstname() { if (!preg_match(VALID_NOT_EMPTY, $this->data[$this->name]['firstname'])) { $this->validationErrors['firstname'] = 'First Name is required'; } } function validateLastname() { if (!preg_match(VALID_NOT_EMPTY, $this->data[$this->name]['lastname'])) { $this->validationErrors['lastname'] = 'Last Name is required'; } } function validateEmail() { /** * Checking for double entries resulted for me in an error using postgres * Query failed: ERROR: zero-length delimited identifier at or near """" at character 241 in /cake/libs/model/dbo/dbo_postgres.php */ $result = $this->find('`' . $this->name . '`.`email` = "' . $this->data[$this->name]['email'] . '"'); if ($result) { $result2 = $this->find('`' . $this->name . '`.`id` = "' . $this->data[$this->name]['id'] . '" and `' . $this->name . '`.`email` = "' . $this->data[$this->name]['email'] . '"'); if (!$result2) { $this->validationErrors['email'] = 'Email is already registered'; } } if (!preg_match(VALID_EMAIL, $this->data[$this->name]['email'])) { $this->validationErrors['email'] = 'Email is invalid'; } if (!preg_match(VALID_NOT_EMPTY, $this->data[$this->name]['email'])) { $this->validationErrors['email'] = 'Email is required'; } } function validatePassword() { if (!preg_match(VALID_NOT_EMPTY, $this->data[$this->name]['password'])) { $this->validationErrors['password'] = 'Password is required'; } } } ?>
Save this file in the ‘app/models’ directory and name it ‘user.php’. You can see that we have identified a camelized validate method for each field we want to validate. One of the big reasons we wanted to do this in the first place was for the additional functionality that you see in the validateEmail field. Notice that there are three separate validation tests taking place; email has already been registered, email is invalid, email is required. These are things that cannot be accomplished with a simple regular expression alone. The other thing to pay attention to here is that we’ve associated a custom error message with each validation test. This is an added bonus as well.
Unfortunately this code doesn’t do anything in it’s current state because nothing has been programmed to call these methods. To do this we need to extend the AppModel and for that we need this code:
<?php class AppModel extends Model { /** * perform extra validations before saving a model * * before saving your model check to see if any custom validation methods * have been declared in AppModel and if they do run them before saving */ function beforeSave() { foreach ($this->data[$this->name] as $field => $value) { $validateMethod = 'validate' . Inflector::camelize($field); if (method_exists(&$this, $validateMethod)) { call_user_func(array(&$this, $validateMethod)); } } if ($this->validationErrors) { return(false); } else { return(true); } } } ?>
Save this file in the ‘app’ directory and name it ‘app_model.php’. All we’re doing here is stopping to look for the validate[Fieldname] methods that we previously created in our model file. Every validation method that is found is executed. Once they’ve all been executed we look to see if any of them generated validationErrors and return true or false based on that information. Any validation failures will stop the model from being saved to the database. So far so good.
Step 4: Create the controller
The rest of the major processing all happens in the controller. After all it is the controller. First let’s create our users controller with the following code:
<?php class usersController extends AppController { var $name = 'Users'; var $helpers = array('Html','Error'); function index() { // list all $this->set('data', $this->User->findAll()); } function admin_index() { // list all $this->set('data', $this->User->findAll()); } function view($id) { $this->User->setId($id); $this->params['data'] = $this->User->read(); $this->set('data', $this->params['data']); $this->render(); } function admin_view($id) { $this->User->setId($id); $this->params['data'] = $this->User->read(); $this->set('data', $this->params['data']); $this->render(); } function admin_add() { if (empty($this->params['data'])) { $this->render(); } else { if ($this->User->save($this->params['data'])) { $this->flash('New user was created successfully','/admin/users'); } else { $this->set('data', $this->params['data']); $this->validateErrors($this->User); $this->render(); } } } function admin_edit($id = null) { if (empty($this->params['data'])) { $this->User->setId($id); $this->params['data'] = $this->User->read(); $this->set('data', $this->params['data']); $this->render(); } else { if ($this->User->save($this->params['data'])) { $this->flash('User was modified successfully','/admin/users'); } else { $this->set('data', $this->params['data']); $this->render(); } } } function admin_delete($id) { if ($this->User->del($id)) { $this->flash('User was deleted successfully','/admin/users'); } } } ?>
Save this file in the ‘app/controllers’ directory and name it ‘users_controller.php’. You may notice that we’ve included a new helper called ‘Error’. Don’t worry about it right now, we’ll be creating it later. Aside from the general index view nothing is accessible through this controller unless it is accessed through the admin route, i.e. /admin/users/edit will work while /users/edit will not.
Note: I prefer using the ‘admin’ route, but it’s really just based on personal preference and/or specific needs for your project. All of these examples will work just as well without the ‘admin’ route.
Nothing too mind boggling, just a standard controller layout. Let’s move onto the views (there are plenty more of them).
Step 5: Create the views
Now we need a view for this model and this will come in the form of four templates. Let’s start with the default index template:
<h1>Users</h1>
<table>
<tr>
<th>first name</th>
<th>last name</th>
<th>email</th>
<th></th>
</tr>
<?php foreach ($data as $user) { ?>
<tr>
<td><?php echo $user['User']['firstname']; ?></td>
<td><?php echo $user['User']['lastname']; ?></td>
<td><?php echo $html->link($user['User']['email'], "mailto:{$user['User']['email']}"); ?></td>
<td><?php echo $html->link('View', "/users/view/{$user['User']['id']}" ); ?></td>
</tr>
<?php } ?>
</table>
Save this file in the ‘app/views/users’ directory and name it ‘index.thtml’. This just shows a normal user what other users are in the database and gives them a way to view their profile. The next file will look very similar, but it has much more power:
<h1>Users</h1>
<table>
<tr>
<th>first name</th>
<th>last name</th>
<th>email</th>
<th colspan="3"></th>
</tr>
<?php foreach ($data as $user) { ?>
<tr>
<td><?php echo $user['User']['firstname']; ?></td>
<td><?php echo $user['User']['lastname']; ?></td>
<td><?php echo $html->link($user['User']['email'], "mailto:{$user['User']['email']}"); ?></td>
<td><?php echo $html->link('View', "/admin/users/view/{$user['User']['id']}" ); ?></td>
<td><?php echo $html->link('Delete',"/admin/users/delete/{$user['User']['id']}", null, "Are you sure you want to delete {$user['User']['firstname']} {$user['User']['lastname']}?")?></td>
<td><?php echo $html->link('Edit',"/admin/users/edit/{$user['User']['id']}")?></td>
</tr>
<?php } ?>
<tr>
<td colspan="6" align="right"><?php echo $html->link('Add new user', '/admin/users/add') ?></td>
</tr>
</table>
Save this file in the ‘app/views/users’ directory and name it ‘admin_index.thtml’. This is the same index file you just created but with admin capabilities. In other words it also provides links to add, edit and delete user entries. So on to the next template:
<b><?php echo $data['User']['firstname'] . ' ' . $data['User']['lastname']; ?></b><br/> <?php echo $html->link($data['User']['email'], 'mailto:' . $data['User']['email']); ?><br/> <?php echo $data['User']['phone']; ?>
Save this file in the ‘app/views/users’ directory and name it ‘view.thtml’. This just shows us a users profile.
<b><?php echo $data['User']['firstname'] . ' ' . $data['User']['lastname']; ?></b><br/> <?php echo $html->link($data['User']['email'], 'mailto:' . $data['User']['email']); ?><br/> <?php echo $data['User']['phone']; ?><br/><br/> <?php echo $html->link('Back to users', '/admin/users/'); ?>
Save this file in the ‘app/views/users’ directory and name it ‘admin_view.thtml’. This is just like the last template except it provides a ‘Back to users’ link for the administrator.
<h1>Add User</h1> <?php echo $html->formTag('/admin/users/add'); echo $html->hidden('User/id'); ?> <table> <tr> <td><label class="required" for="user_firstname">first name</label></td> <td><?php echo $html->input('User/firstname', array('id' => 'user_firstname')) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/firstname'))); ?></td> </tr> <tr> <td><label class="required" for="user_lastname">last name</label></td> <td><?php echo $html->input('User/lastname', array('id' => 'user_lastname')) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/lastname'))); ?></td> </tr> <tr> <td><label class="required" for="user_email">email</label></td> <td><?php echo $html->input('User/email', array('id' => 'user_email')) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/email'))); ?></td> </tr> <tr> <td><label class="required" for="user_password">password</label></td> <td><?php echo $html->password('User/password', array('id' => 'user_password')) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/password'))); ?></td> </tr> <tr> <td><label for="user_phone">phone</label></td> <td><?php echo $html->input('User/phone', array('id' => 'user_phone')) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/phone'))); ?></td> </tr> <tr> <td colspan="2" align="right"><?php echo $html->submit('Save'); ?> <input type="button" onclick="location.href='/admin/users';" value="Cancel"/></td> </tr> </table> </form>
Save this file in the ‘app/views/users’ directory and name it ‘admin_add.thtml’. This file has a lot of different snippets inside it, but broken down they’re all quite simple. The three things to pay attention to here are the calls to the ‘$html’ helper, the ‘$error’ helper and the ‘errorMessage’ element. The html helper is built-in to CakePHP, but the other two are not and so we have to build them. But first, let’s add our last template:
<h1>Edit User</h1> <?php echo $html->formTag('/admin/users/edit'); echo $html->hidden('User/id'); ?> <table> <tr> <td><label class="required" for="user_firstname">first name</label></td> <td><?php echo $html->input('User/firstname', array('id' => 'user_firstname', 'value' => $data['User']['firstname'])) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/firstname'))); ?></td> </tr> <tr> <td><label class="required" for="user_lastname">last name</label></td> <td><?php echo $html->input('User/lastname', array('id' => 'user_lastname', 'value' => $data['User']['lastname'])) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/lastname'))); ?></td> </tr> <tr> <td><label class="required" for="user_email">email</label></td> <td><?php echo $html->input('User/email', array('id' => 'user_email', 'value' => $data['User']['email'])) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/email'))); ?></td> </tr> <tr> <td><label class="required" for="user_password">password</label></td> <td><?php echo $html->password('User/password', array('id' => 'user_password', 'value' => $data['User']['password'])) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/password'))); ?></td> </tr> <tr> <td><label for="user_phone">phone</label></td> <td><?php echo $html->input('User/phone', array('id' => 'user_phone', 'value' => $data['User']['phone'])) . $this->renderElement('errorMessage', array('message' => $error->showMessage('User/phone'))); ?></td> </tr> <tr> <td colspan="2" align="right"><?php echo $html->submit('Save'); ?> <input type="button" onclick="location.href='/admin/users';" value="Cancel"/></td> </tr> </table> </form>
Save this file in the ‘app/views/users’ directory and name it ‘admin_edit.thtml’. The only difference between these two files is that this one includes the ‘$html→input’ default ‘value’ from the ‘$data’ variable which was set in the controller. On to something else.
Step 6: Create the helper
Previously we mentioned the ‘$error’ helper, well it’s time to build it and it looks like this:
<?php /** * Error Helper class for easy management of processing errors * * ErrorHelper provides the level of control beyond checking for empty fields */ class ErrorHelper extends Helper { /** * show error messages for a particular field */ function showMessage($target) { list($model, $field) = explode('/', $target); if (isset($this->validationErrors[$model][$field])) { return($this->validationErrors[$model][$field]); } } } ?>
Save this file in the ‘app/views/helpers’ directory and name it ‘error.php’. This file provides us with a way to pull back an error message for a particular ‘model/field’ as it was saved earlier from within the model. The other piece to this is the element that we use to display it.
Step 7: Create the element
<?php if (isset($message)) { echo '<div class="error_message">' . $message . '</div>'; } ?>
Save this file in the ‘app/views/elements’ directory and name it ‘errorMessage.thtml’. This file just wraps the same html code around your error message that the $html helper wraps around it’s error messages. I thought I’d just be consistent so there are fewer CSS classes to contend with in the future.
Step 8: Test it out
Hit the path that you setup at ‘/admin/users/’ and you should be good to go. I hope this turns out as useful to your as it was to me. Thanks to all of the guys in IRC for helping me out with this, especially ‘PhpNut’, ‘olleolleolle’ and ‘the_undefined’.