Symfony reusable AJAX select / ChoiceType - php

I want to create a reusable AJAX-based select (select2) using Symfony form types and I've spent quite some time on it but can't get it to work like I want.
As far as I know you cannot override options of form fields after they have been added, so you have to re-add them with the new config. The Symfony docs also provide some examples on how to dynamically add or modify forms using events. https://symfony.com/doc/current/form/dynamic_form_modification.html
I've managed to create my AJAX based elements in the form and it's working but not completely reusable yet:
Form field extends Doctrine EntityType to have full support of data mappers etc
Form field is initialized with 'choices' => [], so Doctrine does not load any entities from db
Existing choices on edit is added during FormEvents::PRE_SET_DATA
Posted choices are added during FormEvents::PRE_SUBMIT
The current setup works but only in the parent form. I want to have my AjaxNodeType completely reusable so the parent form does not need to care about data handling and events.
Following the 2 examples, parent and child form. Here with event listeners in both, but of course they should only be in one.
Is it simply not possible in single element types to replace "yourself" in the parent form or am I doing something wrong? Is there any other way to dynamically change the choices?
Parent form
This works!
class MyResultElementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'fixedNode',
AjaxNodeType::class,
[
'label' => 'Fixed node',
'required' => false,
]
);
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData()->getFixedNode();
//this works here but not in child???
if ($fixedNode) {
$name = 'fixedNode';
$parentForm = $event->getForm();
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$fixedNode];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
$data = $event->getData()['fixedNode'];
if ($data) {
$name = 'fixedNode';
$parentForm = $event->getForm();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => MyResultElement::class,
'method' => 'POST',
]
);
}
}
Child form / single select This does NOT work. On POST the fixedNode field is not set to the form data-entity.
class AjaxNodeType extends AbstractType
{
/** #var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//this does not work here but in parent???
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData();
$name = $event->getForm()->getName();
$parentForm = $event->getForm()->getParent();
$options = $parentForm->get($name)->getConfig()->getOptions();
$newChoices = [$fixedNode];
// check if the choices already match, otherwise we'll end up in an endless loop ???
if ($options['choices'] !== $newChoices) {
$options['choices'] = $newChoices;
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
if ($event->getData()) {
$name = $event->getForm()->getName();
$data = $event->getData();
$parentForm = $event->getForm()->getParent();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, self::class, $options);
}
},
1000
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => Node::class,
// prevent doctrine from loading ALL nodes
'choices' => [],
]
);
}
public function getParent(): string
{
return EntityType::class;
}
}

Again, answering my own Questions :)
After spending some more time on it I got a working solution. And maybe it will be helpful for someone.
I've switched from EntityType to ChoiceType as it only made things more complicated and is not actually needed
multiple and single selects need different settings/workarrounds (like by_reference), see the line comments below
re-adding yourself to the parent form works, I don't know why it did not before...
beware of endless-loops when re-adding / re-submitting values
The main re-usable AjaxEntityType that does all the logic without reference to any specific entity:
<?php
namespace Tool\Form\SingleInputs;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AjaxEntityType extends AbstractType
{
/** #var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// shamelessly copied from DoctrineType - this is needed to support Doctrine Collections in multi-selects
if ($options['multiple'] && interface_exists(Collection::class)) {
$builder
->addEventSubscriber(new MergeDoctrineCollectionListener())
->addViewTransformer(new CollectionToArrayTransformer(), true);
}
// PRE_SET_DATA is the entrypoint on form creation where we need to populate existing choices
// we process current data and set it as choices so it will be rendered correctly
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) use($options) {
$data = $event->getData();
$hasData = ($options['multiple'] && count($data) > 0) || (!$options['multiple'] && $data !== null);
if ($hasData) {
$entityOrList = $event->getData();
$name = $event->getForm()->getName();
$parentForm = $event->getForm()->getParent();
$options = $parentForm->get($name)->getConfig()->getOptions();
// ONLY do this if the choices are empty, otherwise readding PRE_SUBMIT will not work because this is called again!
if(empty($options['choices'])) {
if($options['multiple']) {
$newChoices = [];
foreach ($entityOrList as $item) {
$newChoices[$item->getId()] = $item;
}
} else {
$newChoices = [$entityOrList->getId() => $entityOrList];
}
$options['choices'] = $newChoices;
$parentForm->add($name, self::class, $options);
}
}
},
1000
);
// PRE_SUBMIT is the entrypoint where we need to process the submitted values
// we have to add the POST-ed choices, otherwise this field won't be valid
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use($options) {
$entityIdOrList = $event->getData();
$entityClass = $options['class'];
// new choices constructed from POST
$newChoices = [];
if ($options['multiple']) {
foreach ($entityIdOrList as $id) {
if ($id) {
$newChoices[$id] = $this->entityManager->find($entityClass, $id);
}
}
} elseif ($entityIdOrList) {
$newChoices = [$entityIdOrList => $this->entityManager->find($entityClass, $entityIdOrList)];
}
$name = $event->getForm()->getName();
$parentform = $event->getForm()->getParent();
$currentChoices = $event->getForm()->getConfig()->getOptions()['choices'];
// if the user selected/posted new choices that have not been in the existing list, add them all
if ($newChoices && count(array_diff($newChoices, $currentChoices)) > 0) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = $newChoices;
// re-add ourselves to the parent form with updated / POST-ed options
$parentform->add($name, self::class, $options);
if(!$parentform->get($name)->isSubmitted()) {
// after re-adding we also need to re-submit ourselves
$parentform->get($name)->submit($entityIdOrList);
}
}
}, 1000);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'mapped' => true,
'choice_value' => 'id',
'choice_label' => 'selectLabel',
'choices' => [],
'attr' => [
'class' => 'select2-ajax',
],
'ajax_url' => null,
]
);
// AJAX endpoint that is select2 compatible
// https://select2.org/data-sources/ajax
$resolver->setRequired('ajax_url');
$resolver->setAllowedTypes('ajax_url', ['string']);
// entity class to process
$resolver->setRequired('class');
// by_reference needs to be true for single-selects, otherwise our entities will be cloned!
// by_reference needs to be false for multi-selects, otherwise the setters wont be called for doctrine collections!
$resolver->setDefault('by_reference', function (Options $options) {
return !$options['multiple'];
});
// adds the ajax_url as attribute
$resolver->setNormalizer('attr', function (Options $options, $value) {
$value['data-custom-ajax-url'] = $options['ajax_url'];
return $value;
});
}
public function getParent(): string
{
return ChoiceType::class;
}
}
Actual usage with specific entity and ajax endpoint:
<?php
namespace Tool\Form\SingleInputs;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Tool\Entities\User\Node;
class AjaxNodeType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => Node::class,
'ajax_url' => 's/ajax-select/nodes',
]
);
}
public function getParent(): string
{
return AjaxEntityType::class;
}
}

Related

symfony: access controller variables from form

Starting with Symfony is quite a learning curve. Even after reading for hours, I cannot get across this presumably simple problem. I want to load a choices-form with values from an entity.
Controller:
namespace AppBundle\Controller
class ItemController extends Controller
{
public function itemAction (Request $request)
{
$myItems = new Itemlist();
//some statements to fill $myItems
$form = $this->createForm (AllitemsType::class, $myItems);
// some more stuff
return $this->render (...);
}
}
Entity:
namespace AppBundle\Entity;
class Itemlist
{
protected $choices;
protected $defaultvalue;
public function __construct ()
{
$choices = array();
}
// all the get and set-methods to fill/read the $choices array and $defaultvalue
}
Form:
namespace AppBundle\Form
class AllitemsType extends AbstractType
{
public function buildForm (FormBuilderInterface $builder, array $options)
{
// and here is my problem: how can I fill next two lines with values from the Itemlist-Entity?
// The Itemlist instance has been build in the controller and is unknown here
$items = ??? // should be 'AppBundle\Entity\Itemlist->$choices
$defaultitem = ??? // should be 'AppBundle\Entity\Itemlist->$defaultvalue
$choices_of_items = array (
'choices' => $items,
'expanded' => true,
'multiple' => false,
'data' => $defaultitem,
);
$builder->add ('radio1', ChoiceType::class, $choices_of_items);
}
}
Any help appreciated,
Wolfram
$builder->add('choices', ChoiceType::class);
should be sufficient as you're binding an entity to the form, the process of getting values and setting them back is automatic. Of course you need to have setter and getter for choices field in AllitemsType
To give a complete answer - part above is the so called "best practice one" - you can also choose one of the following
$items = $options['data'];
or
$builder->addEventListener(
FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$allItems = $event->getData();
$form = $event->getForm();
$form->add('radio1', ChoiceType::class, [
'choices' => $allItems
]);
});
Second one should be preferred as, in options['data'], entity could change during form event's lifetime.
Pass variables with createForm object.
Controller:
namespace AppBundle\Controller
class ItemController extends Controller
{
public function itemAction (Request $request)
{
$myItems = new Itemlist();
$formVars = array("items" => array(1,2,3,4,6), "defaultItems" => 2); // Store variables
^^
//some statements to fill $myItems
$form = $this->createForm (new AllitemsType($formVars), $myItems);
^^
// some more stuff
return $this->render (...);
}
}
Now create constructor in form and set class variables items and defaultitem in form.
Form:
namespace AppBundle\Form
class AllitemsType extends AbstractType
{
$this->items = array();
$this->defaultitem = 0;
public function __construct($itemArr)
{
$this->items = $itemArr['items'];
$this->defaultitem = $itemArr['defaultItems'];
}
public function buildForm (FormBuilderInterface $builder, array $options)
{
$choices_of_items = array (
'choices' => $this->items, // User class variable
'expanded' => true,
'multiple' => false,
'data' => $this->defaultitem, // User class variable
);
$builder->add ('radio1', ChoiceType::class, $choices_of_items);
}
}
It should solve your problem.

Symfony2 - Condition in FormType

I have a class RegisterType for my form:
<?php
class RegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('price');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Register'
));
}
public function getName()
{
return 'register';
}
}
?>
The Register Entity has a relation with an Entity called Product
I would like to add this condition to the buildForm function:
<?php
if ($product->getTitle() == 'SuperProduct') {
$builder->add('amount', 'money', [
'required' => FALSE,
]);
}
?>
If the product title has the value 'SuperProduct' then I add a field in the the form.
But I have no idea about the syntax I have to use to call another Entity value in a FormType.
Thanks in advance for your help.
Ah good you've implemented your FormType as an independent class rather than in a controller, so what I'd do is pass in your product entity as an option to a private variable within your FormType class. I'm not sure what class your product is or the name space it resides in, so you'll have to fill that in.
I've also written this in the expectation that you could have more than one option to pass through in the future, so it'll be useful to other FormType classes you write. Each variable you define in the class scope can be set when you create an instance of the FormType class. Be sure to initialise these variables as false instead of null, or the code in the constructor function won't see them.
<?php
class RegisterType extends AbstractType
{
private $product = false;
public function __construct(array $options = [])
{
foreach ($options as $name => $value) {
if (isset($this->$name)) {
$this->$name => $value;
}
}
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
;
if ($this->product && is_a("\Namespace\To\Your\Entity\Called\Product", $this->product)) {
if ($this->product->getTitle() == 'SuperProduct') {
$builder
->add('amount', 'money', [
'required' => FALSE,
])
;
}
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Register'
));
}
public function getName()
{
return 'register';
}
}
?>
That "is_a()" function checks that the product variable passed in through the form type class constructor really is an instance of your product entity, so you can be sure that the getTitle() function exists.
Now when you create a new instance of your RegisterType class, you need to pass through an options array including your product. Two conceptual variable names for you to rename here.
<?php
$form = $this->createForm(new RegisterType([
"product" => $instanceOfProductEntityOrNull,
]), $theEntityTheFormWillBeHandling);
?>
Try something like this:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$product = $event->getData();
$form = $event->getForm();
if ($product->getTitle() == 'SuperProduct') {
$form->add('name', 'text');
}
});
And do not forget the specific use
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

Symfony 2 Transformer on entity form type

I'm trying to create a new form type in Symfony 2. It is based on entity type, it uses select2 on frontend and I need the user to be able to select existing entity or create the new one.
My idea was to send entity's id and let it to be converted by the default entity type if user select existing entity or send something like "_new:entered text" if user enter new value. Then this string should be converted to the new form entity by my own model transformer, which should look something like this:
<?php
namespace Acme\MainBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class EmptyEntityTransformer
implements DataTransformerInterface
{
private $entityName;
public function __construct($entityName)
{
$this->entityName = $entityName;
}
public function transform($val)
{
return $val;
}
public function reverseTransform($val)
{
$ret = $val;
if (substr($val, 0, 5) == '_new:') {
$param = substr($val, 5);
$ret = new $this->entityName($param);
}
return $ret;
}
}
Unfortunately, the transformer is only called when existing entity is selected. When I enter a new value, the string is sent in the request but transformer's reverseTransform method is not called at all.
I'm new to Symfony so I don't even know if this approach is correct. Do you have any Idea how to solve this?
edit:
My form type code is:
<?php
namespace Acme\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Acme\MainBundle\Form\DataTransformer\EmptyEntityTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class Select2EntityType
extends AbstractType
{
protected $router;
public function __construct(Router $router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->setDefaults(array(
'placeholder' => null,
'path' => false,
'pathParams' => null,
'allowNew' => false,
'newClass' => false,
));
}
public function getParent()
{
return 'entity';
}
public function getName()
{
return 's2_entity';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['newClass']) {
$transformer = new EmptyEntityTransformer($options['newClass']);
$builder->addModelTransformer($transformer);
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$field = $view->vars['name'];
$parentData = $form->getParent()->getData();
$opts = array();
if (null !== $parentData) {
$accessor = PropertyAccess::createPropertyAccessor();
$val = $accessor->getValue($parentData, $field);
if (is_object($val)) {
$getter = 'get' . ucfirst($options['property']);
$opts['selectedLabel'] = $val->$getter();
}
elseif ($choices = $options['choices']) {
if (is_array($choices) && array_key_exists($val, $choices)) {
$opts['selectedLabel'] = $choices[$val];
}
}
}
$jsOpts = array('placeholder');
foreach ($jsOpts as $jsOpt) {
if (!empty($options[$jsOpt])) {
$opts[$jsOpt] = $options[$jsOpt];
}
}
$view->vars['allowNew'] = !empty($options['allowNew']);
$opts['allowClear'] = !$options['required'];
if ($options['path']) {
$ajax = array();
if (!$options['path']) {
throw new \RuntimeException('You must define path option to use ajax');
}
$ajax['url'] = $this->router->generate($options['path'], array_merge($options['pathParams'], array(
'fieldName' => $options['property'],
)));
$ajax['quietMillis'] = 250;
$opts['ajax'] = $ajax;
}
$view->vars['options'] = $opts;
}
}
and then I create this form type:
class EditType
extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('masterProject', 's2_entity', array(
'label' => 'Label',
'class' => 'MyBundle:MyEntity',
'property' => 'name',
'path' => 'my_route',
'pathParams' => array('entityName' => 'name'),
'allowNew' => true,
'newClass' => '\\...\\MyEntity',
))
...
Thanks for your suggestions
I think I found an answer however I'm not really sure if this is the correct solution. When I tried to understand how EntityType works I noticed that it uses EntityChoiceList to retrive list of available options and in this class there is a getChoicesForValues method which is called when ids are transformed to entities. So I implemented my own ChoiceList which adds my own class to the end of the returned array:
<?php
namespace Acme\MainBundle\Form\ChoiceList;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class EmptyEntityChoiceList
extends EntityChoiceList
{
private $newClassName = null;
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null, $newClassName = null)
{
parent::__construct($manager, $class, $labelPath, $entityLoader, $entities, $preferredEntities, $groupPath, $propertyAccessor);
$this->newClassName = $newClassName;
}
public function getChoicesForValues(array $values)
{
$ret = parent::getChoicesForValues($values);
foreach ($values as $value) {
if (is_string($value) && substr($value, 0, 5) == '_new:') {
$val = substr($value, 5);
if ($this->newClassName) {
$val = new $this->newClassName($val);
}
$ret[] = $val;
}
}
return $ret;
}
}
Registering this ChoiceList to the form type is a bit complicated because the class name of original choice list is hardcoded in the DoctrineType which EntityType extends but it is not difficult to understand how to do it if you have a look into this class.
The reason why DataTransformer is not called probably is that EntityType is capable to return array of results and transform is applied to every item of this collection. If the result array is empty, there is obviously no item to call transformer on.
I had exactly the same question as yours, I chose to use an FormEvent with still a DataTransformer
The idea is to switch the field type (the entity one) just before the submit.
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (substr($data['project'], 0, 5) == '_new:') {
$form->add('project', ProjectCreateByNameType::class, $options);
}
}
This will replace the project field with a new custom one before the submit if needed.
ProjectCreateByNameType can extend a TextField and have to add the DataTransformer.

Symfony2 custom form type key pair

I'm currently trying to implement a key-pair value for a form type, which is used together with the FOSRestBundle to allow for sending a request like the following:
{
"user": {
"username": "some_user",
"custom_fields": {
"telephone": "07777",
"other_custom_field": "other custom value"
}
}
}
The backend for this is represented as follows:
User
id, username, customFields
CustomUserField
id, field
CustomUserFieldValue
user_id, field_id, value
I've currently made a custom form as follows:
<?php
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username')
->add(
'custom_fields',
'user_custom_fields_type'
)
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Acme\DemoBundle\Entity\User',
'csrf_protection' => false,
)
);
}
public function getName()
{
return 'user';
}
}
And my user_custom_fields_type:
<?php
class CustomUserFieldType extends AbstractType
{
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$fields = $this->em->getRepository('AcmeDemoBundle:CustomUserField')->findAll();
foreach($fields as $field) {
$builder->add($field->getField(), 'textarea');
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'invalid_message' => 'The selected custom field does not exist'
)
);
}
public function getParent()
{
return 'collection';
}
public function getName()
{
return 'user_custom_fields_type';
}
}
This keeps giving me the error that there are extra fields. Which are the ones I've added in the CustomUserFieldType. How can I get this working?
Note: this is a simplified version of the actual code, I've tried removing all the irrelevant code.
You need to use form listeners to add dynamic fields to your forms:
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
In your case a PRE_SET_DATA should suffice. Something like this:
$em = $this->em;
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ( $em )
{
// get the form
$form = $event->getForm();
// fetch your data here
$fields = $em->getRepository('AcmeDemoBundle:CustomUserField')->findAll();
foreach($fields as $field)
{
// make sure you add the new fields to $form and not $builder from this event
$form->add($field->getField(), 'textarea');
}
});
I had exactly the same issue and finally solved it by parsing the custom fields manually. If there is another solution, please share :)
In the UserType form:
$builder->addEventListener(FormEvents::POST_SET_DATA,
function (FormEvent $event) use ( $oEm, $oUser )
{
$oForm = $event->getForm();
$aFields = $oEm->getRepository('MyDBBundle:CustomUserField')->findAll();
/** #var CustomUserField $oField */
foreach($aFields as $oField)
{
$oForm->add(
'custom__'.$oField->getKey(),
$oField->getType(),
array(
'label' => $oField->getField(),
'mapped' => false,
'required' => false
)
);
/** #var CustomUserFieldValue $oFieldValue */
$oFieldValue = $oEm->getRepository('MyDBBundle:CustomUserFieldValue')->findOneBy(array('user' => $oUser, 'field' => $oField));
if(null !== $oFieldValue) {
$oForm->get('custom__' . $oField->getKey())->setData($oFieldValue->getValue());
}
}
}
);
Then, in your controller action which handles the request of the submitted form:
// Handle custom user fields
foreach($oForm->all() as $sKey => $oFormData)
{
if(strstr($sKey, 'custom__'))
{
$sFieldKey = str_replace('custom__', '', $sKey);
$oField = $oEm->getRepository('MyDBBundle:CustomUserField')->findOneBy(array('key' => $sFieldKey));
/** #var CustomUserFieldValue $oFieldValue */
$oFieldValue = $oEm->getRepository('MyDBBundle:CustomUserFieldValue')->findOneBy(array('user' => $oEntity, 'field' => $oField));
if($oFieldValue === null)
{
$oFieldValue = new CustomUserFieldValue();
$oFieldValue->setUser($oEntity);
$oFieldValue->setField($oField);
}
$oFieldValue->setValue($oFormData->getData());
$oEm->persist($oFieldValue);
}
}
(Assuming that there is both a "field" property and a "key" in the CustomUserField entity; key is a unique, spaceless identifier for your field and field is the human friendly and readable field label.)
This works so hope it can be helpful. However, wondering if someone has a better solution. :)

