2

Symfony version : 2.8.5

Context: I have an entity Restaurant which has a OneToOne relationship with an entity Coordinates which have several relations with other entities related to Coordinates informations. I my backend I create a form related to Restaurant entity with a custom nested form related to Coordinates.

Nota : I use EasyAdminBundle to generate my backend.

Entities relations scheme :

Restaurant

  1 ________ 1 `Coordinates`
                    * ________ 1 `CoordinatesCountry`
                    1 ________ 1 `CoordinatesFR`
                                      * ________ 1 `CoordinatesFRLane`
                                      * ________ 1 `CoordinatesFRCity`

Backend view :

enter image description here

At this point I try the following scenario :

  1. I create a new Restaurant, so I fill the fields related form for the first time. I let the Coordinates nested form blank (empty fields). So after form submission, validation messages are displayed (see image below).

enter image description here

  1. I edit the previous form and this time I fill the fields of the Coordinates nested form. After form submission, a new Coordinates entity is hydrated and a relationship is created between Restaurant and Coordinates.

  2. Once again I edit the previous form and this time I clear all the fields of the Coordinates nested form. The validation is not triggered and I get the following error :

Expected argument of type "FBN\GuideBundle\Entity\CoordinatesFRCity", "NULL" given

I precise that in CoordinatesFRType (see code below), to trigger the validations message the first time I had to use the option empty_data with a closure (like described in the official doc) to instatiate a new CoordinatesFR instance in case of empty datas (all fields blank). But here, in this article (written by the creator of the Symfony form component), it is explained (see empty_data and datta mappers paragraphs) that the empty_data is only called at object creation. So I think this the reason why my validation does not work anymore in case of edition.

Question : why the validation is not effective anymore when editing my form and clearing all embedded form ?

The code (only what is necessary) :

Restaurant entity

use Symfony\Component\Validator\Constraints as Assert;

class Restaurant
{
    /**
     * @ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", inversedBy="restaurant", cascade={"persist"})
     * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
     * @Assert\Valid()
     */
    private $coordinates;
}

Coordinates entity

use Symfony\Component\Validator\Constraints as Assert;

class Coordinates
{
    /**
     * @ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesCountry")
     * @ORM\JoinColumn(nullable=false)
     */
    private $coordinatesCountry;

    /**
     * @ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFR", inversedBy="coordinates", cascade={"persist"})
     * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
     * @Assert\Valid()
     */
    private $coordinatesFR;

    /**
     * @ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Restaurant", mappedBy="coordinates")
     * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
     */
    private $restaurant;
}

CoordinatesFR entity

use Symfony\Component\Validator\Constraints as Assert;

class CoordinatesFR extends CoordinatesISO
{
    /**
    * @ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRLane")
    * @ORM\JoinColumn(nullable=true)
    * @Assert\NotBlank()
    */
   private $coordinatesFRLane;

   /**
    * @ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRCity")
    * @ORM\JoinColumn(nullable=false)
    * @Assert\NotBlank()
    */
   private $coordinatesFRCity;

    /**
     * @ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", mappedBy="coordinatesFR")
     * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
     */
    private $coordinates;
}

Easy Admin config (equivalent to RestaurantType)

easy_admin:
    entities:
        Restaurant:
            class : FBN\GuideBundle\Entity\Restaurant
            form:
                fields:
                    - { property: 'coordinates', type: 'FBN\GuideBundle\Form\CoordinatesType' }

CoordinatesType

class CoordinatesType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('CoordinatesCountry', EntityType::class, array(
                'class' => 'FBNGuideBundle:CoordinatesCountry',
                'property' => 'country',
                ))
            ->add('coordinatesFR', CoordinatesFRType::class)
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'FBN\GuideBundle\Entity\Coordinates',
        ));
    }
}

CoordinatesFRType

class CoordinatesFRType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('laneNum', TextType::class)
            ->add('coordinatesFRLane', EntityType::class, array(
                'class' => 'FBNGuideBundle:CoordinatesFRLane',
                'property' => 'lane',
                'placeholder' => 'label.form.empty_value',
                ))
            ->add('laneName', TextType::class)
            ->add('miscellaneous', TextType::class)
            ->add('locality', TextType::class)
            ->add('metro', TextType::class)
            ->add('coordinatesFRCity', EntityType::class, array(
                'class' => 'FBNGuideBundle:CoordinatesFRCity',
                'property' => 'display',
                'query_builder' => function (CoordinatesFRCityRepository $repo) {
                    return $repo->getAscendingSortedCitiesQueryBuilder();
                    },
                'placeholder' => 'label.form.empty_value',
                ))
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'FBN\GuideBundle\Entity\CoordinatesFR',
            // Ensures that validation error messages will be correctly displayed next to each field 
            // of the corresponding nested form (i.e if submission and CoordinatesFR nested form with all fields empty)
            'empty_data' => function (FormInterface $form) {
                return new CoordFR();
            },
        ));
    }
}
Cruz
  • 695
  • 8
  • 21
  • This might be related to a clash between your entity validity and what the form wants to put in there. I can advice you to read http://blog.iltar.nl/posts/733915-avoiding-entities-in-forms – Anyone May 26 '16 at 06:22
  • @Anyone Thanks, it is a very interesting post. Don't you think that a workaround to solve my issue could be to implement a custom data mapper and in the mapFormsToData() method to instatiate a new `CoordinatesFR` object in case of edition ? – Cruz May 26 '16 at 08:01
  • I think the easiest would be to blindly put all form data in either an array or DTO and when valid put it in your entity, that's the easiest solution to avoid having to nest yourself into data hydration behavior – Anyone May 26 '16 at 09:37
  • @Anyone Yes it's a solution using what we could name an intermediary. – Cruz May 26 '16 at 13:10
  • @Anyone The DTO solution is also evocated by Stof in this [post](https://github.com/symfony/symfony/issues/17003). He also proposes to allow null values in setters. – Cruz May 26 '16 at 13:25
  • While allowing null values are an easy solution and a quick win for this problem, it will not work for a long term solution – Anyone May 27 '16 at 06:00
  • There is nothing to deal with easyadminbundle. You should use 'cascade_validation' => true, in CoordinatesFRType under setDefaultOptions function. – Maya Shah Feb 15 '17 at 11:24

0 Answers0