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.