Accessing an embedded field in a collectionType in form

What I want to do :
I'm trying to access a field from an embedded formType in a collection.
I can easily access the first level (so getting the collection) with $form->get('childType') but I struggle to access the field embedded in childType.
I tried $form->get('childType')->get('anotherAttr') with no success. IMHO the problem comes from the fact that a Collection is not just a field, and getting('anotherAttr') can't be done without Symfony knowing on what item of the collection I want to do this get. Anyways after a lot of search I haven't find how to tell him I want the first item from the collection.
Here is the code :
The parent class type :
<?php
namespace my\myBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ParentType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('attribute1','text',array("label" => 'attribute 1 :'))
->add('childType','collection',array('type' => new ChildType($options['attrForChild'])));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'my\myBundle\Entity\Parent',
'attrForChild' => null
);
}
public function getName()
{
return 'my_mybundle_childtype';
}
}
The childClassType :
<?php
namespace my\myBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ChildType extends AbstractType
{
private $childAttr;
public function __construct($childAttr=null){
$this->childAttr=$childAttr;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('childAttr','text',array("label" => 'childAttr : ','property_path' => false));
if(isset($this->childAttr)){
$childAttr = $this->childAttr;
$builder->add('childAttrDependantEntity','entity',array("label" => 'RandomStuff : ',
'class' => 'mymyBundle:randomEntity',
'property' => 'randProperty',
'multiple' => false,
'query_builder' => function(\my\myBundle\Entity\randomEntityRepository $r) use ($childAttr) {
return $r->findByChildAttr($childAttr);
}
));
}
$builder->add('anotherAttr','text',array("label" => 'Other attr : '))
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'crri\suapsBundle\Entity\Adresse',
'childAttr' => null
);
}
public function getName()
{
return 'my_mybundle_childtype';
}
}
Also, does the childAttr solution I use is ok? (it is working, but it feels a bit as a hack, is there a cleaner way to do the same thing?). What it is used for = the user gives me a text field, I verify if it exists in database, if it does exist, I add an entityType to the form which is related to this attribute. The goal is that the user will select from a restrain list of elements, instead of all the elements from the database.
EDIT : the controller's corresponding code :
public function parentTypeAddAction(Request $request){
$parentEntity = new ParentEntity();
$parentEntity->addChildEntity(new ChildEntity());
$form = $this->createForm(new ParentType,$parentEntity);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
// Testing (everything I tried)
$test=$form->get('childType')->getAttribute('childAttr');
/**
$test=$form['childAttr'];
$test=$form->get('childAttr'); **/
return $this->container->get('templating')->renderResponse('myMyBundle:Default:test.html.twig',
array('test' => $test));
if($test!=null ){
$anEntity = $em->getRepository('crrisuapsBundle:AnEntity')->find($test);
if($anEntity==null){
$form->get('childType')->get('childAttr')->addError(new FormError("Invalid attribute."));
} else {
$form = $this->createForm(new ParentType,$parentType,array('childAttr' => $test));
$individu->getAdresses()->first()->setAnEntity($anEntity);
}
}
$form->bindRequest($request);
if($request->request->get('CHILDATTRPOST')!='Search attribute'){
if ($form->isValid()) {
$em->persist($parentType);
$em->persist($individu->getChildEntity()->first());
$em->flush();
return $this->redirect($this->generateUrl('myMyBundle_homepage'), 301);
}
}
}
return $this->container->get('templating')->renderResponse('myMyBundle:Default:parentTypeAdd.html.twig',
array('form' => $form->createView()));
}
Thanks to cheesemacfly's suggestions I could figure out how to get it. Here is the solution :
//Getting the childEntities forms as an array
$childArray=$form->get('childType')->getChildren();
//Getting the childEntity form you want
$firstChild=$childArray[0];
//Getting your attribute like any form
$childAttrForm=$childArray[0]->get('childAttr');
$childAttr=$childAttrForm->getData();

Categories