diff --git a/form/create_custom_choice_type.rst b/form/create_custom_choice_type.rst new file mode 100644 index 00000000000..51b54ce065c --- /dev/null +++ b/form/create_custom_choice_type.rst @@ -0,0 +1,275 @@ +.. index:: + single: Form; Custom choice type + +How to Create a Custom Choice Field Type +======================================== + +Symfony :doc:`ChoiceType ` is a very useful type +that deals with a list of selected options. +The Form component already provides many different choice types, like the +intl types (:doc:`LanguageType `, ...) and the +:doc:`EntityType ` which loads the choices from +a set of Doctrine entities. + +It's also common to want to re-use the same list of choices for different fields. +Creating a custom "choice" field is a great solution - something like:: + + use App\Form\Type\CategoryChoiceType; + + // ... from any type + $builder + ->add('category', CategoryChoiceType::class, [ + // ... some inherited or custom options for that type + ]) + // ... + ; + + +Creating a Type With Static Custom Choices +------------------------------------------ + +To create a custom choice type when choices are static, you can do the +following:: + + // src/Form/Type/CategoryChoiceType.php + namespace App\Form\Type; + + use App\Domain\Model; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class CategoryChoiceType extends AbstractType + { + /** + * {@inheritdoc} + */ + public function getParent() + { + // inherits all options, form and view configuration + // to create expanded or multiple choice lists + return ChoiceType::class; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + // Use whatever way you want to get the choices - Model::getCategories() is just an example + ->setDefault('choices', Model::getCategories()) + + // ... override more choice options or define new ones + ; + } + } + +.. caution:: + + The ``getParent()`` method is used instead of ``extends``. + This allows the type to inherit from both ``FormType`` and ``ChoiceType``. + +Loading Lazily Static Custom Choices +------------------------------------ + +Sometimes, the callable to define the ``choices`` option can be a heavy process +that could be prevented when the submitted data is optional and empty. +Sometimes it can depend on other options. + +The solution is to load the choices lazily using the ``choice_loader`` option, +which accepts a callback:: + + use Symfony\Component\Form\ChoiceList\ChoiceList; + use Symfony\Component\OptionsResolver\Options; + + $resolver + // use this option instead of the "choices" option + ->setDefault('choice_loader', ChoiceList::lazy($this, static function() { + return Model::getCategories(); + })) + + // or if it depends on other options + ->setDefault('some_option', 'some_default') + ->setDefault('choice_loader', function(Options $options) { + $someOption = $options['some_option']; + + return ChoiceList::lazy($this, static function() use ($someOption) { + return Model::getCategories($someOption); + }, $someOption); + })) + ; + +.. note:: + + The ``ChoiceList::lazy()`` method creates a cached + :class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader` + object. The first argument ``$this`` is the type configuring the form, and + a third argument ``$vary`` can be used as array to pass any value that + makes the loaded choices different. + +Creating a Type With Dynamic Choices +------------------------------------ + +When loading choices is complex, a callback is not enough and a "real" service +is needed. Fortunately, the Form component provides a +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`. +You can pass any instance to the ``choice_loader`` option to handle things +any way you need. For example, you could leverage this new power to load +categories from an HTTP API. The easiest way is to extend the +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader` +class, which already implements the interface and avoids triggering your logic +when it is not needed (e.g when the form is submitted empty and valid). +This could look like this:: + + // src/Form/ChoiceList/AcmeCategoryLoader.php. + namespace App\Form\ChoiceList; + + use App\Api\AcmeApi; + use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader; + + class AcmeCategoryLoader extends AbstractChoiceLoader + { + // this must be passed by the type + // this loader won't be registered as service + private $api; + // define more options if needed + private $someOption; + + public function __construct(AcmeApi $api, string $someOption) + { + $this->api = $api; + $this->someOption = $someOption; + } + + protected function loadChoices(): iterable + { + return $this->api->loadCategories($this->someOption)); + } + + protected function doLoadChoicesForValues(array $values): array + { + return $this->api->loadCategoriesForNames($values, $this->someOption); + } + + protected function doLoadValuesForChoices(array $choices): array + { + $values = []; + + // ... compute string values that must be submitted + + return $values; + } + } + +Here we implement three protected methods: + +``loadChoices(): iterable`` + + This method is abstract and is the only one that needs to be implemented. + It is called when the list is fully loaded (i.e when rendering the view). + It must return an array or a traversable object, keys are default labels + unless the :ref:`choice_label ` option is + defined. + Choices can be grouped with keys as group name and nested iterable choices + in alternative to the :ref:`group_by ` option. + +``doLoadChoicesForValues(array $values): array`` + + Optional, to improve performance this method is called when the data is + submitted. You can then load the choices partially, by using the submitted + values passed as only argument. + The list is fully loaded by default. + +``doLoadValuesForChoices(array $choices): array`` + + Optional, as alternative to the + :ref:`choice_value ` option. + You can implement this method to return the string values partially, the + initial choices are passed as only argument. + The list is fully loaded by default unless the ``choice_value`` option is + defined. + +Then you need to update the form type to use the new loader instead:: + + // src/Form/Type/CategoryChoiceType.php; + + // ... same as before + use App\Api\AcmeApi; + use App\Form\ChoiceList\AcmeCategoryLoader; + + class CategoryChoiceType extends AbstractType + { + // using the default configuration, the type is a service + // so the api will be autowired + private $api; + + public function __construct(AcmeApi $api) + { + $this->api = $api; + } + + // ... + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + // ... same as before + // but use the custom loader instead + ->setDefault('choice_loader', function(Options $options) { + $someOption = $options['some_option']; + + return ChoiceList::loader($this, new AcmeCategoryLoader( + $this->api, + $someOption + ), $someOption); + }) + ; + } + } + +Creating a Type With Custom Entities +------------------------------------ + +When you need to reuse a same set of options with the +:class:`Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType`, you may need to do +the same as before, with some minor differences:: + + // src/Form/Type/CategoryChoiceType.php; + + // ... + + use App\Entity\AcmeCategory; + use Symfony\Bridge\Doctrine\Form\Type\EntityType; + + class CategoryChoiceType extends AbstractType + { + public function getParent() + { + return EntityType::class; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + // can now override options from both entity and choice types + ->setDefault('class', AcmeCategory::class) + + // you can also customize the "query_builder" option + ->setDefault('some_option', 'some_default') + ->setDefault('query_builder', static function(Options $options) { + $someOption = $options['some_option']; + + return static function(AcmeCategoryRepository $repository) use ($someOption) { + return $repository->createQueryBuilderWithSomeOption($someOption); + }; + }) + ; + } + } + +Customize Templates +------------------- + +Read ":doc:`/form/create_custom_field_type`" on how to customize the form +themes for your new choice field type. diff --git a/form/create_custom_field_type.rst b/form/create_custom_field_type.rst index 2d93199db15..359cfb6b03a 100644 --- a/form/create_custom_field_type.rst +++ b/form/create_custom_field_type.rst @@ -14,9 +14,9 @@ Creating Form Types Based on Symfony Built-in Types The easiest way to create a form type is to base it on one of the :doc:`existing form types `. Imagine that your project -displays a list of "shipping options" as a ```` HTML element. This can be implemented with a :doc:`ChoiceType ` where the -``choices`` option is set to the list of available shipping options. +``choices`` option is set to the list of available category options. However, if you use the same form type in several forms, repeating the list of ``choices`` everytime you use it quickly becomes boring. In this example, a @@ -24,83 +24,17 @@ better solution is to create a custom form type based on ``ChoiceType``. The custom type looks and behaves like a ``ChoiceType`` but the list of choices is already populated with the shipping options so you don't need to define them. -Form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`, -but you should instead extend from :class:`Symfony\\Component\\Form\\AbstractType`, -which already implements that interface and provides some utilities. -By convention they are stored in the ``src/Form/Type/`` directory:: - - // src/Form/Type/ShippingType.php - namespace App\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\Extension\Core\Type\ChoiceType; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class ShippingType extends AbstractType - { - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'choices' => [ - 'Standard Shipping' => 'standard', - 'Expedited Shipping' => 'expedited', - 'Priority Shipping' => 'priority', - ], - ]); - } - - public function getParent() - { - return ChoiceType::class; - } - } - -The ``configureOptions()`` method, which is explained later in this article, -defines the options that can be configured for the form type and sets the -default value of those options. - -The ``getParent()`` method defines which is the form type used as the base of -this type. In this case, the type extends from ``ChoiceType`` to reuse all of -the logic and rendering of that field type. - -.. note:: - - The PHP class extension mechanism and the Symfony form field extension - mechanism are not the same. The parent type returned in ``getParent()`` is - what Symfony uses to build and manage the field type. Making the PHP class - extend from ``AbstractType`` is only a convenience way of implementing the - required ``FormTypeInterface``. - -Now you can add this form type when :doc:`creating Symfony forms `:: - - // src/Form/Type/OrderType.php - namespace App\Form\Type; - - use App\Form\Type\ShippingType; - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class OrderType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - // ... - ->add('shipping', ShippingType::class) - ; - } - - // ... - } - -That's all. The ``shipping`` form field will be rendered correctly in any -template because it reuses the templating logic defined by its parent type -``ChoiceType``. If you prefer, you can also define a template for your custom -types, as explained later in this article. +You can read a dedicated article on this topic in +":doc:``". Creating Form Types Created From Scratch ---------------------------------------- +Form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`, +but you should instead extend from :class:`Symfony\\Component\\Form\\AbstractType`, +which already implements that interface and provides some utilities. +By convention they are stored in the ``src/Form/Type/`` directory. + Some form types are so specific to your projects that they cannot be based on any :doc:`existing form types ` because they are too 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 generic :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType`, which is the parent of all the other types). +.. note:: + + The PHP class extension mechanism and the Symfony form field extension + mechanism are not the same. The parent type returned in ``getParent()`` is + what Symfony uses to build and manage the field type. Making the PHP class + extend from ``AbstractType`` is only a convenience way of implementing the + required ``FormTypeInterface``. + These are the most important methods that a form type class can define: .. _form-type-methods-explanation: diff --git a/forms.rst b/forms.rst index 02336d32169..12db21527e3 100644 --- a/forms.rst +++ b/forms.rst @@ -939,6 +939,7 @@ Advanced Features: /security/csrf /form/form_dependencies /form/create_custom_field_type + /form/create_custom_choice_type /form/data_transformers /form/data_mappers /form/create_form_type_extension diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index 98fab9bf9b0..49cbfdc0143 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -178,6 +178,12 @@ by passing a multi-dimensional ``choices`` array:: To get fancier, use the `group_by`_ option instead. +Using a Custom ChoiceType +------------------------- + +To learn how re-use ``ChoiceType`` options in your application, read +":doc:``". + Field Options ------------- @@ -224,10 +230,14 @@ correct types will be assigned to the model. .. include:: /reference/forms/types/options/choice_translation_domain.rst.inc +.. _reference-form-choice-value: + .. include:: /reference/forms/types/options/choice_value.rst.inc .. include:: /reference/forms/types/options/expanded.rst.inc +.. _reference-form-group-by: + .. include:: /reference/forms/types/options/group_by.rst.inc .. include:: /reference/forms/types/options/multiple.rst.inc diff --git a/reference/forms/types/options/choice_loader.rst.inc b/reference/forms/types/options/choice_loader.rst.inc index c44601ed3eb..0fc6e93b39a 100644 --- a/reference/forms/types/options/choice_loader.rst.inc +++ b/reference/forms/types/options/choice_loader.rst.inc @@ -73,3 +73,8 @@ better performance:: ]); } } + +.. tip:: + + To learn how to create a custom choice type with specific loaders, read + ":doc:``".