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.
Related
I am using form requests in Laravel for validation. I have noticed a pattern that emerges all the time and I couldn't find a solution for it on SE (or at least googling it didn't help me).
Let's say we are creating an API and we are using Laravel's apiResource to create the usual CRUD methods: store, update and delete
Obviously, when we are storing a new record, the field id is not required, but the rest of the fields might be required (and in most cases are). But when we are updating a record, we face the opposite situation. id is required while other fields are no longer required.
Is it possible to handle this situation with one form request in Laravel? Can we use Laravel's required_if in an intelligent way to avoid code duplication?
Edit: it doesn't have to be necessarily a Laravel solution. A solution that uses PHP would be fine too (as long as it is clean and follows SOLID principles).
I faced this problem lots of times and I understand your frustration...
From my point of view and professional experience, the best solution was all the time to have specific FormRequests for each case:
One for store with its own rules
Other for update with similar rules but not same as store
And last one for delete (for sure way less rules than the others and no duplication)
I know you said "no code duplication", but as it is right now, that is not possible (but you should not have code duplication as I stated before).
You said "as long as it is clean and follows SOLID principles", remember SOLID, S = Single Responsability, so if you want to solve this issue with a single FormRequest you are already breaking S. I cannot image a FormRequest with 10 or 15 inputs and those depends on if it is store, update, or delete. That is going to not be clean and for sure will not follow SOLID principles.
What about checking the method and then returning a set of rules based on that method? For instance, I have an InvoiceFormRequest with the following rules. So I'm able to use one form request for two different methods.
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
if ($this->isMethod('post')) {
return [
'template' => 'required',
'due_by_date' => 'required',
'description' => 'required',
'charge_items' => 'required',
];
}
if ($this->isMethod('put')) {
return [
'due_by_date' => 'required',
'description' => 'required',
'charge_items' => 'required',
];
}
}
Here are two possible solutions that I came up with
Use the controller method for returning the proper validation rules:
public function rules()
{
$method = $this->route()->getActionMethod();
switch($method){
case 'store':
return [
\\ validation rules
]
...
}
}
Use $this->getMethod() instead of $this->route()->getActionMethod() and validate by HTTP methods instead.
You could also store your validation rules in an array and manipulate it to reduce code duplication.
This resolves the issue of code duplication to a good extent, I think.
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
I'm working on a project that uses the Yii framework and CActiveRecord models.
In all of them there is something like this:
public function rules() {
return array(
//...
// The following rule is used by search().
// Please remove those attributes that should not be searched.
array(
'id, alias, code',
'safe', 'on'=>'search'
),
);
}
and a method called search().
The question - what is this for, and can I safely remove it if I don't use any of the Zii / Gii / whatever stuff? I also don't use forms and mass assignment.
I suspect this was auto-generated by Gii for whatever reason. I tried commenting it out and didn't notice any problems, but maybe I've broken something.
I also don't use forms and mass assignment.
Then yes you won't need it (but in any other case you will, so imo you might as well leave it there). Here is an article explaining it really well:
http://www.yiiframework.com/wiki/161/understanding-safe-validation-rules/
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.
I am using the symfony embedRelation method to embed forms.The code is like this:
public function configure(){
//......
$this->embedRelation('Foos as foos');
$this->getEmbeddedForm('foos')->mergePostValidator(new MenuValidatorSchema());
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
this does not work.
}
When embeding forms in Symfony, the top-level form keeps track of everything. The widget schema, validator schema, defaults, etc. of the embedded form are no longer directly used. You can see what's happening in sfForm::embedForm.
Note that in this case, since it's a post validator, it's perfectly acceptable to add it to the top-level form, i.e.:
$this->mergePostValidator(new MenuValidatorSchema());
If you want the validator schema on the embedded form and it has no current post validator, you can simply do:
$this->validatorSchema['foos']->setPostValidator(new MenuValidatorSchema());
If it has an existing one, you'll have to turn them into an sfValidatorAnd, doing something like:
$this->validatorSchema['foos']->setPostValidator(new sfValidatorAnd(array(
'validators' => array(
$this->validatorSchema['foos']->getPostValidator(),
new MenuValidatorSchema()
)
)));
The syntax of that last option is just one reason why setting post validators on the top-level form is the preferred option when available.