Validation in another controller

by drayen
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.ctp

View 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(), nulltrue);
    }
?>

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.

Report

More on Tutorials

Advertising

Comments

  • Crash posted on 05/29/10 02:09:40 PM
    I ran into this problem just last night and the solution is far simpler than creating all that extra code.

    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:

    <?php 
    class 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:

    <?php 
    class 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.
  • Angeline posted on 08/06/09 05:18:20 AM
    I'm trying to display custom messages like, this field should not be empty using the $validate array in the model. I have two controllers, main and users.

    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>


  • m3nt0r posted on 07/24/09 04:08:33 AM
    The problem i found was that, if the controller that is used to display the foreign errors does not "use" the model the errors belong to, the code does not display anything.

    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.
  • Namaless posted on 08/23/08 06:19:09 PM
    Thanks to release this tutorial. I need more days a solutions and found here! :)
    • hammettt posted on 09/01/08 06:09:59 AM
      This doesn't seem to do anything for me, the only difference in my code is the view which shouldn't make a difference. I know that it is using the validation in the model as the required div is set. Is there any way I can find out what's wrong?
  • jaredhoyt posted on 09/05/07 11:33:24 AM
    drayen,

    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;

        $this->set('requestUrl',$this->data['Comment']['requestUrl']);
        return;
    And I changed the /comments/create.ctp to:

        <?=$this->requestAction($requestUrl,array('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.

    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.
  • drayen posted on 09/05/07 02:00:04 AM
    Nice solution jaredhoyt,

    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 :)

  • jaredhoyt posted on 09/04/07 04:03:09 PM
    I had this same exact problem making my blog about a month ago. I ended up solving it like this: I displayed my post at /posts/read/$id with the comment form at the bottom within a div with id="leaveComment". I set the url of the form to:

    'url'=>'/comments/create/'.$this->params['pass'][0].'#leaveComment'
    In CommentsController::create() :

        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);
        }
    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:

    <?=$this->requestAction('/posts/read/'.$params['pass'][0],array('return'));?>
    This renders the /posts/read/$id which is set to display errors as normal. The #leaveComment keeps the page focused on the comment form.
login to post a comment.