Update Form with 'choice_list' to Symfony >= 2.8 - php

I want to update a form class to Symfony2.8 (and later to Symfony3). Now the form is converted except one attribute, the choice_list, that is not supported anymore. And I don't know how to do this.
I have the following form type that is defined also as a service:
class ExampleType extends AbstractType
{
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('list', ChoiceType::class, array(
'choice_list' => new ExampleChoiceList($this->delegate),
'required'=>false)
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ExampleClass',
));
}
}
And I have the following class for the choice list:
class ExampleChoiceList extends LazyChoiceList
{
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
/**
* Loads the choice list
* Should be implemented by child classes.
*
* #return ChoiceListInterface The loaded choice list
*/
protected function loadChoiceList()
{
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$id = $person->getId();
$personsList[$id] = (string) $person->getLastname().', '.$person->getFirstname();
}
return new ArrayChoiceList($personsList);
}
}
The class ExampleChoiceList generate the choice list how I want to have it, and until now it worked. But the attribute choice_list is not supported anymore, and my question is "how do I get this converted without too much work?". I read that I should use simple choice but how I get that what I want (the specific label from the database) in Symfony 2.8. I hope somebody can help me.

By using a ChoiceListInterface you are almost there.
I suggest you change the ExampleChoiceList to implement the Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface instead, it requires you implement 3 methods:
<?php
// src/AppBundle/Form/ChoiceList/Loader/ExampleChoiceLoader.php
namespace AppBundle\Form\ChoiceList\Loader;
use Acme\SomeBundle\Delegate;
use Symfony\Component\Form\ArrayChoiceList;
use Symfony\Component\Form\Loader\ChoiceLoaderInterface;
class ExampleChoiceLoader implements ChoiceLoaderInterface
{
/** $var ArrayChoiceList */
private $choiceList;
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
/**
* Loads the choice list
*
* $value is a callable set by "choice_name" option
*
* #return ArrayChoiceList The loaded choice list
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$label = (string) $person->getLastname().', '.$person->getFirstname();
$personsList[$label] = (string) $person->getId();
// So $label will be displayed and the id will be used as data
// "value" will be ids as strings and used for post
// this is just a suggestion though
}
return $this->choiceList = new ArrayChoiceList($personsList);
}
/**
* {#inheritdoc}
*
* $choices are entities or the underlying data you use in the field
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// optimize when no data is preset
if (empty($choices)) {
return array();
}
$values = array();
foreach ($choices as $person) {
$values[] = (string) $person->getId();
}
return $values;
}
/**
* {#inheritdoc}
*
* $values are the submitted string ids
*
*/
public function loadChoicesForValues(array $values, $value)
{
// optimize when nothing is submitted
if (empty($values)) {
return array();
}
// get the entities from ids and return whatever data you need.
// e.g return $this->delegate->getPersonsByIds($values);
}
}
register both the loader and the type as services so they are injected:
# app/config/services.yml
services:
# ...
app.delegate:
class: Acme\SomeBundle\Delegate
app.form.choice_loader.example:
class: AppBundle\Form\ChoiceList\Loader\ExampleChoiceLoader
arguments: ["#app.delegate"]
app.form.type.example:
class: AppBundle\Form\Type\ExampleType
arguments: ["#app.form.choice_loader.example"]
Then change the form type to use the loader:
<?php
// src/AppBundle/Form/Type/ExampleType.php
namespace AppBundle\Form\Type;
use AppBundle\Form\ChoiceList\Loader\ExampleChoiceLoader;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ExampleType extends AbstractType
{
/** #var ExampleChoiceLoader */
private $loader;
public function __construct(ExampleChoiceLoader $loader)
{
$this->loader = $loader;
}
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
$builder->add('list', ChoiceType::class, array(
'choice_loader' => $this->loader,
'required' => false,
));
}
// ...
}

Yes, 'choice_list' is deprecated with SYmfony 2.8, but you can use 'choices' instead, which accepts an array as well. From documentation:
The choices option is an array, where the array key is the item's
label and the array value is the item's value.
You must just pay attention than in Symfony 3.0, keys and values are inversed, and in Symfony 2.8, the recommended way is to use the new inversed order, and to specify 'choices_as_values' => true.
So, in the form type:
$builder->add('list', ChoiceType::class, array(
'choices' => new ExampleChoiceList($this->delegate),
'choices_as_values' => true,
'required'=>false));
And in ExampleChoiceList:
protected function loadChoiceList()
{
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$id = $person->getId();
$personsList[(string) $person->getLastname().', '.$person->getFirstname()] = $id; // <== here
}
return new ArrayChoiceList($personsList);
}
UPDATE:
Ok, so I suggest you don't use a ChoiceType at all, but a EntityType, as you seem to fetch all "Persons" from database. And to display "Last Name, First Name" as a label, use the 'choice_label' option. Assuming your entity is called 'Person':
$builder->add('list', EntityType::class, array(
'class' => 'AppBundle:Person',
'choice_label' => function ($person) {
return $person->getLastName() . ', ' . $person->getFirstName();
}
));

