CodeIgniter: validating form array not working - codeigniter

I have an array of profile data I need to validate:
$user_group_profiles = $this->input->post('user_group_profiles', TRUE);
foreach ($user_group_profiles as $key => $user_group_profile)
{
$this->form_validation->set_rules("user_group_profiles[$key][profile_name]", 'Profile Name', 'trim|required');
$this->form_validation->set_rules("user_group_profiles[$key][birthdate]", 'Birthdate', 'trim|required');
// TODO: heigth/weight not required, but the validation somehow makes it required
$this->form_validation->set_rules("user_group_profiles[$key][height]", 'Height', 'trim|greater_than[0]');
$this->form_validation->set_rules("user_group_profiles[$key][weight]", 'Weight', 'trim|greater_than[0]');
}
Height and weight are option, but when no value is set for those fields, CI validation complains. A var_dump($user_group_profiles); shows this:
array
'ugp_b33333338' =>
array
'profile_name' => string '' (length=0)
'birthdate' => string '' (length=0)
'height' => string '' (length=0)
'weight' => string '' (length=0)
Any ideas what might be wrong?
EDIT 1:
I went into CI's Form_validation library and made $_field_data and public member. When I var_export it after, I got this:
'user_group_profiles[ugp_833333338][height]' =>
array
'field' => string 'user_group_profiles[ugp_833333338][height]' (length=42)
'label' => string 'Height' (length=6)
'rules' => string 'greater_than[1]' (length=15)
'is_array' => boolean true
'keys' =>
array
0 => string 'user_group_profiles' (length=19)
1 => string 'ugp_833333338' (length=13)
2 => string 'height' (length=6)
'postdata' => string '' (length=0)
'error' => string 'The Height field must contain a number greater than 1.' (length=54)

Ok - I've just spent an hour on this - and I've worked out the problem + solution
The issue is that when the variable you are testing is part of an array, CI interprets the field as being set, and thus "contains" a value (even when it is empty). Because that "value" is NOT a number greater than 0 - it fails.
Therefore you'll need to unset the $_POST (NOT $user_group_profiles) variable when it is "empty" so that it passes validation. Note - validation is run on $_POST - that is why you are unsetting $_POST and not $user_group_profiles
So the workaround is this:
$user_group_profiles = $this->input->post('user_group_profiles', TRUE);
foreach ($user_group_profiles as $key => $user_group_profile)
{
// Set the rules
$this->form_validation->set_rules($key."[profile_name]", "Profile Name", "trim|required");
$this->form_validation->set_rules($key."[birthdate]", "Birthdate", "trim|required");
$this->form_validation->set_rules($key."[height]", "Height", "trim|greater_than[0]");
$this->form_validation->set_rules($key."[weight]", "Weight", "trim|greater_than[0]");
// Now check if the field is actually empty
if (empty($user_group_profile['height']))
{
// If empty, remove from array so CI validation doesnt get tricked and fail
unset($_POST[$key]['height']);
}
if (empty($user_group_profile['weight']))
{
unset($_POST[$key]['weight']);
}
}
I've tested this - and it fixes your problem.
You could also program it this way if you dont want to touch the $_POST data:
foreach ($user_group_profiles as $key => $user_group_profile)
{
// Set the rules
$this->form_validation->set_rules($key."[profile_name]", "Profile Name", "trim|required");
$this->form_validation->set_rules($key."[birthdate]", "Birthdate", "trim|required");
// Now check if the field is actually empty
if ( ! empty($user_group_profile['height']))
{
$this->form_validation->set_rules($key."[height]", "Height", "trim|greater_than[0]");
}
if ( ! empty($user_group_profile['weight']))
{
$this->form_validation->set_rules($key."[weight]", "Weight", "trim|greater_than[0]");
}
}

