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:
Download code
<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:
Download code
<?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:
Download code
<?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:
Download code
<?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.
Comments
Comment
1 Another way
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'));?>
Comment
2 Limitations
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 :)
Comment
3 Hadnt thought of that
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.
Comment
4 Works Correctly
Comment
5 Not working for me