Related

Doctrine Array type field not being updated

My question is more or less the same as this one:
How to force Doctrine to update array type fields?
but for some reason the solution didn't work for me, so there must be a detail I am missing and I would be happy if someone could point it out to me.
The context is a Symfony 5.2 app with Doctrine ^2.7 being used.
Entity-Class-Excerpt:
class MyEntity {
// some properties
/**
* #var string[]
* #Groups("read")
* #ORM\Column(type="array")
* #Assert\Valid
*/
protected array $abbreviations = [];
public function getAbbreviations(): ?array
{
return $this->abbreviations;
}
//this is pretty much the same set-function as in the question I referenced
public function setAbbreviations(array $abbreviations)
{
if (!empty($abbreviations) && $abbreviations === $this->abbreviations) {
reset($abbreviations);
$abbreviations[key($abbreviations)] = clone current($abbreviations);
}
$this->abbreviations = $abbreviations;
}
public function addAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $existingAbbreviation) {
if ($abbreviation->equals($abbreviation)) {
return $this;
}
}
$this->abbreviations[] = $abbreviation;
return $this;
}
public function removeAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $i => $existingAbbreviation) {
if ($abbreviation->equals($existingAbbreviation)) {
array_splice($this->abbreviations, $i, 1);
return $this;
}
}
return $this;
}
}
But none of these methods are ever being called (I also tried removing add-/removeAbbreviation only leaving get/set in place).
LocalizableStringEmbeddable being an Embeddable like this:
* #ORM\Embeddable
*/
class LocalizableStringEmbeddable
{
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=false)
*/
private ?string $text;
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=true)
* #Assert\Language
*/
private ?string $language;
//getters/setters/equals/#Assert\Callback
}
By using dd(...) I can furthermore say that in my controller on submit
$myEntity = $form->getData();
yields a correctly filled MyEntity with an updated array but my subsequent call to
$entityManager->persist($myEntity);
$entityManager->flush();
doesn't change the database.
What am I missing?
EDIT: I was asked to give information about the Type I use. It is a custom one that is based on this class. So technically at the base of things I am using a collection type.
abstract class AbstractLocalizableUnicodeStringArrayType extends AbstractType implements DataMapperInterface
{
abstract public function getDataClassName(): string;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('items', CollectionType::class, [
'entry_type' => LocalizedStringEmbeddableType::class,
'entry_options' => [
'data_class' => $this->getDataClassName(),
'attr' => ['class' => 'usa-item-list--item-box'],
],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
])
;
$builder->setDataMapper($this);
}
public function mapDataToForms($viewData, iterable $forms): void
{
$forms = iterator_to_array($forms);
if (null === $viewData) {
return;
}
if (!is_array($viewData)) {
throw new UnexpectedTypeException($viewData, "array");
}
$forms['items']->setData($viewData);
}
public function mapFormsToData(iterable $forms, &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = $forms['items']->getData();
}
}
As I found out this can be solved (in an unelegant way in my opinion but until something better comes around I want to state this) by combining two answers from another post on SO:
https://stackoverflow.com/a/13231876/6294605
https://stackoverflow.com/a/59898632/6294605
This means combining a preflush-hook in your entity class with a "fake-change" of the array in question.
Remark that doing the fake-change in a setter/adder/remover didn't work for me as those are not being called when editing an existing entity. In this case only setters of the changed objects inside the array will be called thus making Doctrine not recognize there was a change to the array itself as no deep-check seems to be made.
Another thing that was not stated in the other thread I wanna point out:
don't forget to annotate your entity class with
#ORM\HasLifecycleCallbacks
or else your preflush-hook will not be executed.

Symfony (2.7) -> Building a form -> pass a value from another field in the form to a custom Constraint