Let's try and break this down into parts. I hope I can help you out here.
I'm assuming you're doing the following for the XSS Filtering with the second "TRUE" argument:
$user_group_profiles = $this->input->post('user_group_profiles', TRUE);
You can actually do the XSS filtering with the form validation rules, or if you prefer filter the post after the rules. See here for my preference:
$this->form_validation->set_rules('profile_name', 'Profile Name', 'xss_clean|trim|required');
So with knowing that now, we can follow the CI convention for their Form Validation Library. It's not necessary to grab the post before using the Form Validation Library because it auto-detects I believe the POST data anyway by the field name. For example:
$this->form_validation->set_rules('profile_name', 'Profile Name', 'trim|required|xss_clean');
$this->form_validation->set_rules('birthdate', 'Birthdate', 'trim|required|xss_clean');
$this->form_validation->set_rules('height', 'Height', 'trim|greater_than[0]|numeric');
$this->form_validation->set_rules('weight', 'Weight', 'trim|greater_than[0]|numeric');
if ($this->form_validation->run() == FALSE) {
$this->load->view('my_view_where_this_post_came_from');
} else {
$profile_name = $this->input->post('profile_name');
//or if you prefer the XSS check here, this:
//$profile_name = $this->input->post('profile_name', TRUE);
$birthdate= $this->input->post('birthdate');
$height= $this->input->post('height');
$weight= $this->input->post('weight');
//$user_group_profiles = $this->input->post();
}
I hope this helps!
EDIT: I also just noticed this. If you're trying to grab the entire post array the code is:
$user_group_profiles = $this->input->post(NULL, TRUE); // returns all POST items with XSS filter
$user_group_profiles = $this->input->post(); // returns all POST items without XSS filter
Not:
$user_group_profiles = $this->input->post('user_group_profiles');
A good help if you don't know your $_POST names or are confused, you can do this to see if that data is even there! Like this as your first line:
var_dump($_POST);
exit();

Related

CakePHP 3.0 throwing 'field required' error on non-empty field

I'm having difficulty with my CakePHP 3.0 site throwing an error when it shouldn't. I'm building an "Add Page" form, shown below:
echo $this->Form->create($newpage);
echo $this->Form->control('title');
echo $this->Form->control('content');
echo $this->Form->button('Save');
echo $this->Form->end();
The title and content are required. Once the form is submitted, 'title' is used to generate a page 'name', which is currently just the title in lowercase and with spaces removed (so "About Us" would have the name "aboutus"). This name is also saved, but must be unique (eg if you had pages with titles "No Space" and "NOSpace" they would both end up with the name "nospace" even though the titles are unique).
I have the following validation rules in PagesTable.php:
public function validationDefault(Validator $validator)
{
$validator = new Validator();
$validator
->requirePresence('title','content')
->lengthBetween('title', [0, 50])
->add(
'name',
['unique' => [
'rule' => 'validateUnique',
'provider' => 'table',
'message' => 'Not unique']
]
);
return $validator;
}
The form is submitted to the controller, which contains this:
public function add($mainpage_id = null) {
$newpage = $this->Pages->newEntity();
if ($this->request->is('post')) {
$newpage = $this->Pages->patchEntity($newpage, $this->request->data);
// Get page name by removing spaces from title
$name = strtolower(str_replace(' \'\"', '', $newpage->title));
// Get navigation order by getting number of current siblings, and adding 1
$siblings = $this->Pages->find('all')
->where(['parent_id' => $newpage->parent_id])
->count();
$nav_order = $siblings + 1;
$newpage = $this->Pages->patchEntity($newpage, ['name' => $name, 'nav_order' => $nav_order]);
if ($newpage->errors()) {
$errors = $newpage->errors();
if(isset($errors['name'])) {
$this->Flash->error('The title you have entered is too similar to one that already exists. To avoid confusion, please choose a different title.');
}else {
$this->Flash->error('Please correct errors below');
}
}else {
$this->Pages->save($newpage);
$page_id = $newpage->id;
$this->Flash->success('Page created');
return $this->redirect('/admin/pages/index');
exit;
}
}
$this->set(compact('newpage'));
$this->set('_serialize', ['newpage']);
}
However, when I try to submit a page, I get "This field is required" on the title field even if I've entered a title. In fact, it won't let me submit the form without entering a title (a message pops up saying "Fill out this field"), but then it throws the error.
Can anyone see what I'm doing wrong?
Codebase looks good. However, this might be not obvious behavior: calling patch entity method validate the passed patch data.
So the first time your patch is validated here:
$newpage = $this->Pages->patchEntity($newpage, $this->request->data);
and second time it's validated here:
$newpage = $this->Pages->patchEntity($newpage, ['name' => $name, 'nav_order' => $nav_order]);
and as second patch have no title, then validator said that it missed. To fix this you need to run patch once with all the data, i.e.:
$data = $this->request->data();
// Get page name by removing spaces from title
$data['name'] = strtolower(str_replace(' \'\"', '', $data['title']));
// Get navigation order by getting number of current siblings, and adding 1
$siblings = $this->Pages->find('all')
->where(['parent_id' => $data['parent_id']])
->count();
$data['nav_order'] = $siblings + 1;
$newpage = $this->Pages->patchEntity($newpage, $data);
Hope you understand the idea, and it'll help.
UPDATE: just noticed that there might be no parent_id in request... You can move this logic into afterSave callback in PagesTabe:
public function afterSave(Event $event, EntityInterface $entity) {
$siblings = $this->count()
->where(['parend_id '=> $entity->parent_id]);
// updateAll won't trigger the same hook again
$this->updateAll(
['nav_order' => ++$siblings],
['id' => $entity->id]
);
}

