Skip to content

Commit 292d516

Browse files
committed
Merge branch 'khepin-dynamic_forms' into 2.1
Conflicts: cookbook/form/dynamic_form_modification.rst
2 parents 76a0014 + 11fa2ef commit 292d516

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

cookbook/form/dynamic_form_modification.rst

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,388 @@ first form event dispatched.
157157
You may view the full list of form events via the `FormEvents class`_,
158158
found in the form bundle.
159159

160+
How to Dynamically Generate Forms based on user data
161+
====================================================
162+
163+
Sometimes you want a form to be generated dynamically based not only on data
164+
from this form (see :doc:`Dynamic form generation</cookbook/dynamic_form_generation>`)
165+
but also on something else. For example depending on the user currently using
166+
the application. If you have a social website where a user can only message
167+
people who are his friends on the website, then the current user doesn't need to
168+
be included as a field of your form, but a "choice list" of whom to message
169+
should only contain users that are the current user's friends.
170+
171+
Creating the form type
172+
----------------------
173+
174+
Using an event listener, our form could be built like this::
175+
176+
// src/Acme/DemoBundle/FormType/FriendMessageFormType.php
177+
namespace Acme\DemoBundle\FormType;
178+
179+
use Symfony\Component\Form\AbstractType;
180+
use Symfony\Component\Form\FormBuilderInterface;
181+
use Symfony\Component\Form\FormEvents;
182+
use Symfony\Component\Form\FormEvent;
183+
use Symfony\Component\Security\Core\SecurityContext;
184+
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
185+
use Acme\DemoBundle\FormSubscriber\UserListener;
186+
187+
class FriendMessageFormType extends AbstractType
188+
{
189+
public function buildForm(FormBuilderInterface $builder, array $options)
190+
{
191+
$builder
192+
->add('subject', 'text')
193+
->add('body', 'textarea')
194+
;
195+
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
196+
// ... add a choice list of friends of the current application user
197+
});
198+
}
199+
200+
public function getName()
201+
{
202+
return 'acme_friend_message';
203+
}
204+
205+
public function setDefaultOptions(OptionsResolverInterface $resolver)
206+
{
207+
}
208+
}
209+
210+
The problem is now to get the current application user and create a choice field
211+
that would contain only this user's friends.
212+
213+
Luckily it is pretty easy to inject a service inside of the form. This can be
214+
done in the constructor.
215+
216+
.. code-block:: php
217+
218+
private $securityContext;
219+
220+
public function __construct(SecurityContext $securityContext)
221+
{
222+
$this->securityContext = $securityContext;
223+
}
224+
225+
.. note::
226+
227+
You might wonder, now that we have access to the User (through) the security
228+
context, why don't we just use that inside of the buildForm function and
229+
still use a listener?
230+
This is because doing so in the buildForm method would result in the whole
231+
form type being modified and not only one form instance.
232+
233+
Customizing the form type
234+
-------------------------
235+
236+
Now that we have all the basics in place, we can put everything in place and add
237+
our listener::
238+
239+
// src/Acme/DemoBundle/FormType/FriendMessageFormType.php
240+
class FriendMessageFormType extends AbstractType
241+
{
242+
private $securityContext;
243+
244+
public function __construct(SecurityContext $securityContext)
245+
{
246+
$this->securityContext = $securityContext;
247+
}
248+
249+
public function buildForm(FormBuilderInterface $builder, array $options)
250+
{
251+
$builder
252+
->add('subject', 'text')
253+
->add('body', 'textarea')
254+
;
255+
$user = $this->securityContext->getToken()->getUser();
256+
$factory = $builder->getFormFactory();
257+
258+
$builder->addEventListener(
259+
FormEvents::PRE_SET_DATA,
260+
function(FormEvent $event) use($user, $factory){
261+
$form = $event->getForm();
262+
$userId = $user->getId();
263+
264+
$formOptions = array(
265+
'class' => 'Acme\DemoBundle\Document\User',
266+
'multiple' => false,
267+
'expanded' => false,
268+
'property' => 'fullName',
269+
'query_builder' => function(DocumentRepository $dr) use ($userId) {
270+
return $dr->createQueryBuilder()->field('friends.$id')->equals(new \MongoId($userId));
271+
},
272+
);
273+
274+
$form->add($factory->createNamed('friend', 'document', null, $formOptions));
275+
}
276+
);
277+
}
278+
279+
public function getName()
280+
{
281+
return 'acme_friend_message';
282+
}
283+
284+
public function setDefaultOptions(OptionsResolverInterface $resolver)
285+
{
286+
}
287+
}
288+
289+
Using the form
290+
--------------
291+
292+
Our form is now ready to use. We have two possible ways to use it inside of a
293+
controller. Either by creating it everytime and remembering to pass the security
294+
context, or by defining it as a service. This is the option we will show here.
295+
296+
To define your form as a service, you simply add the configuration to your
297+
configuration.
298+
299+
.. configuration-block::
300+
301+
.. code-block:: yaml
302+
303+
# app/config/config.yml
304+
acme.form.friend_message:
305+
class: Acme\DemoBundle\FormType\FriendMessageType
306+
arguments: [@security.context]
307+
tags:
308+
- { name: form.type, alias: acme_friend_message}
309+
310+
.. code-block:: xml
311+
312+
<!-- app/config/config.xml -->
313+
<services>
314+
<service id="acme.form.friend_message" class="Acme\DemoBundle\FormType\FriendMessageType">
315+
<argument type="service" id="security.context" />
316+
<tag name="form.type" alias="acme_friend_message" />
317+
</service>
318+
</services>
319+
320+
.. code-block:: php
321+
322+
// app/config/config.php
323+
$definition = new Definition('Acme\DemoBundle\FormType\FriendMessageType');
324+
$definition->addTag('form.type', array('alias' => 'acme_friend_message'));
325+
$container->setDefinition(
326+
'acme.form.friend_message',
327+
$definition,
328+
array('security.context')
329+
);
330+
331+
By adding the form as a service, we make sure that this form can now be used
332+
simply from anywhere. If you need to add it to another form, you will just need
333+
to use::
334+
335+
$builder->add('message', 'acme_friend_message');
336+
337+
If you wish to create it from within a controller or any other service that has
338+
access to the form factory, you then use::
339+
340+
// src/AcmeDemoBundle/Controller/FriendMessageController.php
341+
public function friendMessageAction()
342+
{
343+
$form = $this->get('form.factory')->create('acme_friend_message');
344+
$form = $form->createView();
345+
346+
return compact('form');
347+
}
348+
349+
Dynamic generation for submitted forms
350+
======================================
351+
352+
An other case that can appear is that you want to customize the form specific to
353+
the data that was submitted by the user. If we take as an example a registration
354+
form for sports gatherings. Some events will allow you to specify your preferred
355+
position on the field. This would be a choice field for example. However the
356+
possible choices will depend on each sport. Football will have attack, defense,
357+
goalkeeper etc... Baseball will have a pitcher but will not have goalkeeper. We
358+
will need the correct options to be set in order for validation to pass.
359+
360+
The meetup is passed as an entity hidden field to the form. So we can access each
361+
sport like this::
362+
363+
// src/Acme/DemoBundle/FormType/SportMeetupType.php
364+
class SportMeetupType extends AbstractType
365+
{
366+
public function buildForm(FormBuilderInterface $builder, array $options)
367+
{
368+
$builder
369+
->add('number_of_people', 'text')
370+
->add('discount_coupon', 'text')
371+
;
372+
$factory = $builder->getFormFactory();
373+
374+
$builder->addEventListener(
375+
FormEvents::PRE_SET_DATA,
376+
function(FormEvent $event) use($user, $factory){
377+
$form = $event->getForm();
378+
$event->getData()->getSport()->getAvailablePositions();
379+
380+
// ... proceed with customizing the form based on available positions
381+
}
382+
);
383+
}
384+
}
385+
386+
387+
While generating this kind of form to display it to the user for the first time,
388+
we can just as previously, use a simple listener and all goes fine.
389+
390+
When considering form submission, things are usually a bit different because
391+
subscribing to PRE_SET_DATA will only return us an empty ``SportMeetup`` object.
392+
That object will then be populated with the data sent by the user when there is a
393+
call to ``$form->bind($request)``.
394+
395+
On a form, we can usually listen to the following events::
396+
397+
* ``PRE_SET_DATA``
398+
* ``POST_SET_DATA``
399+
* ``PRE_BIND``
400+
* ``BIND``
401+
* ``POST_BIND``
402+
403+
When listening to bind and post-bind, it's already "too late" to make changes to
404+
the form. But pre-bind is fine. There is however a big difference in what
405+
``$event->getData()`` will return for each of these events as pre-bind will return
406+
an array instead of an object. This is the raw data submitted by the user.
407+
408+
This can be used to get the SportMeetup's id and retrieve it from the database,
409+
given we have a reference to our object manager (if using doctrine). So we have
410+
an event subscriber that listens to two different events, requires some
411+
external services and customizes our form. In such a situation, it seems cleaner
412+
to define this as a service rather than use closure like in the previous example.
413+
414+
Our subscriber would now look like::
415+
416+
class RegistrationSportListener implements EventSubscriberInterface
417+
{
418+
/**
419+
* @var FormFactoryInterface
420+
*/
421+
private $factory;
422+
423+
/**
424+
* @var DocumentManager
425+
*/
426+
private $om;
427+
428+
/**
429+
* @param factory FormFactoryInterface
430+
*/
431+
public function __construct(FormFactoryInterface $factory, ObjectManager $om)
432+
{
433+
$this->factory = $factory;
434+
$this->om = $om;
435+
}
436+
437+
public static function getSubscribedEvents()
438+
{
439+
return [
440+
FormEvents::PRE_BIND => 'preBind',
441+
FormEvents::PRE_SET_DATA => 'preSetData',
442+
];
443+
}
444+
445+
/**
446+
* @param event DataEvent
447+
*/
448+
public function preSetData(DataEvent $event)
449+
{
450+
$meetup = $event->getData()->getMeetup();
451+
452+
// Before binding the form, the "meetup" will be null
453+
if (null === $meetup) {
454+
return;
455+
}
456+
457+
$form = $event->getForm();
458+
$positions = $meetup->getSport()->getPostions();
459+
460+
$this->customizeForm($form, $positions);
461+
}
462+
463+
public function preBind(DataEvent $event)
464+
{
465+
$data = $event->getData();
466+
$id = $data['event'];
467+
$meetup = $this->om
468+
->getRepository('Acme\SportBundle\Document\Event')
469+
->find($id);
470+
if($meetup === null){
471+
$msg = 'The event %s could not be found for you registration';
472+
throw new \Exception(sprintf($msg, $id));
473+
}
474+
$form = $event->getForm();
475+
$positions = $meetup->getSport()->getPositions();
476+
477+
$this->customizeForm($form, $positions);
478+
}
479+
480+
protected function customizeForm($form, $positions)
481+
{
482+
// ... customize the form according to the positions
483+
}
484+
}
485+
486+
We can see that we need to listen on these two events and have different callbacks
487+
only because in two different scenarios, the data that we can use is given in a
488+
different format. Other than that, this class always performs exactly the same
489+
things on a given form.
490+
491+
Now that we have this set up, we need to create our services:
492+
493+
.. configuration-block::
494+
495+
.. code-block:: yaml
496+
497+
# app/config/config.yml
498+
acme.form.sport_meetup:
499+
class: Acme\SportBundle\FormType\RegistrationType
500+
arguments: [@acme.form.meetup_registration_listener]
501+
tags:
502+
- { name: form.type, alias: acme_meetup_registration }
503+
acme.form.meetup_registration_listener
504+
class: Acme\SportBundle\Form\RegistrationSportListener
505+
arguments: [@form.factory, @doctrine]
506+
507+
.. code-block:: xml
508+
509+
<!-- app/config/config.xml -->
510+
<services>
511+
<service id="acme.form.sport_meetup" class="Acme\SportBundle\FormType\RegistrationType">
512+
<argument type="service" id="acme.form.meetup_registration_listener" />
513+
<tag name="form.type" alias="acme_meetup_registration" />
514+
</service>
515+
<service id="acme.form.meetup_registration_listener" class="Acme\SportBundle\Form\RegistrationSportListener">
516+
<argument type="service" id="form.factory" />
517+
<argument type="service" id="doctrine" />
518+
</service>
519+
</services>
520+
521+
.. code-block:: php
522+
523+
// app/config/config.php
524+
$definition = new Definition('Acme\SportBundle\FormType\RegistrationType');
525+
$definition->addTag('form.type', array('alias' => 'acme_meetup_registration'));
526+
$container->setDefinition(
527+
'acme.form.meetup_registration_listener',
528+
$definition,
529+
array('security.context')
530+
);
531+
$definition = new Definition('Acme\SportBundle\Form\RegistrationSportListener');
532+
$container->setDefinition(
533+
'acme.form.meetup_registration_listener',
534+
$definition,
535+
array('form.factory', 'doctrine')
536+
);
537+
538+
And this should tie everything together. We can now retrieve our form from the
539+
controller, display it to a user, and validate it with the right choice options
540+
set for every possible kind of sport that our users are registering for.
541+
542+
.. _`DataEvent`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/DataEvent.php
160543
.. _`FormEvents class`: https://github.com/symfony/Form/blob/master/FormEvents.php
161544
.. _`Form class`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php

0 commit comments

Comments
 (0)