I want to pass a value of some field to my custom Constraint for another field(->to use it in the custom Validator)
Form with some fields:
...
->add('BsaKey', new \app\...\fieldTypes\RadioButtonType(), [
'choices' => [
...
],
'expanded' => true,
'multiple' => false,
...
])
->add('MeteringCodes', 'collection', [
'type' => new \app\...\formTypes\MeteringCodeType(),
'allow_add' => true,
'label' => false,
'options' => ['label' => $this->lang->get('MeteringCode.Caption')],
'constraints' => new \app\...\validators\NoIdenticMeteringCodes()
])
...
Now i need to pass the value of BsaKey to my custom Constraint for the MeteringCodeType:
class MeteringCodeType extends \Symfony\Component\Form\AbstractType
{
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options)
{
$builder->add('meteringCode', 'text', [
'...' => '...',
'constraints' => new \app\...\MeteringCodeConstraint(['param' => 'VALUE_OF_BsaKey'])
]);
}
}
How can i achieve this?
P.S. I'm not using Symfony as a whole, just some standalone Components...
EDIT:
Thx, I found the solution:
class MeteringCodeValidator extends \Symfony\Component\Validator\ConstraintValidator
{
public function validate($value, \Symfony\Component\Validator\Constraint $constraint)
{
$BsaKey = $this->context->getRoot()->get('BsaKey')->getData();
...
}
}
Seems to work independently from the option returned by "getTargets()" function.
I have done something simular using a custom constraint.
Mine checks that one field is greater then the other, feel free to change it to your requirements.
Sample code;
<?php
// src/AppBundle/Validator/Constraints/FieldCompare.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class FieldCompare extends Constraint
{
/**
* Error Message
* #access public
* #var string - with placeholders [field1,field2]
*/
public $message = 'Field field2 must be greater than field1 ';
/**
* Form fields
* #access public
* #var array
*/
public $fields = array();
/**
* Class accessors (getters) for the fields.
* #access public
* #var array
*/
public $properties = array();
/**
* Error Path
* #var string
*/
public $errorPath;
public function __construct($options = null)
{
parent::__construct($options);
// check fields is an array
if (!is_array($this->fields) && !is_string($this->fields)) {
throw new UnexpectedTypeException($this->fields, 'array');
}
// make sure there are two of them
if (2 != count($this->fields)) {
throw new ConstraintDefinitionException("Two fields must be specified.");
}
// make sure they are strings
foreach ($this->fields as $f) {
if (null !== $this->errorPath && !is_int()) {
throw new UnexpectedTypeException($this->errorPath, 'integer or null');
}
}
}
/**
* getTargets()
*
* Set traget (so can be used against the class).
* #access public
* #return type
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
<?php
// src/AppBundle/Validator/Constraints/FieldCompareValidator.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class FieldCompareValidator extends ConstraintValidator {
public function validate($protocol, Constraint $constraint)
{
$fields = (array) $constraint->fields;
$properties = (array) $constraint->properties;
if ($protocol->$properties[0]() >= $protocol->$properties[1]()) {
$this->context->addViolationAt($fields[1], $constraint->message,
array(
'field1' => $this->prettyField($fields[0]),
'field2' => $this->prettyField($fields[1])
), null);
}
}
private function prettyField($field)
{
if (strstr($field, '_')) {
$pretty = str_replace('_', ' ', $field);
} else {
// is camelCase
$converted = preg_replace('/(?!^)[[:upper:]]+/',' \0', $field);
if (is_array($converted)) {
$pretty = implode(' ', $converted);
} else {
$pretty = $converted;
}
}
return ucwords($pretty);
}
}
Here is how I aplied the validator (yaml format);
AppBundle\Model\Foo:
constraints:
- AppBundle\Validator\Constraints\FieldCompare:
fields: [min_lead_time, max_lead_time]
properties: [getminLeadtime, getmaxLeadtime]
groups: [add]

Symfony2: Entity form field with empty value

i have a form definition which uses the so-far great field type entity. With the option query_builder I select my values and the are displayed.
The sad part is, I am required to display a null default value, like all (it's a filter form). I don't like the choices option of entity because I have database values and a FormType shouldn't query the database.
My approach so far was to implement a custom field type which extends entity and adds a null entry to the top of the list. The field type is loaded and used but unfortunately the dummy value is not displayed.
The field definition:
$builder->add('machine', 'first_null_entity', [
'label' => 'label.machine',
'class' => Machine::ident(),
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('m')
->where('m.mandator = :mandator')
->setParameter('mandator', $this->mandator)
->orderBy('m.name', 'ASC');
}
]);
The form type definition:
class FirstNullEntityType extends AbstractType
{
/**
* #var unknown
*/
private $doctrine;
public function __construct(ContainerInterface $container)
{
$this->doctrine = $container->get('doctrine');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired('query_builder');
$resolver->setRequired('class');
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$class = $options['class'];
$repo = $this->doctrine->getRepository($class);
$builder = $options['query_builder']($repo);
$entities = $builder->getQuery()->execute();
// add dummy entry to start of array
if($entities) {
$dummy = new \stdClass();
$dummy->__toString = function() {
return '';
};
array_unshift($entities, $dummy);
}
$options['choices'] = $entities;
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'entity';
}
}
Here is what works in Symfony 3.0.3
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$builder->add('example' EntityType::class, array(
'label' => 'Example',
'class' => 'AppBundle:Example',
'placeholder' => 'Please choose',
'empty_data' => null,
'required' => false
));
You can use placeholder from 2.6
An alternative approach would be to use a ChoiceList with choices that are generated from the database and then use that in a custom choice form type that will allow for an empty_value.
Choice List
namespace Acme\YourBundle\Form\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class MachineChoiceList extends LazyChoiceList
{
protected $repository;
protected $mandator;
public function __construct(ObjectManager $manager, $class)
{
$this->repository = $manager->getRepository($class);
}
/**
* Set mandator
*
* #param $mandator
* #return $this
*/
public function setMandator($mandator)
{
$this->mandator = $mandator;
return $this;
}
/**
* Get machine choices from DB and convert to an array
*
* #return array
*/
private function getMachineChoices()
{
$criteria = array();
if (null !== $this->mandator) {
$criteria['mandator'] = $this->mandator;
}
$items = $this->repository->findBy($criteria, array('name', 'ASC'));
$choices = array();
foreach ($items as $item) {
$choices[** db value **] = ** select value **;
}
return $choices;
}
/**
* {#inheritdoc}
*/
protected function loadChoiceList()
{
return new SimpleChoiceList($this->getMachineChoices());
}
}
Choice List Service (YAML)
acme.form.choice_list.machine:
class: Acme\YourBundle\Form\ChoiceList\MachineChoiceList
arguments:
- #doctrine.orm.default_entity_manager
- %acme.model.machine.class%
Custom Form Type
namespace Acme\YourBundle\Form\Type;
use Acme\YourBundle\Form\ChoiceList\MachineChoiceList;
..
class FirstNullEntityType extends AbstractType
{
/**
* #var ChoiceListInterface
*/
private $choiceList;
public function __construct(MachineChoiceList $choiceList)
{
$this->choiceList = $choiceList;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceList = $this->choiceList;
$resolver->setDefault('mandator', null);
$resolver->setDefault('choice_list', function(Options $options) use ($choiceList) {
if (null !== $options['mandator']) {
$choiceList->setMandator($options['mandator']);
}
return $choiceList;
});
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'choice';
}
}
Custom Form Type Service (YAML)
acme.form.type.machine:
class: Acme\YourBundle\Form\Type\FirstNullEntityType
arguments:
- #acme.form.choice_list.machine
tags:
- { name: form.type, alias: first_null_entity }
In Your Form
$builder
->add('machine', 'first_null_entity', [
'empty_value' => 'None Selected',
'label' => 'label.machine',
'required' => false,
])
;

