Vallidation errors with saveAll in CakePHP - validation

I have a list of User records that I'm saving at one time (the form is populated from an uploaded CSV).
Everything seems to be working fine, and my validation rules are being honoured, but one thing that's eluding me are the specific validation error messages.
I get the general validation error (if say, a field that is supposed to be unique is not), but I'm not getting an error on the specific field to let the user know what the actual problem is.
Is this a bug, or is there something wrong with my Code? Here's the action from my Controller (fwiw I'm passing the CSV data to this action from another action, hence the snippet at the beginning):
public function finalizeCsv() {
if ( isset($this->request->params['named']['csvData']) ) {
$csvData = unserialize( $this->request->params['named']['csvData'] );
} else {
$csvData = $this->request->data;
}
$this->set('users', $csvData);
if ($this->request->is('get')) {
$this->request->data = $csvData;
}
if ($this->request->is('post') || $this->request->is('put')) {
if ($this->User->saveAll($this->request->data)) {
$this->Session->setFlash('Users added!', 'default', array('class' => 'success'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('There were errors with your data.');
}
}
}

For unique fields add a validation rule
'field_name' => array(
'rule' => 'isUnique',
'required' => 'create'
),
Next - when you get a error you will need to review $this->User->validationErrors. There will be many arrays inside - 1 for each created record. Loop through them, collect errors and send them to $this->Session->setFlash().
Also you better wrap $this->User->saveAll($this->request->data) into transaction if you're using InnoDB or other engine that supports transactions. On error - do rollback and try again.
$ds = $this->User->getDataSource();
$ds->begin();
// > saving here < //
$ds->commit(); //< on success
$ds->rollback(); //< on error

Related

Laravel validation: exclude size check when field not required

I have validation in Laravel application, but I can't seem to make it work.
I am making validation on Supplier model not request.
public function requestOpening(Supplier $supplier)
{
$validator = Validator::make($supplier->toArray(), $this->fullValidationRules());
if ($validator->fails()) {
return redirect("open-new/supplier/{$supplier->id}")
->withErrors($validator);
}
// ...
}
I want hfm_code to be required only when is_hfm == 1. I tried both:
'hfm_code' => 'size:6|exclude_if:is_hfm,0',
and
'hfm_code' => 'size:6|required_if:is_hfm,1',
In both scenarios I get validation error:
The hfm code must be 6 characters.
How do I ignore size check when is_hfm == 0 ?
The order seems to matter here:
'hfm_code' => 'exclude_if:is_hfm,0|size:6'
should work. I think this is because it evaluates rules in order and will ignore the remaining ones if exclude_if passes

How can I validate GET controller params in CakePHP 2?

Given this on the model:
public $validate = [
'amount' => array(
'rule' => array('comparison', '>=', 0),
'message' => 'You must buy over 0 of this item!'
)
];
How can I validate param #2 of the below?
public function buy(int $item, int $amount) {
Validation seems to be built only for POST, which I'd like to opt out of here.
First things first, modifying the database with GET requests is an anti-pattern for many different reasons. Even if you assume a friendly user agent (which you never should!), browsers can behave quirky and do unexpected stuff like for example sending GET request multiple times (that is perfectly valid as GET is not ment to modify data), which they usually won't for POST/PUT/DELETE.
I would strongly suggest to change your endpoint to handle POST requests instead.
That being said, you can generally validate whatever you want, the validation mechanisms first and foremost just validate data, they don't know or care where it stems from. You can hand over whatever data you want to your model, and let it validate it:
$data = array(
'item' => $item,
'amount' => $amount,
);
$this->ModelName->set($data);
if ($this->ModelName->validates()) {
// data is valid
} else {
// data is invalid
$errors = $this->ModelName->validationErrors;
}
Moreover you can use CakePHP's validation methods completely manually too:
App::uses('Utility', 'Validation');
$isValid = Validation::comparison($amount, '>' 0);
This example of course doesn't make too much sense, given that $isValid = $amount > 0 would do the same, however it should just show that you can validate anything everywhere without models being involved.
See also
Cookbook > Models > Data Validation > Validating Data from the Controller
Cookbook > Models > Data Validation > Core Validation Rules

CakePHP 3.x unique validation not working for saving multiple records

I have a Questions table which has a validation like :
$validator
->notEmpty('title')
->add('title', [
'unique' => [
'rule' => [
'validateUnique',
['scope' => ['subject_id', 'chapter_id']]
],
'provider' => 'table'
]
]);
I want to save following records into my table at a time.
Array
(
[0] => Array
(
[subject_id] => 1
[chapter_id] => 4
[title] => What is a .ctp file used for in CakePHP?
)
[1] => Array
(
[subject_id] => 1
[chapter_id] => 4
[title] => What is a .ctp file used for in CakePHP?
)
)
I try to save it using saveMany() method. It save both records i.e. validation not working. I also try following code for transactional() method instead of saveMany() method, but validation also not working.
$entities = $this->Questions->newEntities($records);
$this->Questions->connection()->transactional(function () use ($entities) {
foreach ($entities as $entity) {
$this->Questions->save($entity);
}
});
My validation works fine if I save the records one by one using save() method or my records already saved in database. Why my unique validation not working for saveMany() and also for transactional() for duplicate new entities?
Validation happens before saving
Validation happens before saving, so that behavior is to be expected, given that the rule looks up the database, and not the request data (it would only have access to one set of data at the time anyways), ie, no matter how many datasets are being tested, none of the submitted ones will be saved yet, and therefore validation will pass unless a matching record already exists in the database.
So either create/patch and save all entities one by one in a custom transaction (and don't forget to add some proper failure checks),
$this->Questions->connection()->transactional(function () {
foreach ($this->request->data() as $set) {
$entity = $this->Questions->newEntity($set); // < validaton is being applied there
if (!$this->Questions->save($entity)) { // < not there
return false;
}
}
return true;
});
or use application rules instead.
Application rules are being applied in the saving process
Application rules are being applied in the actual saving process, ie upon calling Table::save(), so to avoid the hassle of using custom transactions, and generally to have a last line of defense, use them instead of/additionally to validation.
// QuestionsTable class
public function buildRules(\Cake\ORM\RulesChecker $rules)
{
$rules->add($rules->isUnique(['title', 'subject_id', 'chapter_id']));
// ...
return $rules;
}
See also
Cookbook > Database Access & ORM > Database Basics > Using Transactions
Cookbook > Database Access & ORM > Validation > Applying Application Rules
Cookbook > Database Access & ORM > Validation > Creating Unique Field Rules
Cookbook > Database Access & ORM > Validation > Validation vs. Application Rules

how can I return the validation errors for a REST call?

I am making a REST api for my application. It almost works, but I want it to return the validation errors when they occurs.
With CakePHP 2.x I see there was an invalidFields method, but it's not there anymore with Cake3. I see, instead, that there is an errors method in the Form class, but I don't know how can I use it.
This is what I have in my Users/json/add.ctp file:
<?php
$echo = [
'errors' => $this->Form->errors(),
'user' => $user
];
echo json_encode($echo);
As I said it doesn't work (I know that probably Form is used out of its scope here, but I don't even know if I am on the right path)...
This is the result:
{
"message": "Missing field name for FormHelper::errors",
"url": "\/fatturazione\/api\/users.json",
"code": 500
}
What is the correct way to display all the form errors when they occour?
This is what I would do, check for validation-errors in your controller and send them to the view serialized
public function add() {
$this->request->allowMethod('post');
$data = $this->Model->newEntity($this->request->data);
if($data->errors()) {
$this->set([
'errors' => $data->errors(),
'_serialize' => ['errors']]);
return;
}
//if no validation errors then save here
}

How to avoid errors in saving data - Cakephp

I'm using Cakephp and trying to put in a method to make sure our reservation system doesn't let two users book the same appointment. Ex. User 1 opens the appointment, and User 2 opens it simultaneously. User 1 books the appointment. User 2 tries to book it but the system checks and sees it is no longer available.
I imagine this would take place in validation, or in a beforeSave(), but can't figure out how to do it.
Right now I made a function in the model to call from the controller. In the controller I have:
if ($this->Timeslot->checkIfNotAvailable()) {
$this->Session->setFlash('This timeslot is no longer available');
$this->redirect(array('controller' => 'users', 'action' => 'partner_homepage'));
}
and in the model I have this function:
function checkIfNotAvailable($data) {
$this->recursive = -1;
$timeslot = $this->find('all', array(
'conditions' => array(
'Timeslot.id' => $this->data['Timeslot']['id'])
)
);
if ($timeslot['student_id'] == 0) {
//They can reserve it, do not spring a flag
return false;
} else {
//Throw a flag!
return true;
}
}
I think I'm mixed up using custom validation when it's not called for. And it's not working obviously. Any suggestions?
Thanks!
If what you have is working, you can stick with it, you could also try creating a beforeValidate() call back function in your Model.
class YourModel extends AppModel {
function beforeValidate(){
if( !$this->checkIfNotAvailable( $this->data ) ) {
unset($this->data['YourModel']['time_slot']);
}
return true; //this is required, otherwise validation will always fail
}
}
This way you remove the time_slot before it goes to validation and it will drop a validation error at that point, kicking the user back to the edit page and getting them to pick a different time slot, ideally the updated data entry page will no longer have the used time slot available.

Resources