How to validate array items in Symfony 2.3? - php

I use Symfony v2.3.x and I have a validation file that looks something like this:
Namespace\To\MyEntity:
properties:
entityCollection:
- Type: { groups: [mygroup], type: array }
- All:
constraints:
- Type: { groups: [mygroup], type: Namespace\To\AnotherEntity }
So MyEntity must contain an array of AnotherEntitys in the entityCollection field.
It successfully validates that entityCollection must be an array. However, it fails to validate that the elements of that array are of the specified type.
For example, these two var_dumps both show the value 0 on the screen even though I expect something non-zero:
$obj = new MyEntity();
$obj->entityCollection = array(12345);
$errors = $symfonyValidator->validate($obj, array('mygroup'));
var_dump(count($errors));
$obj = new MyEntity();
$obj->entityCollection = array("something");
$errors = $symfonyValidator->validate($obj, array('mygroup'));
var_dump(count($errors));
I even tried removing the constraints entry from the validation file; it made no difference.
I looked at Symfony's official page about the All keyword but I couldn't find anything that would work.
How should I modify the entry in my validation.yml file so that the validation works as intended ?
Thank you in advance.

Your configs look fine, although I'm more accustomed to having them as annotations so it's clear what entity properties they apply to. Have you tried:
$errors = $symfonyValidator->validate($obj, array('mygroup'), true);
The 3rd parameter to the validate() method is a boolean for traversing the object you're validating.
bool $traverse Whether to traverse the value if it is traversable.
Symfony Validator API v2.3

Related

PUT Operations in Symfony 5 and Doctrine ODM

We're developing an API on top of Symfony 5 using Doctrine ODM and MongoDB as well as API Platform. Problem is trying to support PUT operations.
What we find is that if we PUT a JSON document with some fields not included, the object (eg $data) seems to have those fields filled in from the original document in the database.
In some cases, this means that the document passed in via the PUT request will pass validations such as NotBlank() when the document coming n over the wire is missing a required field, but the $data object being validated has that field filled in from the database prior to validation.
As an example, if the original JSON document in Mongo is
{
projectID: "ABCD",
projectName: "My Project",
cost: 100
}
And assuming projectName is a required field and we PUT the following document to our endpoint /v1/example-endpoint/ABCD
{
cost: 100
}
then in out custom data persister we have the following:
public function persist($data, array $context = [])
{
...do something here...
}
The $data object passed in to the persister will include the previous projectName "My Project" from the database.
Is this expected behavior and/or is there a way around this to validate the data sent over the wire as-is?
FWIW, we tried created a new Object of the class and setting the data from the PUT content, then creating a validator and validating the new object, but ran in to an issue where custom validation methods that had constructors were not being autowired when calling the validator manually. If we could successfully validate the new object that would work too.
$putData = json_decode($this->requestStack->getCurrentRequest()->getContent(), true);
$newProject = (new Project());
foreach ($putData as $property => $value) {
$newProject->setProperty($property, $value);
}
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadValidatorMetadata')
->getValidator();
$errors = $validator->validate($newProject);

Symfony serializes dates as empty arrays

I'm making a JSON REST API using Symfony 5.2. One of the API calls returns a date, however I noticed the date is always serialized as an empty array ([]). Here is an extract from the response I get:
{"count":1,"data":[{"id": 6, "published_from":[],"published_until":[]}]}
The other fields like id are correct, but the dates aren't. In the database, the fields are populated with dates.
I tried running the query with the default services.yaml with the same result.
Here is the field declaration in the entity:
/**
* #ORM\Column(type="datetime")
* #Groups({"base", "updatable"})
*/
private $publishedFrom;
Finally, still edited for brevety, here is relevant business logic code (eg: in Controller):
$qb = $this->createQueryBuilder('p')
->andWhere('p.store = :store')
->setParameter('store', $store_id);
$paginator = new Paginator($qb, false);
$entity = ['count' => count($paginator), 'data' => $paginator);
return $this->json($entity, context: ['groups' => ['base']]);
It looks like the Symfony\Component\Serializer\Normalizer\DateTimeNormalizer isn't used by default, when I think it should be.
I'm fairly sure it's only a matter of configuration, but I can't find any clue.
After some debugging, I've seen that both dates are replaced by empty arrays at line 203 of AbstractObjectNormalizer:
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
Thank you for any input.
EDIT: See my answer for more information on this bug.
Alright, I found the answer. Even after reusing the default services.yaml and running bin/console c:c, my custom Denormalizer was still loaded somehow. It does not override the supportsNormalization method. In the AbstractNormalizer stub, this resolves to true.
The solution for me was to override it: In my denormalizers I added this to prevent them from being used for normalization:
public function supportsNormalization($data, string $format = null)
{
return false;
}
I'll leave this issue up in case anyone encounters this very specific issue.
EDIT: I realized after re-enabling my services.yaml, that the snake_case converter re-enables this bug. I guess this is for the same reason. I have not found a workaround yet.
Github issue: https://github.com/symfony/symfony/issues/40818
The problem also can be with normalizer's order(priority).
You can set it with:
#services.yaml
get_set_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
tags:
- { name: serializer.normalizer, priority: -991 }
And check it:
php bin/console debug:container --tag=serializer.normalizer