Symfony2 DataTransformer for choice field

I'm trying to create a custom choice list field.
And almost all seems working, except the preselected values on the edit part.
Basically i'm creating a mixed list field with multiple object type (the backend is mongodb), i know that is a dirty way to operate but i didn't find a better solution (keeping things simple).
The process is working, i have a mixed objects in the backend and i can choose which one in the edit form, but the form doesn't show the preselected (with the values extracted from mongo)
<?php
namespace www\DefaultBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use www\DefaultBundle\Form\DataTransformer\AccessorioTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AccessorioType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new AccessorioTransformer($this->om);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choices = array();
$data = array();
$documents = array(
'Document1',
'Document2',
'Document3',
);
foreach ($documents as $document)
{
$objects = $this->om->getRepository('wwwDefaultBundle:' . $document)->findAll();
foreach ($objects as $object)
{
if (#!$object->getId()) print_r($object);
$key = sprintf("%s_%s", $object->getId(), basename(str_replace('\\', '/', get_class($object))));
$value = sprintf("%s (%s)", $object, basename(str_replace('\\', '/', get_class($object))));
$choices[$key] = $value;
}
}
$resolver->setDefaults(array(
'choices' => $choices,
'expanded' => false,
'multiple' => true,
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'accessorio';
}
}
the datatransformer:
<?php
namespace www\DefaultBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\ObjectChoiceList;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;
class AccessorioTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function transform($values)
{
return array();
// i tried everything here but none working
}
public function reverseTransform($values)
{
if (!$values) return null;
$array = array();
foreach ($values as $value)
{
list($id, $type) = explode("_", $value);
$array[] = $this->om->getRepository('wwwDefaultBundle:' . $type)->find($id);
}
return $array;
}
}
the form builder:
<?php
namespace www\DefaultBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ValvolaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// [snip]
->add('ref',
'accessorio',
array(
'label_attr' => array('class' => 'control-label col-sm-2'),
'attr' => array('class' => 'form-control '),
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'www\DefaultBundle\Document\Valvola',
'attr' => array('class' => 'press form-horizontal'),
));
}
public function getName()
{
return 'www_defaultbundle_valvolatype';
}
}
Did someone faced the same problem? How "choice" field should be transformed? How managed mixed object in the same field?
Someone can enlighten me?
Regards
I finally found the solution. The culprit was the datatransformer (and mine, of course :-))
In this way the form shows the preselected values:
<?php
namespace www\DefaultBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\ObjectChoiceList;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;
class AccessorioTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function transform($values)
{
if ($values === null) return array();
$choices = array();
foreach ($values as $object)
{
$choices[] = sprintf("%s_%s", $object->getId(), basename(str_replace('\\', '/', get_class($object))));
}
return $choices;
}
public function reverseTransform($values)
{
if (!$values) return array();
$array = array();
foreach ($values as $value)
{
list($id, $type) = explode("_", $value);
$array[] = $this->om->getRepository('wwwDefaultBundle:' . $type)->find($id);
}
return $array;
}
}
In most cases using ChoiceType the array returned by the transform function should contain the ID's only. For example:
public function transform($objectsArray)
{
$choices = array();
foreach ($objectsArray as $object)
{
$choices[] = $object->getId();
}
return $choices;
}
Although it might not be the answer to the original post, I am pretty sure that googlers get here and look for this hint.