CakePHP Validate a specific rule only when a couple required fields aren't empty

I wrote a custom rule method for validating whether a record exists in the DB before adding a new record. I put the method in a behavior so I could share it with other models, but I've run into a chicken and egg situation.
In order to know whether a category has a specific group name already I need to have the category id, and the group name. So I pass those keys through using my custom rule (category_id and name). But, this won't work since if I don't choose a category_id by mistake then the query will occur on just the name, so I patched this with a couple lines, but need to return true if this is the case and bank on the category_id validation being invalid.
Is there a better way to implement this kind of validation? Is this not as bad as I think? Or just don't bother and in my controller drop hasAny() under my call to validates() if it passes.
MODEL:
public $validate = [
'category_id' => [
'rule' => 'notEmpty',
'message' => 'Category is required.'
],
'name' => [
'notEmpty' => [
'rule' => 'notEmpty',
'message' => 'Team is required.'
],
'recordExists' => [
'rule' => [ 'recordExists', [ 'category_id', 'name' ] ],
'message' => 'Group already exists.'
]
]
];
// BEHAVIOR:
public function recordExists( Model $Model, $conditions, $requireKeys )
{
// Overrite conditions to
$conditions = $Model->data[ $Model->name ];
// Trim all array elements and filter out any empty indexes
$conditions = array_map( 'trim', $conditions );
$conditions = array_filter( $conditions );
// Get the remaining non-empty keys
$conditionKeys = array_keys( $conditions );
// Only query for record if all required keys are in conditions
if (empty( array_diff( $requireKeys, $conditionKeys ) )) {
return !$Model->hasAny( $conditions );
}
// NOTE: seems wrong to return true based on the assumption the category_id validation has probably failed
return true;
}
Use the beforeValidate() callback of the model to check if the fields are present and if they're empty. If they're empty just unset() the recordExists validation rule in your models validation property. Copy them to a temporary variable or property in the case you want to set them back after your current operation.
And use $Model->alias, name will break if the model is used through an association that has a different name.
$conditions = $Model->data[ $Model->name ];

Drupal 7 Form Api AJAX callback on a text_format field do not work

