manage field population in edit form - php

I'm using symfony 4 and I have this form:
<?php
namespace App\Form;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use App\Entity\TypeParking;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TypeParkingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('libelle')
->add('tempsmax')
->add('jourdebut')
->add('jourfin')
->add('Exception_Name', TextType::class, ['property_path' => 'exception[name]'])
->add('Starting_date', DateType::class, [
'property_path' => 'exception[datedebut]',
])
->add('Ending_date', DateType::class, [
'property_path' => 'exception[datefin]',
])
->add('Starting_time', TimeType::class, ['property_path' => 'exception[heuredebut]'])
->add('Ending_time', TimeType::class, ['property_path' => 'exception[heurefin]'])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => TypeParking::class,
]);
}
}
When I create a new form I usually take the values of Exception_Name,Starting_date,Ending_date,Starting_time and Ending_time manually and put them into a single json array in a single database field.
However when I go to edit the form, the data inside the json doesn't get divided to populate each of the fields.
let's say that I have this json array: {"Exceptions": {"name": "6fdfs", "StartDate": "2015-03-03", "StartHour": "00:00:00", "EndingDate": "2015-03-03", "EndingHour": "00:00:00"}}
I'm gonna take the name value and use it to populate the Exception_Name field, etc...
TL;DR: How can I control how I want to pre-populate each field of the edit form
Edit:
this is my datamapper
<?php
// src/Form/DataMapper/ColorMapper.php
namespace App\Form\DataMapper;
use App\Painting\Color;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormInterface;
final class TypeParkingMapper implements DataMapperInterface
{
/**
* #param TypeParking|null $data
*/
public function mapDataToForms($data, $forms)
{
// there is no data yet, so nothing to prepopulate
if (null === $data) {
return;
}
/** #var FormInterface[] $forms */
$forms = iterator_to_array($forms);
// initialize form field values
$Excep=$data->getException();
$forms['Exception_Name']->setData($Excep['Exceptions']['name']);
$forms['Starting_date']->setData($Excep['Exceptions']['StartDate']);
$forms['Ending_date']->setData($Excep['Exceptions']['EndingDate']);
$forms['Starting_time']->setData($Excep['Exceptions']['StartHour']);
$forms['Ending_time']->setData($Excep['Exceptions']['EndingHoure']);
}
public function mapFormsToData($forms, &$data)
{
/** #var FormInterface[] $forms */
$forms = iterator_to_array($forms);
// as data is passed by reference, overriding it will change it in
// the form object as well
// beware of type inconsistency, see caution below
$data = new TypeParking(
$forms['Exception_Name']->getData()
);
}
}
So it works fine when I edit an object (since getException() returns the json array from my database), but when I create a new object it throws an error since there's no data to get.
So is there any way I can turn the mapper off when I create a new form and only activate it when editing a form ?
the error is : "Notice: Undefined index: Exceptions"

You could write a DataMapper to fully control how your data is mapped to your form.

Related

How could my custom Symfony ChoiceType allow extra items?

I'm working on a TagField for EasyAdmin 4 (and Symfony 6) that will rely on a TagType. This TagType will have the native ChoiceType as a parent.
This field will be rendered as a multiple select, with these attributes to allow adding tags on the fly:
[ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ]
To do so, I created a TagListener. Its main goal is to prefill the options with the already existing tags (on other entities) to support tag suggestion. After reading the docs and many articles, I chose to listen to the FormEvents::PRE_SET_DATA event.
Unfortunately there does not seem to be an easy way to "override" the default options, and we're left with having to override the entire field.
Here's what the TagListener looks like:
<?php
// src/Form/EventListener/TagListener.php
namespace eduMedia\TagBundle\Form\EventListener;
use eduMedia\TagBundle\Entity\TaggableInterface;
use eduMedia\TagBundle\Form\Type\TagType;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TagListener implements EventSubscriberInterface
{
public function __construct(private TagService $tagService)
{
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
];
}
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$parentForm = $event->getForm()->getParent();
/** #var TaggableInterface $taggable */
$taggable = $parentForm->getData();
// We retrieve the existing options to override some of them
$options = $form->getConfig()->getOptions();
// if ($options['pre_set_data_called']) {
// return;
// }
// We prefill options with the existing tags for this resource type
$allTagNames = $this->tagService->getTypeTagNames($taggable->getTaggableType());
// They are our new choices
$options['choices'] = array_combine($allTagNames, $allTagNames);
// We also need to select the entity's tags
$options['data'] = $this->tagService->loadTagging($taggable)->getTagNames($taggable);
// We override the form field
// $options['pre_set_data_called'] = true;
$parentForm->add($form->getName(), TagType::class, $options);
}
}
Doing so seems to create an infinite loop, where onPreSetData is called when calling $parentForm->add(). Is that normal? Is PRE_SET_DATA dispatched again when adding a field in a listener? Is there a way to prevent this from happening?
I tried adding a pre_set_data_called form option, setting it to true when calling $parentForm->add() and exiting the listener when it is indeed true. It kind of works, but then I get this error:
An exception has been thrown during the rendering of a template ("Field "tags" has already been rendered, save the result of previous render call to a variable and output that instead.").
How can I manage to allow extra items in my custom field type?
For reference, here is my TagType class:
<?php
namespace eduMedia\TagBundle\Form\Type;
use eduMedia\TagBundle\Form\EventListener\TagListener;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function __construct(private TagService $tagService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new TagListener($this->tagService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false,
'multiple' => true,
// 'pre_set_data_called' => false,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}
And my TagField class:
<?php
namespace eduMedia\TagBundle\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use eduMedia\TagBundle\Form\Type\TagType;
class TagField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null)
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TagType::class)
->setFormTypeOption('attr', [ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ])
->setTemplatePath('#eduMediaTag/fields/tag.html.twig')
;
}
}
I ended up not using the ChoiceType as the parent (<select> element), but rather the TextType (<input type=text> element), and splitting/exploding a simple string.
The actual bundle is live on GitHub and even though it might not be perfect (yet 😉), the implementation is way simpler and the end-user behaviour is exactly what I expected.

