php - Is it possible to dynamically generate choices in symfony 4 form collections?

514

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.

People are also looking for solutions to the problem: php - Log All API request and response + Slim framework

Source

Didn't find the answer?

Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.

Ask a Question

Write quick answer

Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.

Similar questions

Find the answer in similar questions on our website.