|
| 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 - Model::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', 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. |
0 commit comments