Injecting Doctrine Entity into Symfony controller based on route parameters

I want to inject Doctrine entities into controller actions based on the route parameters in an attempt to reduce the insane amount of code duplication inside my controllers.
For example I have the following route
product:
path: /product/edit/{productId}
defaults: { _controller: ExampleBundle:Product:edit }
Instead of my current approach
public function editAction($productId)
{
$manager = $this->getDoctrine()->getManager();
$product = $manager->getRepository('ExampleBundle:Product')
->findOneByProductId($productId);
if (!$product) {
$this->addFlash('error', 'Selected product does not exist');
return $this->redirect($this->generateUrl('products'));
}
// ...
}
I'd like this to be handled else where as it's repeated in at least 6 controller actions currently. So it would be more along the lines of
public function editAction(Product $product)
{
// ...
}
It seems this has in fact been done before and the best example I can find is done by the SensioFrameworkBundle http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
I'd use this but were not using annotations in our Symfony projects so need to look at alternatives. Any suggestions on how this can be achieved?
If you read the docs carefully, you'll learn that param converters actually work without annotations:
To detect which converter is run on a parameter the following process is run:
If an explicit converter choice was made with #ParamConverter(converter="name") the converter with the given name is chosen.
Otherwise all registered parameter converters are iterated by priority. The supports() method is invoked to check if a param converter can convert the request into the required parameter. If it returns true the param converter is invoked.
In other words if you don't specify a param converter in an annotation, Symfony will iterate through all registered converters and find the most appropriate one to handle your argument (based on a type hint).
I prefer to put an annotation in order to:
be explicit
save some processing time

Zend Framework 2 Form and InputFilter retuning different values

I have a element(Select) of name,parameter. The problem is that on validation it returns error like this:
The input was not found in the haystack
I know that this is returned by InArray Validator. But, how can this happen when the input is valid. So, I tried to inspect the form element and inputfilter. So, i did:
print_r($form->get('parameter')->getValue()); // returns frequency
print_r($form->getInputFilter()->get('parameter')->getValue()); // returns 0
I just cant understand, why are they returning different values?
Here is the full code:
$postData = $request->getPost()->toArray();
$form->setData($postData);
print_r($form->get('parameter')->getValue());
if ($form->isValid()) {
$alarm->exchangeArray($form->getData());
$this->getAlarmMapper()->save($alarm);
$changesSaved = true;
}
print_r($form->getInputFilter()->get('parameter')->getValue());
As far as I know, if validation fails your filter simply doesn't return a value, that's why it returns 0. So you should probably look inside your validation, why it fails to validate.

Zend Framework 2 Form, created with the AnnotationReader, is valid even when the data is invalid

I have a Form, which I create using the annotation builder like this:
$builder = new AnnotationBuilder();
$fieldset = $builder->createForm(new \Application\Entity\Example());
$this->add($fieldset);
$this->setBaseFieldset($fieldset);
In the controller everything is standard:
$entity = new \Application\Entity\Example();
$form = new \Application\Form\Example();
$form->bind($entity);
if($this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if($form->isValid()) {
// save ....
}
}
The problem is, that $form->isValid() always returns true, even when empty or invalid form is submitted. What is even more weird is that the form element error messages are all set, hinting that they are not valid.
I have looked into the ZF2 Form / InputFilter / Input classes and found out that:
Input->isValid() is called twice: once in the Form->isValid() and once in Form->bindValues()
In the first call the validator chain in Input->isValid() ($this->getValidatorChain) is empty and in the second call (from bindValues) it is correct.
What may got wrong?
PS. Using devel version 2.1
I found out what was causing it.
It turns out, that the annotation builder was never intended to work this way. The annotation builder creates a \Zend\Form\Form instance, which I placed in as a fieldset in my base form. I am not sure why, but this was causing the base form not to validate. So in order to make the above code work, there should be no extra Form class and in controller we should have:
$entity = new \Application\Entity\Example();
$builder = new AnnotationBuilder();
$form = $builder->createForm($entity);
$form->bind($entity);
if($this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if($form->isValid()) {
// save ....
}
}
Maybe there will be a createFieldset function in the AnnotationBuilder in the future, but for now this seems to be the only way. Hope this helps someone. :)
I am also experiencing the same problem. When I use Annotations to create Fieldsets #Annotation\Type("fieldset") in a form, isValid() always returns true.
Looking at the code for Zend\Form\Factory, when we are creating a Fieldset the configureFieldset() function does not call prepareAndInjectInputFilter(), even where there is an input_filter as part of the form specification.
Only when we are creating a Form, does the Zend\Form\Factory::configureForm() function call prepareAndInjectInputFilter().
So it seems that input filters, and validation groups are only created by the AnnotationBuilder when its type is set to create a form.
I created an input filter myself, from the annotations by adding the code below to my form:
$fspec = ArrayUtils::iteratorToArray($builder->getFormSpecification($entity));
$outerfilter = new InputFilter();
$iffactory = new \Zend\InputFilter\Factory ();
$filter = $iffactory->createInputFilter($fspec['input_filter']);
$outerfilter->add($filter, 'shop'); // Use the name of your fieldset here.
$this->setInputFilter($outerfilter);

Categories