I have a field in my custom module
$form['update']['text'] = array(
//'#type' => 'textarea',
'#type' => 'text_format',
If I use textarea everything is ok, but if I use the text_format my ajax callback do not change the value of the field.
function maintenance_update_form_update_callback($form, $form_state) {
$entry = $form_state['entries'][$form_state['values']['aid']];
// Setting the #value of items is the only way I was able to figure out
// to get replaced defaults on these items. #default_value will not do it
// and shouldn't.
dpm($form_state['entries'][$form_state['values']['aid']]);
foreach (array('status', 'type', 'title', 'text', 'name', 'email', 'phone', 'notes') as $item) {
$form['update'][$item]['#value'] = $entry->$item;
//dpm($entry);
//drupal_set_message("entry->item értéke: ENTRY: $entry, ITEM: $item , $entry->$item");
}
What can be wrong with this field? As far as I know this type also support ajax requests....
//drupal_set_message('Callback $form');
dpm($form);
return $form;
}
It's very simple - text_format is not simple single-value form item. It takes array for value param like that
array(
'value' => '123123',
'format' => 'html'
)
So, if you want to change value try this code:
$form['update']['text']['value']['#value'] = 'text message';
Adding to #ProFak solution, if you want to change the format of the text_format field type, you will have to use the following code
$altered_text_format = 'full_html';
$form['update']['format']['format']['#value'] = $altered_text_format;
So the complete solutions will look like as below
function maintenance_update_form_update_callback($form, $form_state) {
$entry = $form_state['entries'][$form_state['values']['aid']];
// Setting the #value of items is the only way I was able to figure out
// to get replaced defaults on these items. #default_value will not do it
// and shouldn't.
foreach (array('status', 'type', 'title', 'text', 'name', 'email', 'phone', 'notes') as $item) {
// #ProFak Solution
$form['update']['text']['value']['#value'] = $entry->$item;
// #Sukhjinder Singh additional solution
$altered_text_format = 'full_html';
$form['update']['format']['format']['#value'] = $altered_text_format;
}
return $form['update'];
}

cakephp: model validation based on another field

I am trying to setup Model validation on one field that only need to be checked if another field equals a particular value.
My first field is query which is a dropdown with many values, one value is 'Other' if this is selected then I need the second field 'query_other' to not be empty.
I have this setup in my Item Model:
public $validate = array(
'query' => array(
'notempty' => array(
'rule' => array('notempty'),
'message' => 'THE QUERY IS REQUIRED',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'query_other' => array(
'notempty' => array(
'rule' => array('if_query_other', 'query'),
'message' => 'REASON IS REQUIRED',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
);
I then have this custom function which is being called above.
function if_query_other(&$data, $check, $query_field) {
if($this->data[$this->name][$query_field] == 'Other' && $check == NULL)
{
return false;
}
else
{
return true;
}
}
It's not working, I am currently getting this error: Parameter 1 to Item::if_query_other() expected to be a reference, value given
CakePHP Version 2.3.6
Thanks
The error message is pretty clear, parameter 1 is passed by value, not by reference as your method signature requires. There is nothing to be passed by reference to a custom validation method, so simply remove the &, respectively remove the first parameter from the signature altogether.
The first parameter passed to a custom validation method will always be the data of the field to validate (in key => value format), followed by possible parameters defined in the rule array, like for example your fieldname. So $check would have never been null unless you would have defined null in the rule array, ie 'rule' => array('if_query_other', null), consequently your third parameter would have never been the fieldname.
Long story short, you need to define two parameters only, the first will contain the data of the field to validate, the second one the additional value defined in the rule array.
Here's an example, it checks whether the field passed in $query_field exists and whether its value equals Other, if it does it returns whether the value of the current field is not empty (I'm assuming the built-in Validation::notEmpty() is sufficient enough for your "not empty" check needs). If the value of the $query_field field doesn't equal Other, then this rule will always validate successfully, ie the value then isn't required to be not empty.
...
App::uses('Hash', 'Utility');
App::uses('Validation', 'Utility');
class Item extends AppModel
{
...
public function if_query_other($check, $query_field)
{
if(Hash::get($this->data[$this->alias], $query_field) === 'Other')
{
return Validation::notEmpty(current($check));
}
return true;
}
}

List node comments using existing comment.tpl.php from custom Drupal module

I have created a module which gets the comments from the nodes which the user has specified as 'favourites'. So I'm not trying to output all comments from all nodes like the recent comments block do, but just the ones from nodes specified as 'favourites'.
The queries all work, I've tested this by printing values from the different objects. So I've got the whole comment object for each comment and the corresponding node object. I've been able to create lists of the cid, nid, comment text etc. and output these with
$block['content'] = theme('item_list', array('items' => $items));
but how would I go about rendering the comment objects I've got in my module in the same layout/design as I have on my node pages? The comments on my node pages are rendered with the comment.tpl.php file which I set up with my own layout/design and I'd like my module to render these comments the same way.
So this is my hook_block_view() implementation which I believe is the correct way for output from a module:
function jf_comment_feed_block_view($delta = '') {
switch($delta){
case 'jf_comment_feed':
$block['subject'] = t('Comment feed');
if(user_access('access content')){
// Get users favourite locations
$loc_result = jf_comment_feed_locations();
$fav_loc = array();
foreach ($loc_result as $loc) {
$fav_loc[] = array(
'data' => $loc->nid,
);
}
if (empty($fav_loc)) { //No content in the last week.
$block['content'] = t('No favourite locations added.
To see what goes on at your favourite locations add locations to
+My Locations and the posts from those locations will show here.');
} else {
//Use our custom function to retrieve data.
$result = jf_comment_feed_contents($fav_loc);
// ############################################
// Here I need to create my output... I think...
// Previously I rendered my results from the query
// by using this code (this prints the comment id):
// $items = array();
// foreach ($result as $comment){
// $items[] = array(
// 'data' => comment_load($comment->cid),
// );
// }
// ############################################
if (empty($items)) { //No content in the last week.
$block['content'] = t('No posts from last week.');
} else {
// This is the code used to render the
// comment id to the block:
// $block['content'] = theme('item_list', array('items' => $items));
}
}
}
}
return $block;
}
I've also tried with:
$block['content'] = theme('comment_view', $mycomment, $mynode);
$block['content'] = theme('comment', $mycomment, $mynode);
where $mycomment is the comment object and $mynode is the node object. But this breaks the page.
Surely there must be a line of code I'm missing here, but I've now spent two days googling this and had no luck... So thanks for any help with this.
EDIT
#Clive did trigger some ideas and I tried creating my own array based on what the arrays look like on the node page. I got the structure and names for the array with the Devel Themer info module.
This array outputs the comments creators user pic and the date, but I've added a custom field, field_jf_comment, to my comments and this isn't showing, although I can see the information in the array with Devel. I don't use the standard out-of-the-box comment field because I wanted a textfield and not a scalable textarea for the input. A design decision.
Now obviously this isn't ideal as I set most of the values manually. This works for my current project, but would be cool if the module was a bit more generic so other people could use it too. When I click on a individual comment on my node page with Devel Themer info I get an array which has elements, the user object and array items such as db_is_active, is_admin among other things. If I could somehow recreate this array and then set this array to $block['content'] I believe this would work.
Here's the implementation of the array:
foreach ($result as $comment) {
$items[] = array(
'#entity_type' => 'comment',
'#bundle' => 'comment_node_location',
'#theme' => 'comment__node_location',
'#comment' => comment_load($comment->cid, FALSE),
'#node' => node_load($comment->nid),
'#view_mode' => 'full',
'field_jf_comment' => array(
'#theme' => 'field',
'#title' => 'Title',
'#access' => TRUE,
'#label_display' => 'above',
'#view_mode' => 'full',
'#language' => 'und',
'#field_name' => 'field_jf_comment',
'#field_type' => 'text',
'#entity_type' => 'comment',
'#bundle' => 'comment_node_location',
'#items' => array(
'0' => array(
// This isn't working and gives an error saying:
// Notice: Undefined property: stdClass::$field_jf_comment in
// jf_comment_feed_block_view()
'value' => $comment->field_jf_comment['und']['0']['value'],
'format' => $comment->field_jf_comment['und']['0']['format'],
'safe_value' => $comment->field_jf_comment['und']['0']['safe_value']
)
)
)
);
}
And I get it rendered with:
$block['content'] = $items;
EDIT
#Clive was right. His code does the same as mine, but in way less code. And with some modifications I managed to get my custom field in there too:
$content = '';
foreach ($items as $item) {
$single_comment = comment_load($item['cid']);
$custom_field = field_attach_view('comment', $single_comment, 'field_jf_comment');
$to_render = array(
'#theme' => 'comment',
'#comment' => $single_comment,
'#node' => node_load($item['nid']),
'field_jf_comment' => $custom_field
);
$content .= render($to_render);
}
$block['content'] = $content;
Now the only thing I'm missing is the links for each comment. The only one I'm using is the Reply to comment. Anyone got any idea of how to get that to show too?
The theme() calls probably break because you're using Drupal 7 but trying to pass the parameters in a Drupal 6 style. If you have a look at the theme_comment() documentation you can see it takes a single $variables parameter which should be an array. Try this:
$content = '';
foreach ($items as $item) {
$to_render = array(
'#theme' => 'comment',
'#comment' => comment_load($item['cid'])
);
$content .= render($to_render);
}
$block['content'] = $content;
The new Drupal 7 theme() syntax takes an array for its second argument. This array will be extracted before calling the template file so each key will become a new php var.
For example array( 'comment' => $mycomment ) will get you a $commentvariable in your template.
Hope this can help.

Resources