Hi guyz,
I've a validation's problem when I try to validate a OneToMany with a dynamic validation_groups.
The goals is to apply a different set of validation based on data submitted, see data based validation.
I've a Quiz entity with a OneToMany collection of Panel entity.
I want to apply a specific validation_groups based on the value of type property of Panel Entity.
# src/AppBundle/Entity/Quiz.php
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Quiz
*
* @ORM\Table(name="quiz")
* @ORM\Entity(repositoryClass="AppBundle\Repository\QuizRepository")
*/
class Quiz
{
use TimestampableEntity;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
# ...
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Panel", mappedBy="quiz", cascade={"persist"})
* @Assert\Valid()
*/
private $panels;
/**
* Quiz constructor.
*/
public function __construct()
{
$this->published = true;
$this->panels = new ArrayCollection();
}
# ...
/**
* Add panel.
*
* @param Panel $panel
*
* @return Quiz
*/
public function addPanel(Panel $panel)
{
$this->panels[] = $panel;
$panel->setQuiz($this);
return $this;
}
/**
* Remove panel.
*
* @param Panel $panel
*/
public function removePanel(Panel $panel)
{
$this->panels->removeElement($panel);
}
/**
* Get panels.
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getPanels()
{
return $this->panels;
}
}
# src/AppBundle/Entity/Panel.php
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Fluoresce\ValidateEmbedded\Constraints as FluoresceAssert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Panel
*
* @ORM\Table(name="panel")
* @Vich\Uploadable
* @ORM\Entity(repositoryClass="AppBundle\Repository\PanelRepository")
*/
class Panel
{
use TimestampableEntity;
const TYPE_INTRO = 'intro';
const TYPE_LANG = 'lang';
const TYPE_QUESTION = 'question';
const TYPE_FORM = 'form';
const TYPE_SPLASH = 'splash';
const TYPE_THANKS = 'thanks';
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="type", type="string", length=50)
* @Assert\Length(max="50")
* @Assert\NotBlank(message="Vous devez renseigner le type de panneau.")
*/
private $type;
# ...
/**
* Set type
*
* @param string $type
*
* @return Panel
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* @return string
*/
public function getType()
{
return $this->type;
}
# ...
/**
* Set Quiz
*
* @param \AppBundle\Entity\Quiz $quiz
*
* @return Panel
*/
public function setQuiz(\AppBundle\Entity\Quiz $quiz)
{
$this->quiz = $quiz;
return $this;
}
/**
* Get quiz
*
* @return \AppBundle\Entity\Quiz
*/
public function getQuiz()
{
return $this->quiz;
}
/**
* @Assert\Callback(groups={"PanelQuestion"})
*
* @param ExecutionContextInterface $context
*/
public function validateAnswers(ExecutionContextInterface $context)
{
$nbAnswers = $this->answers->count();
$goodAnswers = $this->answers->filter(
function (Answer $answer) {
return $answer->isGood();
}
);
if ($goodAnswers->isEmpty()) {
$context->buildViolation('Au moins une des r茅ponses doit 锚tre valide.')
->atPath('answers')
->addViolation();
}
}
}
# app/config/config_easyadmin.yml
# ...
entities:
Quiz:
class: AppBundle\Entity\Quiz
# ...
form:
title: 'Quiz'
fields:
- { type: 'group', label: 'Infos', icon: 'info-circle' }
- { property: 'title', css_class: 'col-sm-6', label: 'Titre' }
# ...
- { type: 'group', label: 'Panneaux', icon: 'columns' }
- { property: 'panels', label: 'Liste des panneaux', type: 'collection', type_options: { entry_type: 'AppBundle\Form\Type\PanelType', by_reference: false } }
```php
namespace AppBundle\Form\Type;
use AppBundle\EntityPanel;
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
/**
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Panel',
'label' => false,
'validation_groups' => function (FormInterface $form) {
/** @var Panel $panel **/
$panel = $form->getData();
switch($panel->getType()){
case Panel::TYPE_INTRO:
return ["Default", "PanelIntro"];
case Panel::TYPE_LANG:
return ["Default", "PanelLang"];
case Panel::TYPE_QUESTION:
return ["Default", "PanelQuestion"];
case Panel::TYPE_FORM:
return ["Default", "PanelForm"];
case Panel::TYPE_SPLASH:
return ["Default", "PanelSplash"];
case Panel::TYPE_THANKS:
return ["Default", "PanelThanks"];
}
return ["Default"];
},
));
}
}
````
I've not attached the whole code but I've 6 differents validation_groups and I want to execute some spectific asserts based on the type of each Panel.
When I check symfony toolbar, I can see the correct validation groups on each panel but in reality on only Default group is validated.
I've even tried to set at Quiz level in EasyAdmin config:
form_options: { validation_groups: ['PanelQuestion'] }
To pass the same plain groups on the collection:
- { property: 'panels', label: 'Liste des panneaux', type: 'collection', type_options: { entry_type: 'AppBundle\Form\Type\PanelType', by_reference: false, validation_groups: ['PanelQuestion'] } }
And even in my custom FormType:
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Panel',
'label' => false,
'validation_groups' => ['PanelQuestion']
)
)
The result is still the same, only Default validation_groups is applied on each Panel entity and I never reach my Assert\Callback binded on PanelQuestion group or my other asserts, so I'm confused, it seems that EasyAdmin or Symfony doesn't care of custom groups.
I'm not a rock star of Symfony form logic but do I miss something ?
The only way I found for the moment is using Fluoresce which allow to specify the validation group to apply to the nested collection:
* @Fluoresce\Validate(groups={"PanelQuestion"}, embeddedGroups={"PanelQuestion"});
*/
private $panels;
But with this, I can't do it dynamically and not the expected result :/
Maybe the whole problem is just related to Symfony validation of embedded sub-forms but any help/advises would be appreciated ;)
If it's not possible, I'll do a custom Assert\Callback on Default group and trigger all the violations in there, but I lose the dynamic asserts
The code of your examples look correct and it should definitely work. Maybe it's not working because of what you said about embedded forms? Anyway, this issue is too old, so hopefully you have fixed it.
I've bypassed the pb with a custom service in which i'm trying to validate the correct group based on my type...
If someone is interested:
class PanelValidationGroupsValidator extends ConstraintValidator
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function validate($value, Constraint $constraint)
{
if ($value === null) {
return;
}
if (!$value instanceof Panel) {
throw new UnexpectedTypeException($value, 'Panel');
}
/** @var Panel $panel */
$panel = $value;
$violations = [];
switch ($panel->getType()){
case "intro":
$violations = $this->validator->validate($panel, null, ["PanelIntro"]);
break;
case "lang":
$violations = $this->validator->validate($panel, null, ["PanelLang"]);
break;
case "question":
$violations = $this->validator->validate($panel, null, ["PanelQuestion"]);
break;
case "form":
$violations = $this->validator->validate($panel, null, ["PanelForm"]);
break;
case "splash":
$violations = $this->validator->validate($panel, null, ["PanelSplash"]);
break;
case "thanks":
$violations = $this->validator->validate($panel, null, ["PanelThanks"]);
break;
}
foreach($violations as $violation){
/** @var ConstraintViolation $violation */
$this->context->buildViolation($violation->getMessage())
->atPath($violation->getPropertyPath())
->addViolation();
}
}
}
But i'm thinking the problem is not related to easyadmin.
Like you, i'm not confortable with Form behavior, so i'm not sure.
Just saying: in your original comment you shared this code:
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Panel',
'label' => false,
'validation_groups' => function (FormInterface $form) {
/** @var Panel $panel **/
$panel = $form->getData();
switch($panel->getType()){
case Panel::TYPE_INTRO:
return ["Default", "PanelIntro"];
case Panel::TYPE_LANG:
return ["Default", "PanelLang"];
case Panel::TYPE_QUESTION:
return ["Default", "PanelQuestion"];
case Panel::TYPE_FORM:
return ["Default", "PanelForm"];
case Panel::TYPE_SPLASH:
return ["Default", "PanelSplash"];
case Panel::TYPE_THANKS:
return ["Default", "PanelThanks"];
}
return ["Default"];
},
));
}
Today I saw this in the Symfony Docs repo: https://github.com/symfony/symfony-docs/pull/8522/files
Would replacing return ["...", "..."] by return new GroupSequence(["..", ".."]), solve this problem?
Hey @javiereguiluz
Still the same, I don't know why but the FormType only tries to validate Default group.
I'm closing this issue because we're starting a new phase in the history of this bundle (see #2059). We've moved it into a new GitHub organization and we need to start from scratch: no past issues, no pending pull requests, etc.
I understand if you are angry or disappointed by this, but we really need to "reset" everything in order to reignite the development of this bundle.