Prevent multiple validation errors in Symfony - php

$this->createFormBuilder(null, array(
'validation_constraint' => new Collection(array(
'randominput' => array(
new NotBlank(),
new Email(),
new MyCustomConstraint()
)
))
->add('randominput', 'text');
Submit result (with required attribute removed from html, with firebug):
The message from Email() constraint does not apper because inside that constraint exists a piece of code, witch I think is a clone/hardcode for NotBlank() constraint
if (null === $value || '' === $value) {
return;
}
I think the Email() constraint should be a child of NotBlank()...
I want MyCustomConstraint() to not be executed if NotBlank() founds a violation. So it will be good if will be some option, for example "breakNextConstraintExecutionOnFirstViolation" => true. So if I set 10 constraints for one field, and 3rd constraint sets a violation, then next 7 constraints will not be executed.
If that kind of logic/option does not exists in symfony2, it will be good if I can access the 'validation' service from MyCustomConstraint class and reuse existing constraints but not write hardcode for each new constraint:
class MyCustomConstraintValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
// use the validator to validate the value, not hardcode
if (count( $this->get('validator')->validateValue(
$value,
new NotBlank()
)) > 0)
{
return;
}
$this->context->addViolation('MyCustomConstraint Message...');
}
}
So my question is: What should I do to prevent multiple violation messages for one filed and do not use in every new constraint hardcode?
P.S. In my previous projects (not on symfony), I made forms showing only one error message. So user completes fields one by one and see only one error but not submitting the form and that fills every fields with red errors (and scares some users). But now at least I want to resolve this issue.

Related

Symfony 5 - Forms and Entities - NotBlank constraint with not nullable string entity attribute

I create a form to edit an existing object in database.
One of the field is a string, and cannot be blank or empty.
In order to display a validation error message if the value is blank at form submit, I add the following NotBlank constraint into the form type class:
...
->add(
'firstname',
null,
[
'label' => 'user.firstname',
'constraints' => [
new Assert\NotBlank()
]
]
)
...
I also specify the attribute type (string) into the entity:
...
private string $firstname;
...
public function setFirstname(string $firstname) {
$this->firstname = $firstname;
}
...
public function getFirstname(): string {
return $this->firstname;
}
When I submit the form with a blank value, I expect to see a normal form validation error message telling me the value cannot be blank. Instead, I get the following Symfony error:
Expected argument of type "string", "null" given at property path "firstname".
If I remove the type string from the entity attribute declaration, then I do not get that Symfony error anymore and the validation error message is displayed as expected.
Is there anyway to keep declaring attributes types while applying a NotBlank constraint on it ?
I have noticed that unexpected behavior only occurs when I use a form to edit an existing object but not when I create a brand new object in database. In that last case, setting a blank value with a NotBlank constraint and declaring the attribute type into the entity (private string $firstname) does not cause any issue and the form validation error message (telling the field cannot be blank) is displayed correctly.

Laravel Validation: only allow known properties/attributes, otherwise fail validation

