3

Is it possible to dynamically generate choices in form collections?

The situation:

  • ItemEntity

  • PropertyEntity

These entities can be added and edited through the Item Form.

When you add a type to the Item Entity the status on the property entities will be loaded through an ajax request. Each type of item has different status choices that are provided by a service.

This service is injected into the PropertyFormType to provide the available choices.

Everything works fine, except submitting the form keeps returning errors. It appears the choices are not loaded.

On the Property Entity the provided status options are empty. I know the service provides the right data (an array with all the status choices for a type of item).

Debugging this tells me that during the POST_SUBMIT event, the data is not set. dump($event->getForm()->getData()); shows me that the type property of the Item Entity is still null, even though it has been set in the form.

Is it possible to read submitted data from the parent Form object to determine which choices have been loaded through ajax to fix the ConstraintViolation errors?

Symfony docs:

Form Errors:

Caused by:
ConstraintViolation {#2314 ▶}
TransformationFailedException {#1587 ▼
  #message: "Unable to reverse value for property path "status": The choice "test" does not exist or is not unique"
  #code: 0
  #file: "/home/vagrant/shop4raad2/vendor/symfony/form/Form.php"
  #line: 1150
  trace: {...}
   …1
}
TransformationFailedException {#1598 ▼
  #message: "The choice "test" does not exist or is not unique"
  #code: 0
  #file: "/home/vagrant/shop4raad2/vendor/symfony/form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php"
  #line: 48
  trace: {...}
}

Item Form:

namespace App\Form;

use App\Entity\ItemEntity;
use App\Form\Type\PropertyFormType;
use App\Service\Provider\ItemTypeProvider;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Item Form
 */
class ItemForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $item = $builder->getData();

        $builder->add("type", ChoiceType::class, [
            "choices"            => ItemEntity::getTypeChoices(),
            "disabled"           => null !== $dataImportMapping->getId(),
            "required"           => null === $dataImportMapping->getId(),
            /* ... */
        ]);

        $builder->add("properties", CollectionType::class, [
            "entry_type"         => PropertyFormType::class,
            "entry_options"      => [PropertyFormType::OPTION_ITEM => $item],
            "prototype"          => true,
            "allow_add"          => true,
            "allow_delete"       => true,
            /* ... */
        ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => ItemEntity::class,
        ]);
    }
}

Property Form Type:

namespace App\Form\Type;

use App\Entity\PropertyEntity;
use App\Service\Provider\PropertyStatusProvider;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Property Form Type
 */
class PropertyFormType extends AbstractType
{
    const OPTION_ITEM = "data_item";

    private $statusProvider;

    public function __construct(PropertyStatusProvider $statusProvider)
    {
        $this->statusProvider = $statusProvider;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $item = $options[self::OPTION_ITEM];

        $formModifier = function(FormInterface $form, ItemEntity $item) {

            // load choices from service - this service returns an array of available choices by 
            $statusChoices = $this->statusProvider->getAvailableChoices($item->getType());

            $form->add("status", ChoiceType::class, [
                "choices"  => $statusChoices,
                "required" => true,
                /* ... */
            ]);
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifier) {
                /* @var PropertyItem $property */
                $property = $event->getData();

                $formModifier($event->getForm(), $property->getItem());
            }
        );

        $builder->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifier) {

                // It's important here to fetch $event->getForm()->getData(), as
                // $event->getData() will get you the client data (that is, the ID)
                /* @var PropertyItem $property */
                $property = $event->getForm()->getData();

                // since we've added the listener to the child, we'll have to pass on
                // the parent to the callback functions
                $formModifier($event->getForm()->getParent(), $property->getItem());
            }
        );
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => PropertyEntity::class,
        ]);
    }
}


Choices are loaded dynamically into the properties select via ajax using the select2 library

item-form.js:

    $("select[id^='item_form_properties_'][id$='_status']").select2({
        ajax: {
            type: "GET",
            url: Routing.generate("async-item-properties"),
            dataType: "json",
            data: function(params) {
                var query = {
                    term: params.term,
                };

                var mapping_type = $("select#item_form_type").val();

                if (null !== mapping_type && "" !== mapping_type) {
                    query['type'] = mapping_type;
                }

                return query;
            },
            processResults: function(data) {

                var properties = [];

                $.each(data, function(key, item) {
                    properties.push({
                        id: item.id,
                        text: item.value,
                    });
                });

                return {
                    results: properties
                };
            },
        },
        /* ... */
    });

Item Entity:

/**
 * @ORM\Entity(/* ... */)
 * @ORM\Tabele(/* ... */)
 */
class ItemEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(name = "item_id", type = "integer")
     */
    private $id;

    /**
     * @ORM\Column(type = "string", length = 64)
     */
    private $type;

    /**
     * @ORM\OneToMany(targetEntity = "PropertyEntity", mappedBy = "item", cascade={"persist", "remove"})
     */
    private $properties;

    /* ... */

    /**
     * @param PropertyEntity $property
     *
     * @return self
     */
    public function addProperty(PropertyEntity $property)
    {
        $property->setItem($this);

        $this->properties[] = $property;

        return $this;
    }

    /**
     * @param PropertyEntity $property
     */
    public function removeProperty(PropertyEntity $property)
    {
        $this->properties->removeElement($property);
    }
}

Property Entity:

/**
 * @ORM\Entity(/* ... */)
 * @ORM\Tabele(/* ... */)
 */
class PropertyEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(name = "property_id", type = "integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity = "ItemEntity", inversedBy = "properties")
     * @ORM\JoinColumn(name = "item_id", referencedColumnName = "item_id")
     */
    private $item;

    /**
     * @ORM\Column(type = "string", length = 64)
     */
    private $status;

    /* ... */
}

The templates are very straight forward and not relevant to this problem so I'm omitting them from this question.

2
  • May be you are looking for Symfony Form Events? symfony.com/doc/current/form/events.html Commented May 18, 2019 at 18:15
  • @JessGabriel There are 2 Event Listeners added to the Property Form Type. The solution is in Form Events, but it doesn't seem to work like I've applied in other forms. This might be because this is a sub form used in a Collection. There is no clear documentation on how to combine a Collection form with form events. I hope someone can shine some light on this topic. Commented May 19, 2019 at 19:13

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.