@@ -157,5 +157,388 @@ first form event dispatched.
157
157
You may view the full list of form events via the `FormEvents class `_,
158
158
found in the form bundle.
159
159
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
160
543
.. _`FormEvents class` : https://github.com/symfony/Form/blob/master/FormEvents.php
161
544
.. _`Form class` : https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php
0 commit comments