Symfony2 -- Creating a form that use's differents entities

I've been using Symfony a bit and i'm trying to figure out a way to create a form.
I need to use a MVC based solution.
My form needs to ask several information of different Entities and then i need to process that information extracting it in the database.
The database wont be a problem.
I was just figuring out how do i make a form with different types of entities ?
And how do i make a scrolldown menu with the data contained in the database for an entity ?
If the comment by #chalasr does not apply, i.e., the entities are not related, it is possible to do something like the following in a controller. Simply create a $form variable for each Entity{x}Type form as in:
$formA = $this->createForm(AppBundle\Entity\EntityAType($entityA));
$formB = $this->createForm(AppBundle\Entity\EntityBType($entityB));
...
return array(
'formA' => $formA->createView(),
'formB' => $formB->createView(),
...
);
You can simply combine the Forms while keeping each separate like so
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ReallyBigFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foo', FooType::class, [
// label, required, ... as well as options of FooType
])
->add('bar', BarType::class, [
// label, required, ... as well as options of BarType
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([]);
}
}
and define FooType and BarType like a regular Form
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'label' => 'foo.name',
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Foo',
]);
}
}

Symfony 2.8/3.0 - Pass array to another formType from a collectionType

I could get this to work prior to v2.8 but as symfony now uses fully qualified class names name i'm unsure sure how to proceed.
I can pass an array (to populate a choice field) to a form without issue but if there is an another formType added via a collectionType how can a pass the array?
BTW - the array is gathered from data from a custom annotations - NOT an entity
Heres my code:
PageType.php
<?php
namespace Prototype\PageBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
const ActiveComponentsType = 'Prototype\PageBundle\Form\ActiveComponentsType';
const collectionType = 'Symfony\Component\Form\Extension\Core\Type\CollectionType';
class PageType extends AbstractType
{
private $cmsComponentArray;
public function __construct($cmsComponentArray = null)
{
$this->cmsComponentArray = $cmsComponentArray;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$cmsComponentArray = $options['cmsComponentArray'];
$componentChoices = array();
foreach($cmsComponentArray as $cmsComponent){
$componentChoices[$cmsComponent['name']] = $cmsComponent['route'];
}
//correct values are shown here
//print_r($componentChoices);
$builder
->add('title')
->add('parent')
->add('template')
->add('active')
->add('content')
->add('components', collectionType, array(
'entry_type' => ActiveComponentsType, // i want to pass $cmsComponentArray to ActiveComponentsType
'allow_add' => true,
'allow_delete' => true
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Prototype\PageBundle\Entity\Page',
'cmsComponentArray' => null
));
}
}
The ActiveComponentsType embeded form does work - except I'm unsure how to pass the $componentChoices array to it.
Any ideas?
The collection type defines the entry_options option which is used to configure the options that are passed to the embedded form type.

Adding choices dynamically using EventSubscriber to Symfony2 form