Nested Symfony2 Forms: $options['data'] = null in nested form?

As the documentation around this topic is somewhat thin, I got to a dead end.
I have two models: Job and JobAttribute.
A Job has many JobAttributes and a JobAttribute has one Job:
class Job {
/**
* #ORM\OneToMany(targetEntity="JobAttribute", mappedBy="job_attributes")
*
* #var ArrayCollection
*/
private $attributes;
}
class JobAttribute {
/**
* #ORM\Column(name="type", type="string", length=50)
*
* #var string
*/
private $type;
/**
* #ORM\ManyToOne(targetEntity="Job", inversedBy="jobs")
*/
private $job;
Now,I have the following FormClass:
class JobType extends AbstractType {
public function buildForm(FormBuilder $f, array $options) {
$f->add('name', 'text');
$f->add('attributes', 'collection', array('type' => new JobAttributeType()));
}
public function getName() {
return 'job';
}
}
class JobAttributeType extends AbstractType {
public function buildForm(FormBuilder $f, array $options) {
$attribute = $options['data'];
$f->add('value', $attribute->getType());
}
public function getDefaultOptions(array $options) {
return array('data_class' => 'JWF\WorkflowBundle\Entity\JobAttribute');
}
public function getName() {
return 'job_attribute';
}
}
Yes, indeed, the type property of JobAttribute contains a Form field type, eg. text.
So, as I call a FormBuilder on JobType in my Controller, $options['data'] is correctly populated with a Job-Object within JobType.
But the nested JobAttributeType's $options['data'] doesn't point to an JobAttribute object. It's NULL.
What's the problem? Where is the association lost? Why is $options['data'] = NULL in nested forms?
Is there a workaround in order to get dynamic field types (out of Doctrine) in a nested form?
Thanks in advance!
You cannot rely on $options['data'] when you build the form, as the data can (and will) be changed anytime after building. You should use event listeners instead.
$formFactory = $builder->getFormFactory();
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formFactory) {
$form = $event->getForm();
$data = $event->getData();
if ($data instanceof JobAttribute) {
$form->add($formFactory->createNamed('value', $data->getType());
}
});
The documentation for this can be found in the cookbook.

Categories