Validation in another controller
You have 2 controllers, posts and comments, you want to be able to have a form on a posts view which submits to the comments controller, which does its thing then redirect to referrer - easy! BUT you also want the validation errors (if any) to display on the posts view - but currently they get lost when your redirect. Heres how to get around it!
The problem
Sample posts/view.ctpView Template:
<div class="post">
<h2><?php __('Post');?></h2>
<?php echo $post['Post']['body']?>
<fieldset>
<?php
echo $form->create('Comment');
echo('<legend>'.__('Add comment',true).'<legend>');
echo $form->input('title');
echo $form->input('body');
echo $form->input('author_name');
echo $form->input('author_email');
echo $form->input('author_url');
echo $form->end('submit')
?>
</fieldset>
</div>
I've hit this problem more than once, a nice way around it is to use AJAX to call up and submit the form, just like the bakery does - however ajax is not always an option, so poLK and i have come up with a very nice solution, even if i do say so myself!
The solution
Session to the rescue, after puzzling over this for a while and talking with a few people on #cakephp it seemed that the simplest and quickest solution was going to be to set the validation data into the session from the validating controller (e.g. Comments) and then pull it out later in the viewing controller (e.g. Posts) ready to be displayed.So thats exactly what we've done, we created function called _persistValidation to simplify getting and setting the data to the session.
Add the following function to your app_controller.php
Controller Class:
<?php
/**
* Called with some arguments (name of default model, or model from var $uses),
* models with invalid data will populate data and validation errors into the session.
*
* Called without arguments, it will try to load data and validation errors from session
* and attach them to proper models. Also merges $data to $this->data in controller.
*
* @author poLK
* @author drayen aka Alex McFadyen
*
* Licensed under The MIT License
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
function _persistValidation() {
$args = func_get_args();
if (empty($args)) {
if ($this->Session->check('Validation')) {
$validation = $this->Session->read('Validation');
$this->Session->del('Validation');
foreach ($validation as $modelName => $sessData) {
if ($this->name != $sessData['controller']){
if (in_array($modelName, $this->modelNames)) {
$Model =& $this->{$modelName};
} elseif (ClassRegistry::isKeySet($modelName)) {
$Model =& ClassRegistry::getObject($modelName);
} else {
continue;
}
$Model->data = $sessData['data'];
$Model->validationErrors = $sessData['validationErrors'];
$this->data = Set::merge($sessData['data'],$this->data);
}
}
}
} else {
foreach($args as $modelName) {
if (in_array($modelName, $this->modelNames) && !empty($this->{$modelName}->validationErrors)) {
$this->Session->write('Validation.'.$modelName, array(
'controller' => $this->name,
'data' => $this->{$modelName}->data,
'validationErrors' => $this->{$modelName}->validationErrors
));
}
}
}
}
?>
We also wanted to make the act of pulling the data out of the session seamless, so no additional code would be needed in the viewing controller (Posts), so were going to pull out the data automatically, using beforeRender().
Add the following function to your viewing controller or app_contoller.php (if your lazy, like all good programmers are, and want it enabled for all controllers).
Controller Class:
<?php
function beforeRender(){
$this->_persistValidation();
}
?>
With that in place, all you need to do is call _persistValidation, from your validating controller (Comments) before you redirect e.g.
Controller Class:
<?php
function add() {
if (!empty($this->data)) {
$this->cleanUpFields();
$this->Comment->create();
if ($this->Comment->save($this->data)) {
$this->Session->setFlash('The Comment has been saved');
} else {
$this->_persistValidation('Comment');
$this->Session->setFlash('The Comment could not be saved. Please correct the errors and try again.');
}
}
$this->redirect($this->referer(), null, true);
}
?>
Yup, its that simple, just $this->_persistValidation('Comment'); and your done!!
Enjoy
Gotchas
In most situations this should just work, but there are situations where it can cause problems.
Say you have Post->Message and Message->UserTo Message->UserFrom, after a restore, both UserTo and UserFrom (as they are references to one model instance) will be initialized with the correct data, BUT not with array('UserTo'=> etc - instead with array('User' =>
There may be others, please if you find them and or any improvements let me know and i will update the tutorial.

In typical fashion, the usual need for this falls into the blog category where comments are being posted from the posts controller.
In my Comments controller (receiving post data and validating):
Controller Class:
<?phpclass CommentsController extends AppController {
function add() {
if ($this->Comment->save($this->data)) {
$this->Session->setFlash('Thank you for commenting. All comments are subject to moderation','success');
$this->redirect($this->referer());
} else {
$this->Session->write('Entries.data',$this->data);
$this->Session->write('Entries.validationErrors',$this->Comment->validationErrors);
$this->redirect($this->referer().'#post-a-comment');
}
}
}
?>
My Entries (Posts) controller:
Controller Class:
<?php
class EntriesController extends AppController {
var $uses = array('Entry','Comment');
function afterFilter() {
if ($this->Session->valid()) {
if($this->Session->check('Entries.data') && $this->Session->check('Entries.validationErrors')) {
$this->Session->delete('Entries.data');
$this->Session->delete('Entries.validationErrors');
}
}
}
function view($url) {
if ($this->Session->check('Entries.data')) {
$this->data = $this->Session->read('Entries.data');
}
if ($this->Session->check('Entries.validationErrors')) {
$this->Comment->validationErrors = $this->Session->read('Entries.validationErrors');
}
if ($entry = $this->Entry->findByUrl($url)) {
$this->set('entry',$entry['Entry']);
$this->set('comments',$entry['Comment']);
} else $this->redirect('/');
}
}
?>
and finally, my Posts view:
View Template:
<?php
if ($session->check('Entries.validationErrors'))
$validationErrors = $session->read('Entries.validationErrors');
echo $form->create('Comment', array('action' => 'add'));
echo $form->input(
'entry_id',
array('type'=>'hidden', 'value' => $entry['id']));
echo $form->input('username');
echo $form->input('email',array('after' => '(optional)'));
echo $form->input('website',array('after' => '(optional)'));
echo $html->div('error-message',$validationErrors['website']);
echo $form->input('body', array('class' => 'wmd-ignore','rows' => '20'));
echo $form->end('Add Comment');
?>
All of this operates under the assumption you have the following in each controller or in your App controller:
Controller Class:
<?phpclass AppController extends Controller {
var $helpers = array('Html','Session','Form');
var $components = array('Session');
}
?>
In essence, this simply adds the post data and validation errors to a session variable after the validation fails. It then redirects back to the page the user was previously on (the Posts controller). The Posts controller then checks for the data in the session and if the POST data is there it assigns it to $this->data, which automatically populates the form in the view. In the controller, I override the validationErrors for the model that passed data back to me (Comment) which allows the FormHelper to populate the errors automagically in the view (don't forget to set $uses in your controller to include the Comment model). The Posts controller's afterFilter function then takes care of unsetting the session data used to populate the form's errors and data.
The end result is just setting, checking, and unsetting the session post data and validationErrors.
The index file of the main controller has the login and registration views. The action part of the login and register functions are in the user_controller. If the login and register function validate, they are redirected to the home page of the main controller,else they remain in the index page itself.
I want the validation messages to be displayed in the index page itself. But those messages appear only if there is a separate view file for login and register,i.e, /views/forms/register.ctp and /views/forms/login.ctp exist.
Is there a way to display those validation messages without having a separate view file for those functions? I tried _persistValidate, but I only get the Session->setFlash messages and not the $validate messages.
I have given my code below.Someone guide me please.
Model Class:
<?php
class User extends AppModel {
var $name = 'User';
var $components=array('Auth');
var $validate = array(
'name' => array(
'rule' => 'notEmpty',
'message' =>'Name cannot be null.'
),
'password' => array(
'rule' => 'notEmpty'
),
'email_id' => array(
'rule' => 'notEmpty'
)
);
function registerUser($data)
{
if (!empty($data))
{
$this->data['User']['name']=$data['User']['name'];
$this->data['User']['email_id']=$data['User']['email_id'];
$this->data['User']['password']=$data['User']['password'];
$existingUsers= $this->find('all');
foreach($existingUsers as $existingUser):
if($this->data['User']['email_id']==$existingUser['User']['email_id']){
return 0;
}
else{
$this->save($this->data);
$this->data['User']['id']= $this->find('all',array('fields' => array('User.id'),
'order' => 'User.id DESC'
));
$userId=$this->data['User']['id'][0]['User']['id'];
return $userId;
}
endforeach;
}
}
function loginUser($data)
{
$this->data['User']['email_id']=$data['User']['email_id'];
$this->data['User']['password']=$data['User']['password'];
$login=$this->find('all');
foreach($login as $form):
if($this->data['User']['email_id']==$form['User']['email_id'] && $this->data['User']['password']==$form['User']['password'])
{
$this->data['User']['id']= $this->find('all',array('fields' => array('User.id'),
'conditions'=>array('User.email_id'=> $this->data['User']['email_id'],'User.password'=>$this->data['User']['password'])
));
$userId=$this->data['User']['id'][0]['User']['id'];
return $userId;
}
endforeach;
}
}
?>
Controller Class:
<?php
class UsersController extends AppController
{
var $name = 'Users';
var $uses=array('Form','User','Attribute','Result');
var $helpers=array('Html','Ajax','Javascript','Form');
function register()
{
//$userId=$this->User->registerUser($this->data);
$this->Session->write('userId',$this->User->registerUser($this->data));
$this->User->data=$this->data;
if (!$this->User->validates())
{
$this->_persistValidation('Main');
$this->Session->setFlash('Please enter valid inputs');
$this->redirect('/main' );
return;
}
if($this->Session->read('userId')==0){
$this->_persistValidation('Main');
$this->Session->setFlash('You are already a registerd member.Log in your account');
$this->redirect('/main');
}
else{
$this->Session->setFlash('User account created');
$this->redirect('/main/home');
}
}
function login()
{
//$userId=$this->User->loginUser($this->data);
$this->Session->write('userId',$this->User->loginUser($this->data));
$this->User->data=$this->data;
if (!$this->User->validates())
{
$this->_persistValidation('Main');
$this->Session->setFlash('Please enter valid inputs');
$this->redirect('/main' );
return;
}
if($this->Session->read('userId')>0){
$this->Session->setFlash('Login Successful');
$this->redirect('/main/home');
break;
}
else{
$this->_persistValidation('Main');
$this->Session->setFlash('Username and password do not match.');
$this->redirect('/main');
}
}
}
?>
View Template:
<div id="register">
<h3>Register</h3>
<?php
echo $form->create('User',array('action'=>'register'));
echo $form->input('name');
echo $form->input('email_id');
echo $form->input('password');
echo $form->end('Register');
?>
</div>
<div id="login">
<h3>Login</h3>
<?php
echo $form->create('User',array('action'=>'login'));
echo $form->input('email_id');
echo $form->input('password');
echo $form->end('Login');
?>
</div>
That's because the conditional model initialization was not properly done. I changed the if/else in the if(empty($args)) section to the following (removed the "continue" aswell).
if (in_array($modelName, $this->modelNames)) {
$Model = & $this->{$modelName};
} else {
$Model = & ClassRegistry::init($modelName);
}
Now, if the model is not present in the controller, it will be initialized and everything works just fine.
Hope this helps.
Good question. Right now I'm only using comments for my posts. However, I'm wondering if there is a way to get the referrer and set() it into the view as the url to be used in requestAction().
edit: I actually tried the above and realized that it would work for one invalidation only. After that, the referring url would be /comments/create/$id ... Unless I set it in the session in the beginning - in which case I might as well just go with yours. I'm going to try the solution below and I think that will work.
Another way might be to add a hidden field onto the comments form itself with the url to be requested.
second edit: okay, so the above worked great. I added this to my comments form on /posts/read/$id:
<?=$form->hidden('Comment.requestUrl',array('value'=>$this->here));?>$this->here points to the intended url because it is contained within /posts/read/$id or /file/view/$id etc.In my CommentsController::create(), I added this line right before the return;
And I changed the /comments/create.ctp to:$this->set('requestUrl',$this->data['Comment']['requestUrl']);
return;
I don't consider it the most elegant solution only because I hate hidden values for some reason - but it is an alternative (not better ;) solution to using Sessions. I'm going to keep looking at Controller::viewPath and render() to see if can come up with something better.<?=$this->requestAction($requestUrl,array('return'));?>
note: I was worried that the above solution might be vulnerable to form forging, but I don't think it is. requestAction() doesn't accept urls outside of the site. It also checks my Auth/Acl for access to the requested Urls, so I don't have to worry about a forged form accessing a restricted part of the site. But if someone else has any opinions about its security, I'm all ears.
But what if you have say posts and files, both of which have comments - then you run into trouble (i used a similar method to you until i hit this problem). This solution gets around that problem and more :)
In CommentsController::create() :'url'=>'/comments/create/'.$this->params['pass'][0].'#leaveComment'
So, if data was posted to /comments/create/$id and it validates, then it's saved and the page is redirected back to /posts/read/$id#comments. If it doesn't validate, /comments/create/$id#leaveComment is rendered with the /comments/create.ctp view which contains one line of code:function create($post_id=null) {
if(!empty($this->data) && $post_id) {
$this->Post->id = $post_id;
if($this->Post->exists()) {
$this->Comment->create($this->data);
if($this->Comment->validates()) {
$this->Comment->data['Comment']['post_id'] = $post_id;
$this->Comment->save();
$this->redirect('/posts/read/'.$post_id.'#comments',null,true);
}
return;
}
}
$this->redirect('/',null,true);
}
This renders the /posts/read/$id which is set to display errors as normal. The #leaveComment keeps the page focused on the comment form.<?=$this->requestAction('/posts/read/'.$params['pass'][0],array('return'));?>