Skip to content

Commit 65a905b

Browse files
committed
[Form] Added article for custom choice fields
1 parent 1e3df40 commit 65a905b

File tree

5 files changed

+308
-75
lines changed

5 files changed

+308
-75
lines changed

form/create_custom_choice_type.rst

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
.. index::
2+
single: Form; Custom choice type
3+
4+
How to Create a Custom Choice Field Type
5+
========================================
6+
7+
Symfony :doc:`ChoiceType </reference/forms/types/choice>` is a very useful type
8+
that deals with a list of selected options.
9+
The Form component already provides many different choice types, like the
10+
intl types (:doc:`LanguageType </reference/forms/types/language>`, ...) and the
11+
:doc:`EntityType </reference/forms/types/entity>` which loads the choices from
12+
a set of Doctrine entities.
13+
14+
It's also common to want to re-use the same list of choices for different fields.
15+
Creating a custom "choice" field is a great solution - something like::
16+
17+
use App\Form\Type\CategoryChoiceType;
18+
19+
// ... from any type
20+
$builder
21+
->add('category', CategoryChoiceType::class, [
22+
// ... some inherited or custom options for that type
23+
])
24+
// ...
25+
;
26+
27+
28+
Creating a Type With Static Custom Choices
29+
------------------------------------------
30+
31+
To create a custom choice type when choices are static, you can do the
32+
following::
33+
34+
// src/Form/Type/CategoryChoiceType.php
35+
namespace App\Form\Type;
36+
37+
use App\Domain\Model;
38+
use Symfony\Component\Form\AbstractType;
39+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
40+
use Symfony\Component\OptionsResolver\OptionsResolver;
41+
42+
class CategoryChoiceType extends AbstractType
43+
{
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
public function getParent()
48+
{
49+
// inherits all options, form and view configuration
50+
// to create expanded or multiple choice lists
51+
return ChoiceType::class;
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function configureOptions(OptionsResolver $resolver)
58+
{
59+
$resolver
60+
// Use whatever way you want to get the choices - Mode::getCategories() is just an example
61+
->setDefault('choices', Model::getCategories())
62+
63+
// ... override more choice options or define new ones
64+
;
65+
}
66+
}
67+
68+
.. caution::
69+
70+
The ``getParent()`` method is used instead of ``extends``.
71+
This allows the type to inherit from both ``FormType`` and ``ChoiceType``.
72+
73+
Loading Lazily Static Custom Choices
74+
------------------------------------
75+
76+
Sometimes, the callable to define the ``choices`` option can be a heavy process
77+
that could be prevented when the submitted data is optional and empty.
78+
Sometimes it can depend on other options.
79+
80+
The solution is to load the choices lazily using the ``choice_loader`` option,
81+
which accepts a callback::
82+
83+
use Symfony\Component\Form\ChoiceList\ChoiceList;
84+
use Symfony\Component\OptionsResolver\Options;
85+
86+
$resolver
87+
// use this option instead of the "choices" option
88+
->setDefault('choice_loader', ChoiceList::lazy($this, static function() {
89+
return Model::getCategories();
90+
}))
91+
92+
// or if it depends on other options
93+
->setDefault('some_option', 'some_default')
94+
->setDefault('choice_loader', static function (Options $options) {
95+
$someOption = $options['some_option'];
96+
97+
return ChoiceList::lazy($this, static function() use ($someOption) {
98+
return Model::getCategories($someOption);
99+
}, $someOption);
100+
}))
101+
;
102+
103+
.. note::
104+
105+
The ``ChoiceList::lazy()`` method creates a cached
106+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader`
107+
object. The first argument ``$this`` is the type configuring the form, and
108+
a third argument ``$vary`` can be used as array to pass any value that
109+
makes the loaded choices different.
110+
111+
Creating a Type With Dynamic Choices
112+
------------------------------------
113+
114+
When loading choices is complex, a callback is not enough and a "real" service
115+
is needed. Fortunately, the Form component provides a
116+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`.
117+
You can pass any instance to the ``choice_loader`` option to handle things
118+
any way you need. For example, you could leverage this new power to load
119+
categories from an HTTP API. The easiest way is to extend the
120+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader`
121+
class, which already implements the interface and avoids triggering your logic
122+
when it is not needed (e.g when the form is submitted empty and valid).
123+
This could look like this::
124+
125+
// src/Form/ChoiceList/AcmeCategoryLoader.php.
126+
namespace App\Form\ChoiceList;
127+
128+
use App\Api\AcmeApi;
129+
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
130+
131+
class AcmeCategoryLoader extends AbstractChoiceLoader;
132+
{
133+
// this must be passed by the type
134+
// this loader won't be registered as service
135+
private $api;
136+
// define more options if needed
137+
private $someOption;
138+
139+
public function __construct(AcmeApi $api, string $someOption)
140+
{
141+
$this->api = $api;
142+
$this->someOption = $someOption;
143+
}
144+
145+
protected function loadChoices(): iterable
146+
{
147+
return $this->api->loadCategories($this->someOption));
148+
}
149+
150+
protected function doLoadChoicesForValues(array $values): array
151+
{
152+
return $this->api->loadCategoriesForNames($values, $this->someOption);
153+
}
154+
155+
protected function doLoadValuesForChoices(array $choices): array
156+
{
157+
$values = [];
158+
159+
// ... compute string values that must be submitted
160+
161+
return $values;
162+
}
163+
}
164+
165+
Here we implement three protected methods:
166+
167+
``loadChoices(): iterable``
168+
169+
This method is abstract and is the only one that needs to be implemented.
170+
It is called when the list is fully loaded (i.e when rendering the view).
171+
It must return an array or a traversable object, keys are default labels
172+
unless the :ref:`choice_label <reference-form-choice-label>` option is
173+
defined.
174+
Choices can be grouped with keys as group name and nested iterable choices
175+
in alternative to the :ref:`group_by <reference-form-group-by>` option.
176+
177+
``doLoadChoicesForValues(array $values): array``
178+
179+
Optional, to improve performance this method is called when the data is
180+
submitted. You can then load the choices partially, by using the submitted
181+
values passed as only argument.
182+
The list is fully loaded by default.
183+
184+
``doLoadValuesForChoices(array $choices): array``
185+
186+
Optional, as alternative to the
187+
:ref:`choice_value <reference-form-choice-value>` option.
188+
You can implement this method to return the string values partially, the
189+
initial choices are passed as only argument.
190+
The list is fully loaded by default unless the ``choice_value`` option is
191+
defined.
192+
193+
Then you need to update the form type to use the new loader instead::
194+
195+
// src/Form/Type/CategoryChoiceType.php;
196+
197+
// ... same as before
198+
use App\Api\AcmeApi;
199+
use App\Form\ChoiceList\AcmeCategoryLoader;
200+
201+
class CategoryChoiceType extends AbstractType
202+
{
203+
// using the default configuration, the type is a service
204+
// so the api will be autowired
205+
private $api;
206+
207+
public function __construct(AcmeApi $api)
208+
{
209+
$this->api = $api;
210+
}
211+
212+
// ...
213+
214+
public function configureOptions(OptionsResolver $resolver)
215+
{
216+
$resolver
217+
// ... same as before
218+
// but use the custom loader instead
219+
->setDefault('choice_loader', function(Options $options) {
220+
$someOption = $options['some_option'];
221+
222+
return ChoiceList::loader($this, new AcmeCategoryLoader(
223+
$this->api,
224+
$someOption
225+
), $someOption);
226+
})
227+
;
228+
}
229+
}
230+
231+
Creating a Type With Custom Entities
232+
------------------------------------
233+
234+
When you need to reuse a same set of options with the
235+
:class:`Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType`, you may need to do
236+
the same as before, with some minor differences::
237+
238+
// src/Form/Type/CategoryChoiceType.php;
239+
240+
// ...
241+
242+
use App\Entity\AcmeCategory;
243+
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
244+
245+
class CategoryChoiceType extends AbstractType
246+
{
247+
public function getParent()
248+
{
249+
return EntityType::class;
250+
}
251+
252+
public function configureOptions(OptionsResolver $resolver)
253+
{
254+
$resolver
255+
// can now override options from both entity and choice types
256+
->setDefault('class', AcmeCategory::class)
257+
258+
// you can also customize the "query_builder" option
259+
->setDefault('some_option', 'some_default')
260+
->setDefault('query_builder', static function(Options $options) {
261+
$someOption = $options['some_option'];
262+
263+
return static function (AcmeCategoryRepository $repository) use ($someOption) {
264+
return $repository->createQueryBuilderWithSomeOption($someOption);
265+
};
266+
})
267+
;
268+
}
269+
}
270+
271+
Customize Templates
272+
-------------------
273+
274+
Read ":doc:`/form/create_custom_field_type`" on how to customize the form
275+
themes for your new choice field type.

form/create_custom_field_type.rst

Lines changed: 17 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,93 +14,27 @@ Creating Form Types Based on Symfony Built-in Types
1414

1515
The easiest way to create a form type is to base it on one of the
1616
:doc:`existing form types </reference/forms/types>`. Imagine that your project
17-
displays a list of "shipping options" as a ``<select>`` HTML element. This can
17+
displays a list of "category options" as a ``<select>`` HTML element. This can
1818
be implemented with a :doc:`ChoiceType </reference/forms/types/choice>` where the
19-
``choices`` option is set to the list of available shipping options.
19+
``choices`` option is set to the list of available category options.
2020

2121
However, if you use the same form type in several forms, repeating the list of
2222
``choices`` everytime you use it quickly becomes boring. In this example, a
2323
better solution is to create a custom form type based on ``ChoiceType``. The
2424
custom type looks and behaves like a ``ChoiceType`` but the list of choices is
2525
already populated with the shipping options so you don't need to define them.
2626

27-
Form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`,
28-
but you should instead extend from :class:`Symfony\\Component\\Form\\AbstractType`,
29-
which already implements that interface and provides some utilities.
30-
By convention they are stored in the ``src/Form/Type/`` directory::
31-
32-
// src/Form/Type/ShippingType.php
33-
namespace App\Form\Type;
34-
35-
use Symfony\Component\Form\AbstractType;
36-
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
37-
use Symfony\Component\OptionsResolver\OptionsResolver;
38-
39-
class ShippingType extends AbstractType
40-
{
41-
public function configureOptions(OptionsResolver $resolver)
42-
{
43-
$resolver->setDefaults([
44-
'choices' => [
45-
'Standard Shipping' => 'standard',
46-
'Expedited Shipping' => 'expedited',
47-
'Priority Shipping' => 'priority',
48-
],
49-
]);
50-
}
51-
52-
public function getParent()
53-
{
54-
return ChoiceType::class;
55-
}
56-
}
57-
58-
The ``configureOptions()`` method, which is explained later in this article,
59-
defines the options that can be configured for the form type and sets the
60-
default value of those options.
61-
62-
The ``getParent()`` method defines which is the form type used as the base of
63-
this type. In this case, the type extends from ``ChoiceType`` to reuse all of
64-
the logic and rendering of that field type.
65-
66-
.. note::
67-
68-
The PHP class extension mechanism and the Symfony form field extension
69-
mechanism are not the same. The parent type returned in ``getParent()`` is
70-
what Symfony uses to build and manage the field type. Making the PHP class
71-
extend from ``AbstractType`` is only a convenience way of implementing the
72-
required ``FormTypeInterface``.
73-
74-
Now you can add this form type when :doc:`creating Symfony forms </forms>`::
75-
76-
// src/Form/Type/OrderType.php
77-
namespace App\Form\Type;
78-
79-
use App\Form\Type\ShippingType;
80-
use Symfony\Component\Form\AbstractType;
81-
use Symfony\Component\Form\FormBuilderInterface;
82-
83-
class OrderType extends AbstractType
84-
{
85-
public function buildForm(FormBuilderInterface $builder, array $options)
86-
{
87-
$builder
88-
// ...
89-
->add('shipping', ShippingType::class)
90-
;
91-
}
92-
93-
// ...
94-
}
95-
96-
That's all. The ``shipping`` form field will be rendered correctly in any
97-
template because it reuses the templating logic defined by its parent type
98-
``ChoiceType``. If you prefer, you can also define a template for your custom
99-
types, as explained later in this article.
27+
You can read a dedicated article on this topic in
28+
":doc:`</form/create_custom_choice_type>`".
10029

10130
Creating Form Types Created From Scratch
10231
----------------------------------------
10332

33+
Form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`,
34+
but you should instead extend from :class:`Symfony\\Component\\Form\\AbstractType`,
35+
which already implements that interface and provides some utilities.
36+
By convention they are stored in the ``src/Form/Type/`` directory.
37+
10438
Some form types are so specific to your projects that they cannot be based on
10539
any :doc:`existing form types </reference/forms/types>` because they are too
10640
different. Consider an application that wants to reuse in different forms the
@@ -131,6 +65,14 @@ implement the ``getParent()`` method (Symfony will make the type extend from the
13165
generic :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType`,
13266
which is the parent of all the other types).
13367

68+
.. note::
69+
70+
The PHP class extension mechanism and the Symfony form field extension
71+
mechanism are not the same. The parent type returned in ``getParent()`` is
72+
what Symfony uses to build and manage the field type. Making the PHP class
73+
extend from ``AbstractType`` is only a convenience way of implementing the
74+
required ``FormTypeInterface``.
75+
13476
These are the most important methods that a form type class can define:
13577

13678
.. _form-type-methods-explanation:

0 commit comments

Comments
 (0)