Skip to content

Commit b525b7a

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

File tree

5 files changed

+306
-75
lines changed

5 files changed

+306
-75
lines changed

form/create_custom_choice_type.rst

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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+
// ... add more fields
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+
// Using 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+
// use this option instead of the "choices" option
87+
->setDefault('choice_loader', ChoiceList::lazy($this, static function() {
88+
return Model::getCategories();
89+
}))
90+
91+
// or if it depends on other options
92+
->setDefault('some_option', 'some_default')
93+
->setDefault('choice_loader', static function (Options $options) {
94+
$someOption = $options['some_option'];
95+
96+
return ChoiceList::lazy($this, static function() use ($someOption) {
97+
return Model::getCategories($someOption);
98+
}, $someOption);
99+
}))
100+
101+
.. note::
102+
103+
We use the ``ChoiceList::lazy()`` method to create a cached
104+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader`
105+
object. The first argument ``$this`` is the type configuring the form, and
106+
a third argument ``$vary`` can be used as array to pass any value that
107+
makes the loaded choices different.
108+
109+
Creating a Type With Dynamic Choices
110+
------------------------------------
111+
112+
When loading choices is complex, a callback is not enough and a "real" service
113+
is needed. Fortunately, the Form component provides a
114+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`.
115+
You can pass any instance to the ``choice_loader`` option to handle things
116+
any way you need. For example, you could leverage this new power to load
117+
categories from an HTTP API. The easiest way is to extend the
118+
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader`
119+
class, which already implements the interface and avoids triggering your logic
120+
when it is not needed (i.e when the form is submitted empty and valid).
121+
This could look like this::
122+
123+
// src/Form/ChoiceList/AcmeCategoryLoader.php.
124+
namespace App\Form\ChoiceList;
125+
126+
use App\Api\AcmeApi;
127+
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
128+
129+
class AcmeCategoryLoader extends AbstractChoiceLoader;
130+
{
131+
// This must be passed by the type
132+
// this loader won't be registered as service
133+
private $api;
134+
// define more options if needed
135+
private $someOption;
136+
137+
public function __construct(AcmeApi $api, string $someOption)
138+
{
139+
$this->api = $api;
140+
$this->someOption = $someOption;
141+
}
142+
143+
protected function loadChoices(): iterable
144+
{
145+
return $this->api->loadCategories($this->someOption));
146+
}
147+
148+
protected function doLoadChoicesForValues(array $values): array
149+
{
150+
return $this->api->loadCategoriesForNames($values, $this->someOption);
151+
}
152+
153+
protected function doLoadValuesForChoices(array $choices): array
154+
{
155+
$values = [];
156+
157+
// compute string values that must be submitted
158+
159+
return $values;
160+
}
161+
}
162+
163+
Here we implement three protected methods:
164+
165+
`loadChoices(): iterable`
166+
167+
This method is abstract and is the only one that needs to be implemented,
168+
it is called when the list is fully loaded (i.e when rendering the view).
169+
It must return an array or a traversable object, keys are default labels
170+
unless the :ref:`choice_label <reference-form-choice-label>` option is
171+
defined.
172+
Choices can be grouped with keys as group name and nested iterable choices
173+
in alternative to the :ref:`group_by <reference-form-group-by>` option.
174+
175+
`doLoadChoicesForValues(array $values): array`
176+
177+
Optional, to improve performance this method is called when the data is
178+
submitted. You can then load the choices partially, by using the submitted
179+
values passed as only argument.
180+
The list is fully loaded by default.
181+
182+
`doLoadValuesForChoices(array $choices): array`
183+
184+
Optional, in alternative to the
185+
`choice_value <reference-form-choice-value>` option.
186+
You can implement this method to return the string values partially, the
187+
initial choices are passed as only argument.
188+
The list is fully loaded by default unless the ``choice_value`` option is
189+
defined.
190+
191+
Then you need to update the previous example to use the new loader instead::
192+
193+
// src/Form/Type/CategoryChoiceType.php;
194+
195+
// ... same as before
196+
use App\Api\AcmeApi;
197+
use App\Form\ChoiceList\AcmeCategoryLoader;
198+
199+
class CategoryChoiceType extends AbstractType
200+
{
201+
// using the default configuration, the type is a service
202+
// so the api will be autowired
203+
private $api;
204+
205+
public function __construct(AcmeApi $api)
206+
{
207+
$this->api = $api;
208+
}
209+
210+
// ... same as before
211+
212+
public function configureOptions(OptionsResolver $resolver)
213+
{
214+
$resolver
215+
// ... same as before
216+
// but use the custom loader instead
217+
->setDefault('choice_loader', static function(Options $options) {
218+
$someOption = $options['some_option'];
219+
220+
return ChoiceList::loader($this, new AcmeCategoryLoader(
221+
$this->api,
222+
$someOption
223+
), $someOption);
224+
})
225+
;
226+
}
227+
}
228+
229+
Creating a Type With Custom Entities
230+
------------------------------------
231+
232+
When you need to reuse a same set of options with the
233+
:class:`Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType`, you may need to do
234+
the same as before, with some minor differences::
235+
236+
// src/Form/Type/CategoryChoiceType.php;
237+
238+
// ...
239+
240+
use App\Entity\AcmeCategory;
241+
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
242+
243+
class CategoryChoiceType extends AbstractType
244+
{
245+
public function getParent()
246+
{
247+
return EntityType::class;
248+
}
249+
250+
public function configureOptions(OptionsResolver $resolver)
251+
{
252+
$resolver
253+
// can now override options from both entity and choice types
254+
->setDefault('class', AcmeCategory::class)
255+
256+
// you can also customize the "query_builder" option
257+
->setDefault('some_option', 'some_default')
258+
->setDefault('query_builder', static function(Options $options) {
259+
$someOption = $options['some_option'];
260+
261+
return static function (AcmeCategoryRepository $repository) use ($someOption) {
262+
return $repository->getWithSomeOption($someOption);
263+
};
264+
})
265+
;
266+
}
267+
}
268+
269+
Customize Templates
270+
-------------------
271+
272+
This section is already covered in
273+
:doc:`create a custom field type </form/create_custom_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:`create a custom choice type </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:

forms.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,7 @@ Advanced Features:
939939
/security/csrf
940940
/form/form_dependencies
941941
/form/create_custom_field_type
942+
/form/create_custom_choice_type
942943
/form/data_transformers
943944
/form/data_mappers
944945
/form/create_form_type_extension

0 commit comments

Comments
 (0)