We are building an api endpoint where precision is required. We want to enforce strict validation on the parameters that are POST/PUT to the server.
If the api user sends a key=value pair that is not supported (eg. we allow the parameters [first_name, last_name] and the user includes an unsupported parameter [country]), we want the validation to fail.
Have tried building a custom validator called allowed_attributes (used as allowed_attributes:attr1,attr2,...), but for it to be usable in a $validationRules array, it has to be applied to the parent of a list of nested/child attributes (...because otherwise our custom validator did not have access to the attributes being validated).
Validator::extend('allowed_attributes', 'App\Validators\AllowedAttributesValidator#validate');
This created issues with other validators, where we then had to anticipate this parent/child structure and code around it, including additional post-validation clean-up of error keys and error message strings.
tl;dr: very dirty, not a clean implementation.
$validationRules = [
'parent' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
$isValid = Validator::make(['parent' => $request], $validationRules);
var_dump("Validation results: " . ($isValid ? "passed" : "failed"));
Any ideas/suggestions on how this can be accomplished more cleanly in laravel, without requiring the use of parent/child relationship to get access to the list of all $request attributes (within the custom validator)?
I preferred to post a new answer as the approach is different from the previous one and a bit more cleaner. So I would rather keep the two approaches separated and not mixed together in the same answer.
Better problem handling
After digging deeper into the Validation's namespace's source code since my last answer I figured out that the easiest way would have been to extend the Validator class to remplement the passes() function to also check what you needed.
This implementation has the benefit to also correcly handle specific error messages for single array/object fields without any effor and should be fully compatible with the usual error messages translations.
Create a custom validator class
You should first create a Validator class within your app folder (I placed it under app/Validation/Validator.php) and implement the passes method like this:
<?php
namespace App\Validation;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator as BaseValidator;
class Validator extends BaseValidator
{
/**
* Determine if the data passes the validation rules.
*
* #return bool
*/
public function passes()
{
// Perform the usual rules validation, but at this step ignore the
// return value as we still have to validate the allowance of the fields
// The error messages count will be recalculated later and returned.
parent::passes();
// Compute the difference between the request data as a dot notation
// array and the attributes which have a rule in the current validator instance
$extraAttributes = array_diff_key(
Arr::dot($this->data),
$this->rules
);
// We'll spin through each key that hasn't been stripped in the
// previous filtering. Most likely the fields will be top level
// forbidden values or array/object values, as they get mapped with
// indexes other than asterisks (the key will differ from the rule
// and won't match at earlier stage).
// We have to do a deeper check if a rule with that array/object
// structure has been specified.
foreach ($extraAttributes as $attribute => $value) {
if (empty($this->getExplicitKeys($attribute))) {
$this->addFailure($attribute, 'forbidden_attribute', ['value' => $value]);
}
}
return $this->messages->isEmpty();
}
}
This would essentially extend the default Validator class to add additional checks on the passes method. The check compute the array difference by keys between the input attributes converted to dot notation (to support array/object validation) and the attributes which have at least one rule assigned.
Replace the default Validator in the container
Then the last step you miss is to bind the new Validator class in the boot method of a service provider. To do so you can just override the resolver of the Illuminate\Validation\Factory class binded into the IoC container as 'validator':
// Do not forget the class import at the top of the file!
use App\Validation\Validator;
// ...
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$this->app->make('validator')
->resolver(function ($translator, $data, $rules, $messages, $attributes) {
return new Validator($translator, $data, $rules, $messages, $attributes);
});
}
// ...
Pratical use in a controller
You don't have to do anything specific to use this feature. Just call the validate method as usual:
$this->validate(request(), [
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
]);
Customize Error messages
To customize the error message you just have to add a translation key in your lang file with a key equal to forbidden_attribute (you can customize the error key name in the custom Validator class on the addFailure method call).
Example: resources/lang/en/validation.php
<?php
return [
// ...
'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',
// ...
];
Note: this implementation has been tested in Laravel 5.3 only.
It should work for simple key/value pairs with this custom validator:
Validator::extendImplicit('allowed_attributes', function ($attribute, $value, $parameters, $validator) {
// If the attribute to validate request top level
if (strpos($attribute, '.') === false) {
return in_array($attribute, $parameters);
}
// If the attribute under validation is an array
if (is_array($value)) {
return empty(array_diff_key($value, array_flip($parameters)));
}
// If the attribute under validation is an object
foreach ($parameters as $parameter) {
if (substr_compare($attribute, $parameter, -strlen($parameter)) === 0) {
return true;
}
}
return false;
});
The validator logic is pretty simple:
If $attribute doesn't contains a ., we're dealing with a top level parameter, and we just have to check if it is present in the allowed_attributes list that we pass to the rule.
If $attribute's value is an array, we diff the input keys with the allowed_attributes list, and check if any attribute key has left. If so, our request had an extra key we didn't expect, so we return false.
Otherwise $attribute's value is an object we have to check if each parameter we're expecting (again, the allowed_attributes list) is the last segment of the current attribute (as laravel gives us the full dot notated attribute in $attribute).
The key here is to apply it to validation rules should like this (note the first validation rule):
$validationRules = [
'parent.*' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
The parent.* rule will apply the custom validator to each key of the 'parent' object.
To answer your question
Just don't wrap your request in an object, but use the same concept as above and apply the allowed_attributes rule with a *:
$validationRules = [
'*' => 'allowed_attributes:first_name,last_name',
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
];
This will apply the rule to all the present top level input request fields.
NOTE: Keep in mind that laravel validation is influenced by order of the rules as they are putted in rules array.
For example, moving the parent.* rule on bottom will trigger that rule on parent.first_name and parent.last_name; as opposed, keeping it as the first rule will not trigger the validation for the first_name and last_name.
This means that you could eventually remove the attributes that has further validation logic from the allowed_attributes rule's parameter list.
For example, if you would like to require only the first_name and last_name and prohibit any other field in the parent object, you might use these rules:
$validationRules = [
// This will be triggered for all the request fields except first_name and last_name
'parent.*' => 'allowed_attributes',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
But, the following WON'T work as expected:
$validationRules = [
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40',
// This, instead would be triggered on all fields, also on first_name and last_name
// If you put this rule as last, you MUST specify the allowed fields.
'parent.*' => 'allowed_attributes',
];
Array Minor Issues
As far as I know, per Laravel's validation logic, if you were up to validate an array of objects, this custom validator would work, but the error message you would get would be generic on the array item, not on the key of that array item that wasn't allowed.
For example, you allow a products field in your request, each with an id:
$validationRules = [
'products.*' => 'allowed_attributes:id',
];
If you validate a request like this:
{
"products": [{
"id": 3
}, {
"id": 17,
"price": 3.49
}]
}
You will get an error on product 2, but you won't be able to tell which field is causing the problem!

CakePHP 3 - Cake trying to validate notEmpty for some reason

My table is throwing a default "notEmpty" validation error, even though I have not written any validation of the sort.
Basic validation in my Table class:
public function validationDefault(Validator $validator)
{
return $validator->requirePresence('my_field', 'create', 'Custom error message');
}
Data being set:
['my_field' => null]
As far as I can tell from the docs, this should not fail validation.
Key presence is checked by using array_key_exists() so that null values will count as present.
However, what is actually happening is that validation is failing with a message:
'my_field' => 'This field cannot be left empty'
This is Cake's default message for the notEmpty() validation function, so where is it coming from? I want it to allow the null value. My database field also allows NULL.
Edit
I have managed to solve the issue by adding allowEmpty() to the validation for that field. This would, therefore, seem to show that Cake assumes that if your field is required you also want it validate notEmpty() by default, even if you didn't tell it so.
This directly contradicts the documentation line I showed above:
Key presence is checked by using array_key_exists() so that null values will count as present.
So does the documentation need to be updated, or is it a bug?
Although it is not mentioned in the Cake 3 documentation, required fields are not allowed to be empty by default, so you have to explicitly state that the field is required and allowed to be empty.
public function validationDefault(Validator $validator)
{
return $validator
->requirePresence('my_field', 'create', 'Custom error message')
->allowEmpty('my_field', 'create');
}
This had me stumped for a while. The default behaviour is not at all intuitive. Here's some code that applies conditional validation on an array of scenarios coded as [ targetField, whenConditionalField, isConditionalValue ]
public function validationRegister()
{
$validator = new Validator();
$conditionals = [ ['shipAddress1','shipEqualsBill','N'], ['shipTown','shipEqualsBill','N'], ['shipPostcode','shipEqualsBill','N'] ];
foreach($conditionals as $c) {
if (!is_array($c[2])) $c[2] = [$c[2]];
// As #BadHorsie says, this is the crucial line
$validator->allowEmpty($c[0]);
$validator->add($c[0], 'notEmpty', [
'rule' => 'notEmpty',
'on' => function ($context) use ($c) {
return (!empty($context['data'][$c[1]]) && in_array($context['data'][$c[1]], $c[2]));
}
]);
}
return $validator;
}
So, in this case, if the user selects that the Shipping Address is not the same as the Billing Address, various shipping fields must then be notEmpty.

Sonata Admin Bundle and Doctrine issue: how to update an entity from inversed side?

I have a many-to-many relationship between two entities. Let's call those User And Group.
I've decided that onto creation/update interface, because they could be associated, you can directly associate users from group form or groups from user form. Notice that the owning side of relation is User
Now comes the issue. If I associate groups from user form interface, all is good and works perfectly (doctrine looks for changes into owning side). If I try to associate User from group form interface, nothing works.
Obviously I perfectly know that I have to "add" user(s) into group object and add group (this) to every user(s) object that I passed down from form. In fact this is my snippet of code into Group entity
public function setUsers(\Doctrine\Common\Collections\ArrayCollection $utente)
{
/* snippet of code for removing old association , didn't reported */
foreach($utente as $u){
$this->users[] = $u;
$u->addGroups($this);
}
}
Into creation form this snippet do well his job. Into update, it doesn't.
So I suppose that this must be a sonata issue or something that, at the moment, I missed.
Any advice?
UPDATE
After some time spent to understand what's going on here, I just find that setUser() isn't called into update operation (read as submit a form builded onto an existent entity). So my code runs only when I create new ones entry ( I still haven't a solution )
I've just find out how to update the entity.
I suppose that is a Symfony2 related behaviour and not a Sonata Admin one.
In a nutshell, you have to tell Symfony2 to call the setter of the object that you want to update.
For that use:
by_reference => false
In Sonata Admin Bundle case:
$formMapper
->add('nome')
->add('canali', 'sonata_type_model', array('required' => false))
->add('utenti', 'sonata_type_model', array('required' => false,
'by_reference' => false))
;
In pure Symfony2 Form case:
->add('utenti', 'collection', array(
'type' => new User(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
))
;
Unclear what you mean that for update it does not do job well. Based on your code I assume it adds new relations between users and groups but do not remove old.
public function setUsers(\Doctrine\Common\Collections\ArrayCollection $utente)
{
// to synch internal and external collections, remove relation between users and groups if user is not in the new collection
foreach ($this->users as $u) {
if (!$utente->contains($u)) {
// add this function to user object, it's trivial, just remove give group from internal groups collection
$u->removeGroup($this);
}
}
foreach ($utente as $u) {
if (!$this->users->contains($u)) {
$this->users[] = $u;
$u->addGroups($this);
}
}
}
Try to add the following to your admin class
public function prePersist($group)
{
$group->setUsers($group->getUsers());
parent::prePersist($testQuestion);
}
public function preUpdate($group)
{
$group->setUsers($group->getUsers());
parent::preUpdate($testQuestion);
}

Symfony 1.4 conditional validation

I have a Profile form that inherits from sfGuardRegisterForm
I have these fields:
$this->useFields(
array('first_name',
'last_name',
'email_address',
'country',
'language',
'current_password',
'new_password',
'password_again',
)
);
Required fields are:
email_address, country and language
And the conditions are:
If the email_address is not equal with the current email_address
then check if it's unique then save it
If the current_password is the actual password of the user then verify if new_password and password_again are equals and verify that the new_password is not equal to the actual password of the user
I just can't figure out in how implement this
EDIT
Thanks 1ed your example works but the problem is that I load the user Profile and I fill the fields: 'first_name', 'last_name', 'email_address', 'country', 'language' with the actual logged user so the email_address field will show the email address:
//...
$this->widgetSchema['email_address']->setDefault($this->_user->getEmailAddress());
//...
If the user dont change the email it will always show this message:
An object with the same "email_address" already exist.
I just want to skip that
Also this $this->getObject()->checkPassword() does not works, always show this message:
Incorrect current password.
I use:
$this->_user = sfContext::getInstance()->getUser()->getGuardUser();
To get actual user profile
EDIT2
Thanks again 1ed
This is very weird and I'm getting frustated, this is the situation
I have a "workaround" for this but it does not follow the standard, I can make it works but using sfContext::getInstance()->getUser()->getGuardUser(); and it will be more unnecesary code
If I use new ProfileForm($user) automatically fills all the fields, that's very good but I can't setDefault() I can't set null or empty any field so I can't use doUpdateObject() because this function only works when the current data is updated, also I have tested overriding bind(), save() etc. without results
email_address uniqueness: you should set unique: true in schema, in sfDoctrineGuardPlugin that's the case by default, so in BasesfGuardUserForm you should see a unique validator already: sfValidatorDoctrineUnique(array('model' => 'sfGuardUser', 'column' => array('email_address'))
current_password: you should create a callback type post validator for this
// in sfGuardRegisterForm::configure()
// these fields can be blank
$this->getValidator('current_password')->setOption('required', false);
$this->getValidator('new_password')->setOption('required', false);
$this->getValidator('password_again')->setOption('required', false);
// check the current password (this validator is not `required` by default)
$this->mergePostValidator(new sfValidatorCallback(array(
'callback' => array($this, 'checkPassword'),
), array(
'invalid' => 'Incorrect current password.'
)));
// add this method to the same form class
public function checkPassword(sfValidatorBase $validator, array $values, array $arguments)
{
// if a new password is given check whether the old one is correct or not and rise an error if not correct
if(0 != strlen($values['new_password']) && !$this->getObject()->checkPassword($values['current_password']))
{
throw new sfValidatorErrorSchema($validator, array(
'current_password' => new sfValidatorError($validator, 'invalid')
));
}
return $values;
}
Alternatively you can create a custom post validator, but I think it's not necessary.
EDIT:
If you would like to display empty email address just like the password fields add these to your form class:
// at form load remove the default value
protected function updateDefaultsFromObject()
{
parent::updateDefaultsFromObject();
if (isset($this['email_address']))
{
$this->setDefault('email_address', '');
}
}
// before save remove empty email address
protected function doUpdateObject($values)
{
if (isset($values['email_address']) && 0 == strlen($values['email_address']))
{
unset($values['email_address']);
}
parent::doUpdateObject($values);
}
I'll try and explain this in methodical terms instead of giving you a big block of code....
So first, you want to if (email_addr != current_email) and if that's true, go on to do
if (new_pass != current_pass) then follow on to make sure if (new_pass == new_pass_again)
Inside all of these IFs, you can return a true/false or some kind of flag, or just //do code inside the brackets :p
EDIT: encase these IFs in: if (country != NULL && language != NULL && email_addr != NULL)

Categories