Symfony nested constraints don't work properly - php

I came across a strange problem with Symfony validation.
Seems that "nested" constraints don't work properly.
For example, I create a string variable $data which needs to be validated.
$data = 'string';
$constraint = new Assert\Type('integer');
$violations = $validator->validate($data, $constraint);
self::assertTrue($violations->count() > 0);
In this case it works properly. We pass the string variable to the constraint which allows only integer. But if I create "nested" constraint the test won't pass.
$data = 'string';
$constraint = new Assert\Required([
new Assert\Type('integer'),
]);
$violations = $validator->validate($data, $constraint);
self::assertTrue($violations->count() > 0);
In this case the test is failed. The validator doesn't find any violations.
Is it a bug? Or do I do something wrong?

if you want your data to be not empty (required) and to be a number:
$data = 'string';
$validator = Validation::createValidator();
$violations = $validator->validate($data, [
new NotBlank(),
new Type(['integer'),
]);
see https://symfony.com/doc/current/components/validator.html

There is no Assert\Required constraint.
Since Symfony 5.4 you can use Attributes to combine constraints:
#[Assert\All([
new Assert\NotNull(),
new Assert\Range(min: 3),
])]
or
#[Assert\Collection(
fields: [
'foo' => [
new Assert\NotNull(),
new Assert\Range(min: 3),
],
'bar' => new Assert\Range(min: 5),
'baz' => new Assert\Required([new Assert\Email()]),
]
)]
https://symfony.com/blog/new-in-symfony-5-4-nested-validation-attributes

Related

Symfony constraint LessThan Date

I use Symfony v5.2
I've created this constraint :
$constraint = new Assert\Collection([
'firstname' => [
new Assert\NotNull(),
new Assert\NotBlank()
],
'lastname' => [
new Assert\NotNull(),
new Assert\NotBlank()
],
'birthdate' => [
new Assert\NotNull(),
new Assert\NotBlank(),
new Assert\Date(),
new Assert\LessThan('today')
]
]);
Then I send this JSON with postman :
{
"firstname": "john",
"lastname": "doe",
"birthdate": "2030-01-01"
}
But the assert is not triggered. It seems that the lessThan is only on DateTime, but not sure.
What I've done wrong ? Do i need to make my own check for a Date ?
In your case, 2 strings are compared by the LessThan validator
You should convert the request value of birthdate to an object of DateTime before validation.

Inconsistent Data being associate on belongsTo

I am creating a feature test on one to many relationship. I just have a simple one to many relation with a proper setup base on the Laravel documentation. My test goes like this.
/** #test */
public function it_attach_containers()
{
$this->withoutExceptionHandling();
$vendor = factory(Vendor::class)->create();
$containersCount = 30;
$containers = factory(Container::class, $containersCount)->create();
$user = factory(User::class)->create();
$attributes = [
'vendor' => $vendor->id,
'ordered' => null,
'deployed' => null,
'last_contact' => null,
'containers' => $containers->pluck('name')
];
$response = $this->actingAs($user, 'api')
->withHeaders([
'X-Requested-With' => 'XMLHttpRequest'
])
->json('POST', '/api/deployments', $attributes);
$deployment = Deployment::find($containers[0]->id);
$this->assertInstanceOf(Deployment::class, $deployment);
$this->assertCount($containersCount, $deployment->containers()->get());
$this->assertDatabaseHas('deployments', [
'vendor' => $vendor->id,
'ordered' => null,
'deployed' => null,
'last_contact' => null
]);
}
The relation I have is a one to many relationship. A one deployment has many container. The code below is how I associate relation..
public function associateDeployment(Deployment $deployment, $data)
{
foreach ($data['containers'] as $containerName) {
$container = Container::where('name', $containerName)->first();
if (!$container) {
$container = Container::create([
'name' => $containerName,
'status' => true
]);
}
if (is_null($container->deployment_id)) {
$container->deployment()->associate($deployment);
$container->save();
}
}
}
The result on my test is really weird. sometimes it pass but sometimes not. I notice that the issue occur on the assertCount. as you can see on my test. it assert if the containers is 30. but mostly it didnt go up to 30. its about 25-29.. then sometimes it pass. what do you think is the problem?
I think the bug is the following line:
$deployment = Deployment::find($containers[0]->id);
Here you are fetching a deployment record by using container id. Instead use the following code:
$deployment = Deployment::find($containers[0]->deployment_id);

How can I get array key => errorMessage from $violations. Symfony 4

Here is my code
$arParams = $request->all();
$validator = Validation::createValidator();
$groups = new GroupSequence(['Default', 'custom']);
$constraint = new Assert\Collection([
'name' => new Assert\Length(['min' => 2]),
'city' => new Assert\Length(['min' => 2]),
'email' => new Assert\Email(),
'phone' => new Assert\Length(['min' => 18]),
'message' => new Assert\NotNull()
]);
$violations = $validator->validate($arParams, $constraint, $groups);
If i get some errors, how can I get an array like
['name' => not enough symbols, 'email' => wrong email]?
I tried to use foreach on $violations but cant find all the methods of its elements Phpstorm sign $violation as mixed. I found only $violation->getMessage() and ->getCode()
I recommend you to read this article https://symfony.com/doc/current/validation.html
If validation fails, a non-empty list of errors (class ConstraintViolationList) is returned.
So you can get your list this way:
if ($violations->count() > 0) {
$formatedViolationList = [];
for ($i = 0; $i < $violations->count(); $i++) {
$violation = $violations->get($i);
$formatedViolationList[] = array($violation->getPropertyPath() => $violation->getMessage());
}
}
Couple explanations. We use methods from violation api count() or get a number of violations, and after in for loop we use get($i) for get every violation by index. After we use getPropertyPath() for get path (name of your property) and getMessage() for get message.