I am trying to set the choices for a form select dynamically since the choices come from a service call. However, when the form renders in the view, the choices are not there.
I'm doing the following in the FormType
<?php
namespace My\Form\Customer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ItemReturnRequestForm extends AbstractType
{
/**
* #var EventSubscriberInterface
*/
protected $reasonsSubscriber;
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'item_return_request';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('reason', 'choice', [
'label' => 'order.returns.reason_for_return',
'required' => true,
'multiple' => false,
'expanded' => false,
'placeholder' => 'order.returns.reasons.empty',
'empty_data' => null,
]);
$builder->addEventSubscriber($this->reasonsSubscriber);
}
/**
* #param EventSubscriberInterface $reasonsSubscriber
*/
public function setReasonsSubscriber(EventSubscriberInterface $reasonsSubscriber)
{
$this->reasonsSubscriber = $reasonsSubscriber;
}
}
The FormType has a service definition which injects the EventSubscriber instance since that is also a service definition with it's own dependencies.
The EventSubscrbier looks like
<?php
namespace My\Form\EventSubscriber;
use My\Customer\ItemReturnAware;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class ReturnReasonEventSubscriber implements EventSubscriberInterface
{
use ItemReturnAware;
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SET_DATA => 'getReturnReasons',
];
}
public function getReturnReasons(FormEvent $event)
{
$form = $event->getForm();
if ($form->has('reason')) {
$options = $form->get('reason')->getConfig()->getOptions();
$options['choices'] = $this->itemReturnService->getReasons();
$form->add('reason', 'choice', $options);
}
}
}
Everything seems to work fine up until this point. Using XDEBUG I can see that the EventSubscriber is being triggered. The service call sets $option['choices'] to the expected array value & the field is added successfully.
However, when the form gets rendered. it's as if the EventSubscriber had never been called.
If it makes a difference, the options array is an un-ordered numeric list.
i.e.
$options = [
10 => 'First choice',
15 => 'Second choice',
20 => 'Third choice',
];
Any ideas?
This is an ancient question, but today I found it on the top results searching for event listener to modify form choices.
In my context I have an entity programmatically created, and I redirect the user to the editAction to finish filling the fields.
I have one choice that I can apply only in this particular case, I don't want to allow my user to use it outside it.
That's why I user the POST_SET_DATA event, because I already have an entity with populated fields.
This event listener is set in the formType, inside the
public function buildForm(FormBuilderInterface $builder, array $options)
{
Here a working solution for symfony 3.4:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
// get the form from the event
$form = $event->getForm();
if ('myParticularMode' == $form->get('mode')->getData()) {
// get the field options
$options = $form->get('mode')->getConfig()->getOptions();
// add the mode to the choices array
$options['choices']['MY_PARTICULAR_MODE'] = 'myParticularMode_display_name';
$form->add('mode', ChoiceType::class, $options);
}
});
If you want to replace the choices, you can remove this:
$options = $form->get('mode')->getConfig()->getOptions();
and set a new array for choices.

Embedding a dynamically generated form into another

I use Symfony2.1's FormBuilder to create a dynamic form in the controller:
$form = $this->createFormBuilder($defaultData);
$form->add('field','text');
I would like to embed another form in the same way and embed it with the main form.
$subForm = $this->createFormBuilder();
$subForm->add('subfield','text');
// Does not work
$form->add('subform', $subForm);
Unfortunately this setup does not work properly. I could not find any way how to add a dynamically generated subform into a dynamically generated form, like the example above.
What's the function call I am missing?
The exception thrown by the example code above is
UnexpectedTypeException: Expected argument of type "string or
Symfony\Component\Form\FormTypeInterface",
"Symfony\Component\Form\Form" given.
I would create two Form Clases in two separate fields:
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class FirstFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field', 'text')
->add('subform', new SecondFormType());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\FirstEntity'
));
}
public function getName()
{
return 'first';
}
}
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SecondFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('subfield', 'text');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\SecondEntity',
));
}
public function getName()
{
return 'second';
}
}
After this creation you can create your form from any Controller with this line for creation forms:
$form = $this->createForm(new FirstFormType());
or for edition forms:
$form = $this->createForm(new FirstFormType(), $first);
In your example code you are trying to assig to a field an entire form instead of a Form Type.
Hope it helps
Recommended symfony-way is to create separate FormType classes, as mentioned above. But if you really want to..
You can add dynamic subform into form builder, by calling
$formBuilder->add($subform /* FormBuilder */);
This subform will have name "form" if created with $this->createFormBuilder(); , that means that you can not add two or more subforms created this way - newer one will overwrite previous with same name.
If you need to add multiple sub-forms, you have to create their builders with
$this->get('form.factory')->createNamedBuilder($uniqName)
Without making classes... inside a Controller Action:
$subFormBuilder = $this->createFormBuilder(
null /* default data */,
['label' => 'Sub Form'] /* options */
)
->add('name');
$form = $this->createFormBuilder()
->add($subFormBuilder)
->add('number')
->getForm();

Categories