-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[Form] Added article for custom choice fields #13490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
.. index:: | ||
single: Form; Custom choice type | ||
|
||
How to Create a Custom Choice Field Type | ||
======================================== | ||
|
||
Symfony :doc:`ChoiceType </reference/forms/types/choice>` 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 </reference/forms/types/language>`, ...) and the | ||
:doc:`EntityType </reference/forms/types/entity>` 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) { | ||
HeahDude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 <reference-form-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 <reference-form-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 <reference-form-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 | ||
wouterj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
HeahDude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,93 +14,27 @@ 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 </reference/forms/types>`. Imagine that your project | ||
displays a list of "shipping options" as a ``<select>`` HTML element. This can | ||
displays a list of "category options" as a ``<select>`` HTML element. This can | ||
be implemented with a :doc:`ChoiceType </reference/forms/types/choice>` 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 | ||
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 </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:`</form/create_custom_choice_type>`". | ||
|
||
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`, | ||
HeahDude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
which already implements that interface and provides some utilities. | ||
By convention they are stored in the ``src/Form/Type/`` directory. | ||
HeahDude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Some form types are so specific to your projects that they cannot be based on | ||
any :doc:`existing form types </reference/forms/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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't know what this is. Do you mean inheritance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we should change that, but I propose to do it in #13488 which targets 3.4 instead. You're review is welcome there too :) |
||
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: | ||
|
Uh oh!
There was an error while loading. Please reload this page.