Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic validation_groups on OneToMany #1619

Closed
laurent-bientz opened this issue May 3, 2017 · 5 comments
Closed

Dynamic validation_groups on OneToMany #1619

laurent-bientz opened this issue May 3, 2017 · 5 comments

Comments

@laurent-bientz
Copy link
Contributor

laurent-bientz commented May 3, 2017

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 } }
# src/AppBundle/Form/Type/PanelType.php
<?php

namespace AppBundle\Form\Type;

use AppBundle\Entity\Panel;
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;

/**
 * Class PanelType.
 *
 * used by easyadminbundle custom bo
 */
class PanelType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add(
                'type',
                ChoiceType::class,
                array(
                    'label' => 'Type',
                    'multiple' => false,
                    'choices' => array(
                        'Introduction' =>  Panel::TYPE_INTRO,
                        'Langues' => Panel::TYPE_LANG,
                        'Question' => Panel::TYPE_QUESTION,
                        'Form' => Panel::TYPE_FORM,
                        'Réponse' => Panel::TYPE_SPLASH,
                        'Remerciement' => Panel::TYPE_THANKS,
                    ),
                )
            )
            # ...
        ;
    }

    /**
     * @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

@laurent-bientz laurent-bientz changed the title Dynamic validation_groups on ManyToOne Dynamic validation_groups on OneToMany May 4, 2017
@javiereguiluz
Copy link
Collaborator

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.

@laurent-bientz
Copy link
Contributor Author

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.

@javiereguiluz
Copy link
Collaborator

javiereguiluz commented Oct 18, 2017

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?

@laurent-bientz
Copy link
Contributor Author

Hey @javiereguiluz

Still the same, I don't know why but the FormType only tries to validate Default group.

@javiereguiluz
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants