Skip to content

Commit 862df0d

Browse files
author
Bart van den Burg
committed
Explained a more simple method of dynamic form handling available since
symfony/symfony#8827
1 parent e203c40 commit 862df0d

File tree

1 file changed

+52
-154
lines changed

1 file changed

+52
-154
lines changed

cookbook/form/dynamic_form_modification.rst

Lines changed: 52 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -403,31 +403,33 @@ possible choices will depend on each sport. Football will have attack, defense,
403403
goalkeeper etc... Baseball will have a pitcher but will not have goalkeeper. You
404404
will need the correct options to be set in order for validation to pass.
405405

406-
The meetup is passed as an entity hidden field to the form. So we can access each
406+
The meetup is passed as an entity field to the form. So we can access each
407407
sport like this::
408408

409409
// src/Acme/DemoBundle/Form/Type/SportMeetupType.php
410+
namespace Acme\DemoBundle\Form\Type;
411+
412+
// ...
413+
410414
class SportMeetupType extends AbstractType
411415
{
412416
public function buildForm(FormBuilderInterface $builder, array $options)
413417
{
414418
$builder
415-
->add('number_of_people', 'text')
416-
->add('discount_coupon', 'text')
419+
->add('sport', 'entity', array(...))
417420
;
418-
$factory = $builder->getFormFactory();
419421

420422
$builder->addEventListener(
421423
FormEvents::PRE_SET_DATA,
422-
function(FormEvent $event) use($factory){
424+
function(FormEvent $event) {
423425
$form = $event->getForm();
424426

425427
// this would be your entity, i.e. SportMeetup
426428
$data = $event->getData();
427429

428430
$positions = $data->getSport()->getAvailablePositions();
429431

430-
// ... proceed with customizing the form based on available positions
432+
$form->add('position', 'entity', array('choices' => $positions));
431433
}
432434
);
433435
}
@@ -448,173 +450,69 @@ On a form, we can usually listen to the following events:
448450
* ``BIND``
449451
* ``POST_BIND``
450452

451-
When listening to ``BIND`` and ``POST_BIND``, it's already "too late" to make
452-
changes to the form. Fortunately, ``PRE_BIND`` is perfect for this. There
453-
is, however, a big difference in what ``$event->getData()`` returns for each
454-
of these events. Specifically, in ``PRE_BIND``, ``$event->getData()`` returns
455-
the raw data submitted by the user.
453+
.. versionadded:: 2.2.6
456454

457-
This can be used to get the ``SportMeetup`` id and retrieve it from the database,
458-
given you have a reference to the object manager (if using doctrine). In
459-
the end, you have an event subscriber that listens to two different events,
460-
requires some external services and customizes the form. In such a situation,
461-
it's probably better to define this as a service rather than using an anonymous
462-
function as the event listener callback.
463455

464-
The subscriber would now look like::
456+
The key is to add a ``POST_BIND`` listener to the field your new field is dependent
457+
on. If you add a POST_BIND listener to a form child, and add new children to the parent
458+
from there, the Form component will detect the new field automatically and maps it
459+
to the client data if it is available.
465460

466-
// src/Acme/DemoBundle/Form/EventListener/RegistrationSportListener.php
467-
namespace Acme\DemoBundle\Form\EventListener;
461+
The type would now look like::
468462

469-
use Symfony\Component\Form\FormFactoryInterface;
470-
use Doctrine\ORM\EntityManager;
471-
use Symfony\Component\Form\FormEvent;
472-
use Symfony\Component\Form\FormEvents;
473-
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
463+
// src/Acme/DemoBundle/Form/Type/SportMeetupType.php
464+
namespace Acme\DemoBundle\Form\Type;
474465

475-
class RegistrationSportListener implements EventSubscriberInterface
476-
{
477-
/**
478-
* @var FormFactoryInterface
479-
*/
480-
private $factory;
481-
482-
/**
483-
* @var EntityManager
484-
*/
485-
private $em;
486-
487-
/**
488-
* @param factory FormFactoryInterface
489-
*/
490-
public function __construct(FormFactoryInterface $factory, EntityManager $em)
491-
{
492-
$this->factory = $factory;
493-
$this->em = $em;
494-
}
466+
// ...
467+
Acme\DemoBundle\Entity\Sport;
468+
Symfony\Component\Form\FormInterface;
495469

496-
public static function getSubscribedEvents()
470+
class SportMeetupType extends AbstractType
471+
{
472+
public function buildForm(FormBuilderInterface $builder, array $options)
497473
{
498-
return array(
499-
FormEvents::PRE_BIND => 'preBind',
500-
FormEvents::PRE_SET_DATA => 'preSetData',
501-
);
502-
}
474+
$builder
475+
->add('sport', 'entity', array(...))
476+
;
503477

504-
/**
505-
* @param event FormEvent
506-
*/
507-
public function preSetData(FormEvent $event)
508-
{
509-
$meetup = $event->getData()->getMeetup();
478+
$formModifier = function(FormInterface $form, Sport $sport) {
479+
$positions = $data->getSport()->getAvailablePositions();
510480

511-
// Before binding the form, the "meetup" will be null
512-
if (null === $meetup) {
513-
return;
481+
$form->add('position', 'entity', array('choices' => $positions));
514482
}
515483

516-
$form = $event->getForm();
517-
$positions = $meetup->getSport()->getPositions();
484+
$builder->addEventListener(
485+
FormEvents::PRE_SET_DATA,
486+
function(FormEvent $event) {
487+
$form = $event->getForm();
518488

519-
$this->customizeForm($form, $positions);
520-
}
489+
// this would be your entity, i.e. SportMeetup
490+
$data = $event->getData();
521491

522-
public function preBind(FormEvent $event)
523-
{
524-
$data = $event->getData();
525-
$id = $data['event'];
526-
$meetup = $this->em
527-
->getRepository('AcmeDemoBundle:SportMeetup')
528-
->find($id);
529-
530-
if ($meetup === null) {
531-
$msg = 'The event %s could not be found for you registration';
532-
throw new \Exception(sprintf($msg, $id));
533-
}
534-
$form = $event->getForm();
535-
$positions = $meetup->getSport()->getPositions();
492+
$formModifier($event->getForm(), $sport);
493+
}
494+
);
536495

537-
$this->customizeForm($form, $positions);
538-
}
496+
$builder->get('meetup')->addEventListener(
497+
FormEvents::POST_BIND,
498+
function(FormEvent $event) use ($formModifier) {
499+
// It's important here to fetch $event->getForm()->getData(), as
500+
// $event->getData() will get you the client data (this is, the ID)
501+
$sport = $event->getForm()->getData();
539502

540-
protected function customizeForm($form, $positions)
541-
{
542-
// ... customize the form according to the positions
503+
$positions = $sport->getAvailablePositions();
504+
505+
// since we've added the listener to the child, we'll have to pass on
506+
// the parent to the callback functions!
507+
$formModifier($event->getForm()->getParent(), $sport);
508+
}
509+
);
543510
}
544511
}
545512

546513
You can see that you need to listen on these two events and have different callbacks
547-
only because in two different scenarios, the data that you can use is given in a
548-
different format. Other than that, this class always performs exactly the same
549-
things on a given form.
550-
551-
Now that you have that setup, register your form and the listener as services:
552-
553-
.. configuration-block::
554-
555-
.. code-block:: yaml
556-
557-
# app/config/config.yml
558-
acme.form.sport_meetup:
559-
class: Acme\SportBundle\Form\Type\SportMeetupType
560-
arguments: [@acme.form.meetup_registration_listener]
561-
tags:
562-
- { name: form.type, alias: acme_meetup_registration }
563-
acme.form.meetup_registration_listener
564-
class: Acme\SportBundle\Form\EventListener\RegistrationSportListener
565-
arguments: [@form.factory, @doctrine.orm.entity_manager]
566-
567-
.. code-block:: xml
568-
569-
<!-- app/config/config.xml -->
570-
<services>
571-
<service id="acme.form.sport_meetup" class="Acme\SportBundle\FormType\SportMeetupType">
572-
<argument type="service" id="acme.form.meetup_registration_listener" />
573-
<tag name="form.type" alias="acme_meetup_registration" />
574-
</service>
575-
<service id="acme.form.meetup_registration_listener" class="Acme\SportBundle\Form\EventListener\RegistrationSportListener">
576-
<argument type="service" id="form.factory" />
577-
<argument type="service" id="doctrine.orm.entity_manager" />
578-
</service>
579-
</services>
580-
581-
.. code-block:: php
582-
583-
// app/config/config.php
584-
$definition = new Definition('Acme\SportBundle\Form\Type\SportMeetupType');
585-
$definition->addTag('form.type', array('alias' => 'acme_meetup_registration'));
586-
$container->setDefinition(
587-
'acme.form.meetup_registration_listener',
588-
$definition,
589-
array('security.context')
590-
);
591-
$definition = new Definition('Acme\SportBundle\Form\EventListener\RegistrationSportListener');
592-
$container->setDefinition(
593-
'acme.form.meetup_registration_listener',
594-
$definition,
595-
array('form.factory', 'doctrine.orm.entity_manager')
596-
);
597-
598-
In this setup, the ``RegistrationSportListener`` will be a constructor argument
599-
to ``SportMeetupType``. You can then register it as an event subscriber on
600-
your form::
601-
602-
private $registrationSportListener;
603-
604-
public function __construct(RegistrationSportListener $registrationSportListener)
605-
{
606-
$this->registrationSportListener = $registrationSportListener;
607-
}
608-
609-
public function buildForm(FormBuilderInterface $builder, array $options)
610-
{
611-
// ...
612-
$builder->addEventSubscriber($this->registrationSportListener);
613-
}
614-
615-
And this should tie everything together. You can now retrieve your form from the
616-
controller, display it to a user, and validate it with the right choice options
617-
set for every possible kind of sport that our users are registering for.
514+
only because in two different scenarios, the data that you can use is available in different events.
515+
Other than that, the listeners always perform exactly the same things on a given form.
618516

619517
One piece that may still be missing is the client-side updating of your form
620518
after the sport is selected. This should be handled by making an AJAX call

0 commit comments

Comments
 (0)