diff --git a/cookbook/form/dynamic_form_generation.rst b/cookbook/form/dynamic_form_generation.rst
index 79a07ab9d86..f91140b23f5 100644
--- a/cookbook/form/dynamic_form_generation.rst
+++ b/cookbook/form/dynamic_form_generation.rst
@@ -4,18 +4,18 @@
How to Dynamically Generate Forms Using Form Events
===================================================
-Before jumping right into dynamic form generation, let's have a quick review
+Before jumping right into dynamic form generation, let's have a quick review
of what a bare form class looks like::
// src/Acme/DemoBundle/Form/Type/ProductType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilder;
-
+ use Symfony\Component\Form\FormBuilderInterface;
+
class ProductType extends AbstractType
{
- public function buildForm(FormBuilder $builder, array $options)
+ public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('price');
@@ -29,19 +29,19 @@ of what a bare form class looks like::
.. note::
- If this particular section of code isn't already familiar to you, you
- probably need to take a step back and first review the :doc:`Forms chapter `
+ If this particular section of code isn't already familiar to you, you
+ probably need to take a step back and first review the :doc:`Forms chapter `
before proceeding.
Let's assume for a moment that this form utilizes an imaginary "Product" class
-that has only two relevant properties ("name" and "price"). The form generated
+that has only two relevant properties ("name" and "price"). The form generated
from this class will look the exact same regardless of a new Product is being created
or if an existing product is being edited (e.g. a product fetched from the database).
-Suppose now, that you don't want the user to be able to change the ``name`` value
+Suppose now, that you don't want the user to be able to change the ``name`` value
once the object has been created. To do this, you can rely on Symfony's :doc:`Event Dispatcher `
-system to analyze the data on the object and modify the form based on the
-Product object's data. In this entry, you'll learn how to add this level of
+system to analyze the data on the object and modify the form based on the
+Product object's data. In this entry, you'll learn how to add this level of
flexibility to your forms.
.. _`cookbook-forms-event-subscriber`:
@@ -49,20 +49,20 @@ flexibility to your forms.
Adding An Event Subscriber To A Form Class
------------------------------------------
-So, instead of directly adding that "name" widget via our ProductType form
-class, let's delegate the responsibility of creating that particular field
+So, instead of directly adding that "name" widget via our ProductType form
+class, let's delegate the responsibility of creating that particular field
to an Event Subscriber::
// src/Acme/DemoBundle/Form/Type/ProductType.php
namespace Acme\DemoBundle\Form\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilder;
+ use Symfony\Component\Form\AbstractType
+ use Symfony\Component\Form\FormBuilderInterface;
use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber;
class ProductType extends AbstractType
{
- public function buildForm(FormBuilder $builder, array $options)
+ public function buildForm(FormBuilderInterface $builder, array $options)
{
$subscriber = new AddNameFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
@@ -75,8 +75,8 @@ to an Event Subscriber::
}
}
-The event subscriber is passed the FormFactory object in its constructor so
-that our new subscriber is capable of creating the form widget once it is
+The event subscriber is passed the FormFactory object in its constructor so
+that our new subscriber is capable of creating the form widget once it is
notified of the dispatched event during form creation.
.. _`cookbook-forms-inside-subscriber-class`:
@@ -128,36 +128,419 @@ might look like the following::
// check if the product object is "new"
if (!$data->getId()) {
- $form->add($this->factory->createNamed('text', 'name'));
+ $form->add($this->factory->createNamed('name', 'text'));
}
}
}
.. caution::
- It is easy to misunderstand the purpose of the ``if (null === $data)`` segment
- of this event subscriber. To fully understand its role, you might consider
- also taking a look at the `Form class`_ and paying special attention to
- where setData() is called at the end of the constructor, as well as the
+ It is easy to misunderstand the purpose of the ``if (null === $data)`` segment
+ of this event subscriber. To fully understand its role, you might consider
+ also taking a look at the `Form class`_ and paying special attention to
+ where setData() is called at the end of the constructor, as well as the
setData() method itself.
-The ``FormEvents::PRE_SET_DATA`` line actually resolves to the string ``form.pre_set_data``.
+The ``FormEvents::PRE_SET_DATA`` line actually resolves to the string ``form.pre_set_data``.
The `FormEvents class`_ serves an organizational purpose. It is a centralized location
in which you can find all of the various form events available.
-While this example could have used the ``form.set_data`` event or even the ``form.post_set_data``
-events just as effectively, by using ``form.pre_set_data`` we guarantee that
-the data being retrieved from the ``Event`` object has in no way been modified
-by any other subscribers or listeners. This is because ``form.pre_set_data``
-passes a `DataEvent`_ object instead of the `FilterDataEvent`_ object passed
-by the ``form.set_data`` event. `DataEvent`_, unlike its child `FilterDataEvent`_,
+While this example could have used the ``form.set_data`` event or even the ``form.post_set_data``
+events just as effectively, by using ``form.pre_set_data`` we guarantee that
+the data being retrieved from the ``Event`` object has in no way been modified
+by any other subscribers or listeners. This is because ``form.pre_set_data``
+passes a `DataEvent`_ object instead of the `FilterDataEvent`_ object passed
+by the ``form.set_data`` event. `DataEvent`_, unlike its child `FilterDataEvent`_,
lacks a setData() method.
.. note::
- You may view the full list of form events via the `FormEvents class`_,
+ You may view the full list of form events via the `FormEvents class`_,
found in the form bundle.
+
+How to Dynamically Generate Forms based on user data
+====================================================
+
+Sometimes you want a form to be generated dynamically based not only on data
+from this form (see :doc:`Dynamic form generation`)
+but also on something else. For example depending on the user currently using
+the application. If you have a social website where a user can only message
+people who are his friends on the website, then the current user doesn't need to
+be included as a field of your form, but a "choice list" of whom to message
+should only contain users that are the current user's friends.
+
+Creating the form type
+----------------------
+
+Using an event listener, our form could be built like this::
+
+ // src/Acme/DemoBundle/FormType/FriendMessageFormType.php
+ namespace Acme\DemoBundle\FormType;
+
+ use Symfony\Component\Form\AbstractType;
+ use Symfony\Component\Form\FormBuilderInterface;
+ use Symfony\Component\Form\FormEvents;
+ use Symfony\Component\Form\FormEvent;
+ use Symfony\Component\Security\Core\SecurityContext;
+ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+ use Acme\DemoBundle\FormSubscriber\UserListener;
+
+ class FriendMessageFormType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('subject', 'text')
+ ->add('body', 'textarea')
+ ;
+ $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
+ // ... add a choice list of friends of the current application user
+ });
+ }
+
+ public function getName()
+ {
+ return 'acme_friend_message';
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ }
+ }
+
+The problem is now to get the current application user and create a choice field
+that would contain only this user's friends.
+
+Luckily it is pretty easy to inject a service inside of the form. This can be
+done in the constructor.
+
+.. code-block:: php
+
+ private $securityContext;
+
+ public function __construct(SecurityContext $securityContext)
+ {
+ $this->securityContext = $securityContext;
+ }
+
+.. note::
+
+ You might wonder, now that we have access to the User (through) the security
+ context, why don't we just use that inside of the buildForm function and
+ still use a listener?
+ This is because doing so in the buildForm method would result in the whole
+ form type being modified and not only one form instance.
+
+Customizing the form type
+-------------------------
+
+Now that we have all the basics in place, we can put everything in place and add
+our listener::
+
+ // src/Acme/DemoBundle/FormType/FriendMessageFormType.php
+ class FriendMessageFormType extends AbstractType
+ {
+ private $securityContext;
+
+ public function __construct(SecurityContext $securityContext)
+ {
+ $this->securityContext = $securityContext;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('subject', 'text')
+ ->add('body', 'textarea')
+ ;
+ $user = $this->securityContext->getToken()->getUser();
+ $factory = $builder->getFormFactory();
+
+ $builder->addEventListener(
+ FormEvents::PRE_SET_DATA,
+ function(FormEvent $event) use($user, $factory){
+ $form = $event->getForm();
+ $userId = $user->getId();
+
+ $formOptions = array(
+ 'class' => 'Acme\DemoBundle\Document\User',
+ 'multiple' => false,
+ 'expanded' => false,
+ 'property' => 'fullName',
+ 'query_builder' => function(DocumentRepository $dr) use ($userId) {
+ return $dr->createQueryBuilder()->field('friends.$id')->equals(new \MongoId($userId));
+ },
+ );
+
+ $form->add($factory->createNamed('friend', 'document', null, $formOptions));
+ }
+ );
+ }
+
+ public function getName()
+ {
+ return 'acme_friend_message';
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ }
+ }
+
+Using the form
+--------------
+
+Our form is now ready to use. We have two possible ways to use it inside of a
+controller. Either by creating it everytime and remembering to pass the security
+context, or by defining it as a service. This is the option we will show here.
+
+To define your form as a service, you simply add the configuration to your
+configuration.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # app/config/config.yml
+ acme.form.friend_message:
+ class: Acme\DemoBundle\FormType\FriendMessageType
+ arguments: [@security.context]
+ tags:
+ - { name: form.type, alias: acme_friend_message}
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // app/config/config.php
+ $definition = new Definition('Acme\DemoBundle\FormType\FriendMessageType');
+ $definition->addTag('form.type', array('alias' => 'acme_friend_message'));
+ $container->setDefinition(
+ 'acme.form.friend_message',
+ $definition,
+ array('security.context')
+ );
+
+By adding the form as a service, we make sure that this form can now be used
+simply from anywhere. If you need to add it to another form, you will just need
+to use::
+
+ $builder->add('message', 'acme_friend_message');
+
+If you wish to create it from within a controller or any other service that has
+access to the form factory, you then use::
+
+ // src/AcmeDemoBundle/Controller/FriendMessageController.php
+ public function friendMessageAction()
+ {
+ $form = $this->get('form.factory')->create('acme_friend_message');
+ $form = $form->createView();
+
+ return compact('form');
+ }
+
+Dynamic generation for submitted forms
+======================================
+
+An other case that can appear is that you want to customize the form specific to
+the data that was submitted by the user. If we take as an example a registration
+form for sports gatherings. Some events will allow you to specify your preferred
+position on the field. This would be a choice field for example. However the
+possible choices will depend on each sport. Football will have attack, defense,
+goalkeeper etc... Baseball will have a pitcher but will not have goalkeeper. We
+will need the correct options to be set in order for validation to pass.
+
+The meetup is passed as an entity hidden field to the form. So we can access each
+sport like this::
+
+ // src/Acme/DemoBundle/FormType/SportMeetupType.php
+ class SportMeetupType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('number_of_people', 'text')
+ ->add('discount_coupon', 'text')
+ ;
+ $factory = $builder->getFormFactory();
+
+ $builder->addEventListener(
+ FormEvents::PRE_SET_DATA,
+ function(FormEvent $event) use($user, $factory){
+ $form = $event->getForm();
+ $event->getData()->getSport()->getAvailablePositions();
+
+ // ... proceed with customizing the form based on available positions
+ }
+ );
+ }
+ }
+
+
+While generating this kind of form to display it to the user for the first time,
+we can just as previously, use a simple listener and all goes fine.
+
+When considering form submission, things are usually a bit different because
+subscribing to PRE_SET_DATA will only return us an empty ``SportMeetup`` object.
+That object will then be populated with the data sent by the user when there is a
+call to ``$form->bind($request)``.
+
+On a form, we can usually listen to the following events::
+
+ * ``PRE_SET_DATA``
+ * ``POST_SET_DATA``
+ * ``PRE_BIND``
+ * ``BIND``
+ * ``POST_BIND``
+
+When listening to bind and post-bind, it's already "too late" to make changes to
+the form. But pre-bind is fine. There is however a big difference in what
+``$event->getData()`` will return for each of these events as pre-bind will return
+an array instead of an object. This is the raw data submitted by the user.
+
+This can be used to get the SportMeetup's id and retrieve it from the database,
+given we have a reference to our object manager (if using doctrine). So we have
+an event subscriber that listens to two different events, requires some
+external services and customizes our form. In such a situation, it seems cleaner
+to define this as a service rather than use closure like in the previous example.
+
+Our subscriber would now look like::
+
+ class RegistrationSportListener implements EventSubscriberInterface
+ {
+ /**
+ * @var FormFactoryInterface
+ */
+ private $factory;
+
+ /**
+ * @var DocumentManager
+ */
+ private $om;
+
+ /**
+ * @param factory FormFactoryInterface
+ */
+ public function __construct(FormFactoryInterface $factory, ObjectManager $om)
+ {
+ $this->factory = $factory;
+ $this->om = $om;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return [
+ FormEvents::PRE_BIND => 'preBind',
+ FormEvents::PRE_SET_DATA => 'preSetData',
+ ];
+ }
+
+ /**
+ * @param event DataEvent
+ */
+ public function preSetData(DataEvent $event)
+ {
+ $meetup = $event->getData()->getMeetup();
+
+ // Before binding the form, the "meetup" will be null
+ if (null === $meetup) {
+ return;
+ }
+
+ $form = $event->getForm();
+ $positions = $meetup->getSport()->getPostions();
+
+ $this->customizeForm($form, $positions);
+ }
+
+ public function preBind(DataEvent $event)
+ {
+ $data = $event->getData();
+ $id = $data['event'];
+ $meetup = $this->om
+ ->getRepository('Acme\SportBundle\Document\Event')
+ ->find($id);
+ if($meetup === null){
+ $msg = 'The event %s could not be found for you registration';
+ throw new \Exception(sprintf($msg, $id));
+ }
+ $form = $event->getForm();
+ $positions = $meetup->getSport()->getPositions();
+
+ $this->customizeForm($form, $positions);
+ }
+
+ protected function customizeForm($form, $positions)
+ {
+ // ... customize the form according to the positions
+ }
+ }
+
+We can see that we need to listen on these two events and have different callbacks
+only because in two different scenarios, the data that we can use is given in a
+different format. Other than that, this class always performs exactly the same
+things on a given form.
+
+Now that we have this set up, we need to create our services:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # app/config/config.yml
+ acme.form.sport_meetup:
+ class: Acme\SportBundle\FormType\RegistrationType
+ arguments: [@acme.form.meetup_registration_listener]
+ tags:
+ - { name: form.type, alias: acme_meetup_registration }
+ acme.form.meetup_registration_listener
+ class: Acme\SportBundle\Form\RegistrationSportListener
+ arguments: [@form.factory, @doctrine]
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // app/config/config.php
+ $definition = new Definition('Acme\SportBundle\FormType\RegistrationType');
+ $definition->addTag('form.type', array('alias' => 'acme_meetup_registration'));
+ $container->setDefinition(
+ 'acme.form.meetup_registration_listener',
+ $definition,
+ array('security.context')
+ );
+ $definition = new Definition('Acme\SportBundle\Form\RegistrationSportListener');
+ $container->setDefinition(
+ 'acme.form.meetup_registration_listener',
+ $definition,
+ array('form.factory', 'doctrine')
+ );
+
+And this should tie everything together. We can now retrieve our form from the
+controller, display it to a user, and validate it with the right choice options
+set for every possible kind of sport that our users are registering for.
+
.. _`DataEvent`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/DataEvent.php
.. _`FormEvents class`: https://github.com/symfony/Form/blob/master/FormEvents.php
.. _`Form class`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php