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]
);
}
Related
I'm trying to validate a field array, and I'd like to point out which field is wrong in the validation errors.
I have a form to upload multiple images. For each image, there must be a caption and the alt attribute for HTML. If I try to upload 3 images and miss the fields for two of them, I'll get an error message like the following:
The field 'image file' is required.
The field 'image caption' is required.
The field 'image alt' is required.
The field 'image caption' is required.
The field 'image alt' is required.
The field 'image file' is required.
The problem is that the :attribute is repeated and if the user wants to update multiple images he/she will have to check all of them to find which field is missing!
What I want is this:
The field 'image file (item 1)' is required.
The field 'image caption (item 1)' is required.
The field 'image alt (item 1)' is required.
The field 'image caption (item 3)' is required.
The field 'image alt (item 3)' is required.
The field 'image file (item 1)' is required.
So the user can know where to fix the problem.
First, I tried this:
$attributes = [
'img.*.file' => 'Image file (item :* )',
'img.*.alt' => 'Image alt (item :* )',
'img.*.caption' => 'Image caption (item :* )',
];
//
$this->validate($request, $rules, [], $attributes);
I supposed that the :* would be replaced by the index of the field (1, 2, 3, 4, etc) as the same way :attribute is replaced by the attribute. However, the :* is not replaced by the index of the fields; instead, it is displayed as plain text.
It worths to note that I designed the code in such way that the HTML name attribute is indexed sequentially for all fields (img[1][alt], [img][2][alt], etc, img[1][caption], [img][2][caption], etc), so each field has the right index. Having that in mind, I suppose there is a way to get the index and use to create custom attributes in the error messages.
I searched for this problem and found the same question here Validation on fields using index position, but it uses angular, not laravel.
How can I get the index and put it in the attribute?If that is not possible, is there any other way to accomplish the desirable result without having to set up the error messages?
I would like to change the attributes and keep the default error messages that laravel provides
Try this example
$input = Request::all();
$rules = array(
'name' => 'required',
'location' => 'required',
'capacity' => 'required',
'description' => 'required',
'image' => 'required|array'
);
$validator = Validator::make($input, $rules);
if ($validator->fails()) {
$messages = $validator->messages();
return Redirect::to('venue-add')
->withErrors($messages);
}
$imageRules = array(
'image' => 'image|max:2000'
);
foreach($input['image'] as $image)
{
$image = array('image' => $image);
$imageValidator = Validator::make($image, $imageRules);
if ($imageValidator->fails()) {
$messages = $imageValidator->messages();
return Redirect::to('venue-add')
->withErrors($messages);
}
}
Thanks to the user sss S, I could implement his/her ideas to solve the problem.
Here is the code for the store method. It replaces (item) with (item $i) in the error message, where $i is the index of the field. Therefore the user can know exactly where is the error.
public function store(Request $request)
{
$rules = $this->rules();
$attributes = $this->attributes();
$validator = Validator::make($request->all(), $rules, [], $attributes);
$errors = [];
if ($validator->fails()) {
$errors = $validator->errors()->all();
}
$imgRules = [
'file' => 'required|image|mimes:jpeg,jpg,webp,png|max:1999',
'alt' => 'required|string|max:100|min:5',
'caption' => 'required|string|max:100|min:5',
];
$imgAttributes = [
'alt' => 'HTML alt attribute (item)',
'caption' => 'Caption(item)',
'file' => 'image file (item)',
];
foreach($request->img as $i => $img) {
$imgValidator = Validator::make($img, $imgRules, [], $imgAttributes);
$imgErrors = [];
if($imgValidator->fails()) {
$imgErrors = $imgValidator->errors()->all();
}
foreach($imgErrors as $k => $v) {
$imgErrors[$k] = str_replace('(item)', "(item $i)", $v);
}
$errors = array_merge($errors, $imgErrors);
}
if(count($errors) > 0) {
return response()->json(['success' => false, 'errors' => $errors]);
}
// here is the code store the new resource
// ...
// then return success message
}
I use Kartik Editable input widget. I have a home model and tema model attribute here. Whenever I input and submit value in the field, the value won't change on-the spot but will only change after I refresh the page instead. What should I do? Thanks!
My controller :
public function actionIndex()
{
$searchModel = new HomeSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
// table only has one row
$model= Home::find()->one();
// Check if there is an Editable ajax request
if (isset($_POST['hasEditable'])) {
// use Yii's response format to encode output as JSON
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
// read your posted model attributes
if ($model->load($_POST)) {
// read or convert your posted information. Based on the 'name' property set in the view. So this 'tema' of $model-> tema comes from 'name' property set in the view.
$value = $model->tema;
$model->save();
// return JSON encoded output in the below format
return ['output'=>$value, 'message'=>'output berhasil'];
// alternatively you can return a validation error
// return ['output'=>'', 'message'=>'Validation error'];
}
// else if nothing to do always return an empty JSON encoded output
else {
return ['output'=>'', 'message'=>'output gagal'];
}
};
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
'model'=>$model,
]);
}
The view
<?php
echo Editable::widget([
'model' => $model,
'attribute' => 'tema',
'value'=>$model->tema,
/*'asPopover'=>'false',*/
'type' => 'post',
'header'=>'tema',
'valueIfNull'=>'value-nya NULL',
'format'=>'link',
'size'=> 'lg',
'inputType' => Editable::INPUT_TEXT,
'editableValueOptions' => ['class' => 'text-success h3']
]); ?>
Another issue, whenever I used 'asPopover'=>'false', it shows no error but nothing happen when I click the supposedly editable-input field. The editable-inline field just won't show up. When I use the popOver option,the pop-up just automatically triggered without clicking and also it pop-up on the top left corner of the page. Only after I clicked on the editable widget that triggered the pop-up will it recorrect itself to the proper position. Is it a bug? I used the latest Yii2 with bootstrap 4, and I had set the global parameter in params.php config with 'bsVersion' => '4.x', as in the documentation
In the Controller, try this:
public function actionIndex()
{
$searchModel = new HomeSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
if (isset($_POST['hasEditable']))
{
$tema = Yii::$app->request->post('editableKey');
$modelHome = Home::findOne($tema);
$posted = current($_POST['Home']);
$post = ['Home' => $posted];
if ($modelHome->load($post)) {
$modelHome->save();
$out = Json::encode(['output'=>$modelHome->tema, 'message'=>'']);
return $out;
}
return;
};
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
'model'=>$model,
]);
}
I want to validate a field called survey_id which is an input from user for uniqueness. It is working properly and giving the correct response when adding the new record, but when I tried to edit this record it is giving an error [unique] => Provided value already exist. So what I want is to exclude the survey_id of the current record from uniqueness check and if user input some other value for survey_id it should check for uniqueness search.
Currently I am using the CakePHP 3.0 validation with on create validation. Here is the validation rule that I am using:
validator
->requirePresence('survey_id', __('msg_required'))
->notEmpty('survey_id', __('msg_required'))
->maxlength('survey_id', 32, __('msg_maxlength'))
->add('survey_id', 'unique', ['rule' => ['validateUnique',['id']], 'provider' => 'table', 'message' => 'Provided value already exist', 'on'=>'create']);
return $validator;
Is there anything wrong with this code?
Thanks in advance.
`
It will work with this validation rule
$validator
->requirePresence('survey_id', __('msg_required'))
->notEmpty('survey_id', __('msg_required'))
->maxlength('survey_id', 32, __('msg_maxlength'))
->alphaNumeric('survey_id', __('msg_surveyid_format'))
->add('survey_id', 'custom', [
'rule' => function ($value, $context) {
if (!empty($context['data']['projectId'])) { $values = array($context['data']['projectId']); } else { $values = array(); }
$data = $this->getSurveyId($value, $values);
return (!empty($data)) ? false : true;
},
'message' => __('msg_surveyid_exsist')]);
return $validator;
}
public function getSurveyId($surveyId = null, $exclude = null) {
$where = array('p.survey_id' => $surveyId);
if (!empty($exclude) && is_array($exclude)) {
$where[] = array('p.id NOT IN' => $exclude);
}
return $this->db->newQuery()
->select('*')
->from(['p' => 'projects'])
->where($where)
->execute()
->fetch('assoc');
}
In my ContentsTable, I add an application rule like this, making sure at least one ImageEntity is added to the ContentEntity when saving:
public function buildRules(RulesChecker $rules) {
$rules
->add(function($entity, $options) {
return $entity->get('type') !== 'gallery' || count($entity->images) > 0;
}, 'validDate', [
'errorField' => 'images.0.image',
'message' => 'Lade mindestens ein Bild hoch!'
]);
return $rules;
}
The rule is applied, since the save() fails, but I would have expected the defined error message to appear on this form input in Contents/edit.ctp:
<?php echo $this->Form->input('images.0.image', [
'label' => 'Neues Bild (optional)',
'type' => 'file',
'required' => false
]); ?>
It is, however, not added at all.
How can I set the errorField option to add the error to this field?
Specifying paths isn't possible, also in the entity context the errors are expected to be present on an actual entity object, so even if it were possible to specifiy a path, there would be no entity to set the error on.
You could always modify the entity in the rule yourself, however I'm not overly convinced that this is such a good idea. Anyways, what you'd have to do, is to fill the images property with a list that contains one entity with the proper error on the image field, something along the lines of:
->add(function($entity, $options) {
if ($entity->get('type') !== 'gallery' || count($entity->images) > 0) {
return true;
}
$image = $this->Images->newEntity();
$image->errors('image', 'Lade mindestens ein Bild hoch!');
$entity->set('images', [
$image
]);
return false;
});
Another option, which to me feels a little cleaner, would be to set the error for the images property, and evaluate it manually in the form, ie something like:
->add(
function($entity, $options) {
return $entity->get('type') !== 'gallery' || count($entity->images) > 0;
},
'imageCount',
[
'errorField' => 'images',
'message' => 'Lade mindestens ein Bild hoch!'
]
);
if ($this->Form->isFieldError('images')) {
echo $this->Form->error('images');
}
See also
Cookbook > Database Access & ORM > Validation > Using Validation as Application Rules
Cookbook > Views > Helpers > Form > Displaying and Checking Errors
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();