I'm having some difficulties validating a I18N field in CakePHP3.
The translate behavior is setup like this:
$this->addBehavior('Translate', [
'fields' => ['name', 'body', 'slug'],
'validator' => 'default'
]);
Like advertised here: https://book.cakephp.org/3.0/en/orm/behaviors/translate.html#validating-translated-entities
The core validation is working properly. I have a validation rule in validationDefault function that checks if name is not empty and it works fine. Though, I also would like to add some application rules on top of this validation. The "name" field should have a unique value. I don't want to allow multiple entities with the same translated name.
This piece of code apparently doesn't work. CakePHP docs also are quite silent about this matter.
public function buildRules(RulesChecker $rules) {
// prevent duplicate creation
$rules->add($rules->isUnique(['name']));
return $rules;
}
Is this actually possible?
Thanks
What you are doing there is creating a rule for the name field on the main model, this won't affect translations. There is no built-in functionality for that, the behavior only assists with validation rules by making use of the validationTranslated() method in case it exists on your model class, it won't help with application rules.
You'd have to create a custom application rule that checks the translation table, by matching against the field, locale, model and content fields, something along the lines of this:
$rules->add(
function (EntityInterface $entity) {
$behavior = $this->behaviors()->get('Translate');
$association = $this->association($behavior->getConfig('translationTable'));
$result = true;
foreach ($entity->get('_translations') as $locale => $translation) {
$conditions = [
$association->aliasField('field') => 'name',
$association->aliasField('locale') => $locale,
$association->aliasField('content') => $translation->get('name')
];
if ($association->exists($conditions)) {
$translation->setErrors([
'name' => [
'uniqueTranslation' => __d('cake', 'This value is already in use')
]
]);
$result = false;
}
}
return $result;
}
);
Note that this uses the association object rather then the target table, this will ensure that further conditions like the model name are being applied automatically.
Also this requires to set the errors on the entity manually, as they are nested, which isn't supported by the rules checker, currently it can only set errors on the first level entity (see the errorField option).
It should also be noted that it would be possible to modify the rules checker for the translation table association (via the Model.buildRules event), however this would result in the errors being set on new entities that will be put in a separate property (_i18n by default) on the main entity, where the form helper won't find the error, so one would then have to read the error manually, which is a little annoying.
See also
Cookbook > Database Access & ORM > Validation > Applying Application Rules
Related
I cannot get the validation to work properly when updating entity data. The validation does not work after changing the initial data. The code below provides an example:
// in controller
$user = $this->Users->newEntity([
'mail' => 'wrong',
'password' => 'password',
'auth_level' => 0,
]);
debug($user->getErrors()); // Will show error: wrong email format
$user->mail = "correct#correct.correct";
debug($user->getErrors()); // Will show no errors
$user->mail = "wrong";
debug($user->getErrors()); //Will show no errors
if (TableRegistry::get('users')->save($user)) {
// This actually executes
}
My validation rule in the model is as follows:
public function validationDefault(Validator $validator): Validator
{
$validator
->email('mail')
->add('mail', 'unique',
[
'on' => ['create', 'update'],
'rule' => 'validateUnique',
'provider' => 'table',
'message' => "Email already in use"
]
);
return $validator
}
I tried creating rules with "on" => "update", but it does not help.
What I am trying to achieve is to get an entity, change the email address and save it back to database. Upon saving, the email field is neither validated for format nor uniqueness.
For the sake of completeness.
There is a difference between Application Rules and Validation Rules.
Validation Rules validate data typically coming from user's input (newEntity(), patchEntity()). Entity is regenerated. Baked ones are in "validationDefault" function within Tables.
Application Rules establish some rules for data modified within application code which is expected to be 'safe' (setters). Entity is not regenerated. Baked ones are in "buildRules" function within Tables.
"save()" function won't go through validation rules, but through application rules.
When saving data that needs to pass through validation rules because it's assigned/set within the application but it's data coming from user's input, make sure you use patchEntity().
More info: https://github.com/cakephp/cakephp/issues/6654
The solution is to always update entity data with
$model->patchEntity($entity, $newdata)
I use AuthComponent with CakePHP 3.8 and now I need to do some logic in Model buildRules method but for this I need to get the current user ID.
Is there any way to pass/retrieve it without using hacks such as accessing directly from the session.
I know that it is possible to pass id via validator from controller as described in CakePHP's documentation
https://book.cakephp.org/3/en/core-libraries/validation.html#using-custom-validation-rules
And it works for validation, however, I am unable to access the validator from the inside of build rules.
When I do as described in here, I get an empty object.
https://book.cakephp.org/3/en/orm/validation.html#using-validation-as-application-rules
It seems that I am able to attach new validation rules but unable to retrieve the "Passed" provider to get the User ID.
It seems a trivial thing but a I spent quite a few hours trying to get the id in a proper way.
OK, After working a bit more, I found how to retrieve user_id inside build rules. Might be helpful to someone.
Do this in the controller
$this->ExampleModel->validator('default')->provider('passed', [
'current_user' => $this->Auth->user('id')
]);
And then put this in you buildRules method
public function buildRules(RulesChecker $rules)
{
$user_id = $this->validator('default')->provider('passed')['current_user'];
$rules->add(
function ($entity, $options) use($user_id) {
//return boolean for pass or fail
},
'ruleName',
[
'errorField' => 'some_id',
'message' => 'Some ID field is inconsistent with App logic.'
]
);
return $rules;
}
The proper way to handle this is usually using either events, or saving options.
For example to make the ID available for the application rules of all tables, you could do something like this:
$this->getEventManager()->on('Model.beforeRules', function (
\Cake\Event\Event $event,
\Cake\Datasource\EntityInterface $entity,
\ArrayObject $options
) {
$options['current_user'] = $this->Auth->user('id');
});
It would be available in the $options argument of the rule accordingly, ie as $options['current_user'].
For a specific save operation you can pass it to the options of the save() call:
$this->ExampleModel->save($entity, [
'current_user' => $this->Auth->user('id')
]);
There's also plugins that can help you with it, for example muffin/footprint.
Some form fields in my laravel 5.5 application have validation rules that run against a remote API and take quite some time. I would thus only want to run these expensive checks when the field value changes (is different from the value currently stored in the model).
Is there something that implements this already, e.g. as a rule similar to sometimes?
I would image it like this: only_changed|expensive_validation|expensive_validation2. The latter rules would only be executed when the field value has changed.
Assuming you are using a custom Request class, the rules() method expects an associative array to be returned before it applies the validation.
You can build your array of rules dynamically using the request contents before applying the validation like so:
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
$validation_array = [
'name' => 'required|string|max:255',
];
if ($this->my_field === $some_condition) {
$validation_array = array_merge($validation_array. [
'my_field' => "required|expensive_validation|expensive_validation2"
]);
}
return $validation_array;
}
Note: I haven't run this yet but the principle should be fine.
I want to know if there is a easy way to mass assign a new set of rules for a model.
The use case is that I could have a validator rule which contains a set of sub rules for a specific model. I want to dynamically load that model, assign its attributes (both of which I know how to do) and then mass assign the set of rules. The rules will look like:
'rules' => array(
array('road', 'string'),
array('town', 'string'),
array('county', 'string'),
array('post_code', 'string'),
array('telephone', 'integer')
)
I know I can do this by picking out the classes individually and building up the validators manually but is there any easy way to just tell a Yii model to reload the validators with this specification?
I actually found out the answer in the end through a issue ( https://github.com/yiisoft/yii/issues/987#issuecomment-8886072 ) on Github whereby it was mentioned to look at the CModel validatorList. After browsing this source code for a while I came up with the following piece of code, mostly ripped from CModel itself:
$c=new EMongoModel();
foreach($this->rules as $rule){
if(isset($rule[0],$rule[1])) // attributes, validator name
$c->validatorList->add->add(CValidator::createValidator($rule[1],$this,$rule[0],array_slice($rule,2)));
else
throw new CException(Yii::t('yii','{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.',
array('{class}'=>get_class($this))));
}
Now this allows me to take a list of array elements that look like validation rules for a model and on the spot actually make them into validation rules for the model.
I am trying to register a custom validation rule but it does not seem to work. I need either of 2 fields to be filled in. One is a URL(link) field and other is a File input(file_upload).
Here is my custom validation:
Validator::register('file_check', function($attribute, $value, $parameters) {
if (!trim($value) == "" || array_get(Input::file($parameters[0]), 'tmp_name')) {
return true;
}
return false;
});
$messages = array(
'file_check' => 'Please upload a file or provide a link to files.',
);
$rules = array(
'link' => 'url|file_check:file_upload',
'file_upload' => 'mimes:jpg,jpeg,gif,png,psd,ai,bmp,xls,xlsx,doc,docx,zip,rar,7z,txt,pdf'
);
$validation = Validator::make(Input::all(), $rules, $messages);
if ($validation - > fails()) {
return Redirect::to('page') - > with_errors($validation - > errors) - > with_input();
}
Need help :)
EDITED
Also, I just noticed that the validation rule should accept "PSD" files but when I try to upload a PSD file it redirects with the error "Invalid file type".
I am maybe late in party but may be somebody will find it useful, in case you need to create implicit rule which will be called even if field is not present in Input (like required,required_if....) use
Validator::extendImplicit( 'validator_name', function($attribute, $value, $parameters)
{
});
Check this out
I was just struggling with this myself! It turns out that except when a few specific rules are applied to them, Laravel doesn't pass empty fields through the Validator at all. So a custom either-this-or-that rule can't work, since at least one of the two fields is likely to not be visible to it.
You can get around this by moving from the registering-a-new-rule approach to the alternate extend-the-Validator-class approach. Your new class will inherit all the methods of the standard Validator, including a method called "implicit" (you can find the original on line 215 of the standard Validator class), which specifies a whitelist of rules that Laravel should pass fields along to even if they are empty. Add your new rule to that list and you should be good to go.
Jason is right, but there is one thing that can be confusing.
Laravel's 'registering a new rule' approach uses the syntax 'Validator::extend(...'. As described elsewhere, this is convenient when you want to customize in a special situation. However, if you want to add a number of reusable rules, then you probably want to use the extend-the-Validator-class approach. In that case, IF you have a rule conditionally requires something, you need to override the existing implicitRules array with a new one adding your rule.
If the first rules you add don't conditionally require, you will think you have it nailed, then you will spend hours trying to figure out why your new 'RequireWhenBlaBla...' rule is invisible.