Add property and value in a object array

im quite new in php OOP, and im using laravel has my framework of choice. Im creating a object user and create the record, the only thing i have to do is to check if one of my input fields(not required) was set, if it was set than i have to add in in the user object, but how can i add this property (mobilephone) and value in a exiting object, am i doing it right?.
code:
$user = User::create([
'email' => $userDat['email'],
'name' => $userDat['name'],
'surname' => $userDat['surname'],
]);
if (isset($userDat['mobilephone'])) {
$user->mobilephone = $userDat['mobilephone'];
}
If you absolutely have to add the property to the object, I believe you could cast it as an array, add your property (as a new array key), then cast it back as an object. The only time you run into stdClass objects (I believe) is when you cast an array as an object or when you create a new stdClass object from scratch (and of course when you json_decode() something - silly me for forgetting!).
Instead of:
$foo = new StdClass();
$foo->bar = '1234';
You'd do:
$foo = array('bar' => '1234');
$foo = (object)$foo;
Or if you already had an existing stdClass object:
$foo = (array)$foo;
$foo['bar'] = '1234';
$foo = (object)$foo;
Also as a 1 liner:
$foo = (object) array_merge( (array)$foo, array( 'bar' => '1234' ) );
or maybe you could do it this way
$user->{'mobilephone'} = $userDat['mobilephone'];
There're a few ways you can do it.
Prepare the array in advance
$data = [
'email' => $userDat['email'],
'name' => $userDat['name'],
'surname' => $userDat['surname'],
];
if ($phone = array_get($userDat, 'mobilephone')) {
$data['mobilephone'] = $phone;
}
User::create($data);
Or create the object first then save it
$user = new User([
'email' => $userDat['email'],
'name' => $userDat['name'],
'surname' => $userDat['surname'],
]);
if ($phone = array_get($userDat, 'mobilephone')) {
$user->mobilephone = $phone;
}
$user->save();
As you are currently, but with an extra query (Ill advised)
$user = User::create([
'email' => $userDat['email'],
'name' => $userDat['name'],
'surname' => $userDat['surname'],
]);
if ($phone = array_get($userDat, 'mobilephone')) {
$user->mobilephone = $phone;
$user->save();
}
how can i add this property (mobilephone) and value in a exiting object, am i doing it right?.
This strongly depends on which framework you are using. In general, you can add arbitrary properties to any object in PHP (which does not mean you always should).
In your case, you are using the Eloquent framework that's shipped with Laravel, which actually depends on dynamically assigned object attributes and are using it absolutely correctly. This is documented in-depth in the official documentation. Note that you'll probably have to call $user->save() at some point to actually save your user object (thanks to Ben for the hint).
Note that alternatively, you could assign all properties of your User instance that way:
$user = new User();
$user->email = $userDat['email'];
$user->name = $userDat['name'];
$user->surname = $userDat['surname'];
if (isset($userDat['mobilephone'])) {
$user->mobilephone = $userDat['mobilephone'];
}
$user->save();

How to validate if an element of an array is an array itself?

Given this input:
[
'key' => 'value',
]
How to validate to ensure that:
key attribute exists
Its value is an array (with any number of elements)
I expected this constraint to work
$constraint = new Collection([
'key' => new Required([
new Type('array'),
new Collection([
'value' => new Required([
new NotBlank(),
]),
]),
]),
]);
but it throws an exception:
Symfony\Component\Validator\Exception\UnexpectedTypeException: Expected argument of type "array or Traversable and ArrayAccess", "string" given
What am I missing?
PS: it's symfony v2.7.1
PPS: just to clarify: I know one can use a callback. If I wanted to re-implement the validation manually from scratch - I wouldn't have used symfony at the very first place. So the question is particularly about combining the existing constraints and not about using a callback constraint..
I had the exact same problem two nights ago.
The conclusion at the very end was that Symfony2 validation has no "fast-fail" validation. That is, even if your Type() constraint would fail it would proceed with other constraints and thus fail with UnexpectedTypeException exception.
However, I was able to find a way to tackle that:
$constraint = new Collection([
'key' => new Required([
new Type(['type' => 'array']),
new Collection([
// Need to wrap fields into this
// in order to provide "groups"
'fields' => [
'value' => new Required([
new NotBlank(),
]),
],
'groups' => 'phase2' // <-- THIS IS CRITICAL
]),
]),
]);
// In your controller, service, etc...
$V = $this->get('validator');
// Checks everything by `Collection` marked with special group
$violations = $V->validate($data, $constraint);
if ( $violations->count()){
// Do something
}
// Checks *only* "phase2" group constraints
$violations = $V->validate($data, $constraint, 'phase2');
if ( $violations->count()){
// Do something
}
Hope that this helps a bit. Personally, I find it annoying that we need to do this. Some sort of "fast-fail" flag within validator service would be much helpful.
You're saying the Collection constraint should just fail instead of throwing an exception because 'value' is a string and not an array.
There is a recently logged Symfony bug for this: https://github.com/symfony/symfony/issues/14943
Use Callback constraint(docs) where you can implement your custom validation logic.
The other way is to create custom constraint and validator classes. (docs)

Categories