Skip to content

[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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions form/create_custom_choice_type.rst
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) {
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
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.
92 changes: 17 additions & 75 deletions form/create_custom_field_type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
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 </reference/forms/types>` because they are too
different. Consider an application that wants to reuse in different forms the
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PHP class extension mechanism

I don't know what this is. Do you mean inheritance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Expand Down
Loading