Skip to content
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

[WIP] [Form] Choice Loaders #9313

Closed
wants to merge 3 commits 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
50 changes: 50 additions & 0 deletions form/choice_loaders.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.. index::
single: Forms; Choice Loaders

Choice Loaders
==============

The built-in :doc:`choice Field Type </reference/forms/types/choice>` offers a
powerful way to render and handle a list of options the user can choose from.
These options are stored in objects called *choice lists*. By default, choice
lists are cached and will be reused throughout the form for increased
performance. In addition, the process of creating choice lists can also be
delegated to *choice loaders*.

There are multiple scenarios where using choice loaders is beneficial over
only providing a list of choices:

* You want to use *lazy loading* to load the choice list.
* You want to load the choice list only *partially* in cases where a
fully-loaded list is not necessary (such as the user submitting the form
through a PUT/POST request).
* You want to load the choice list from a data source with *custom logic*
(such as a third-party API or a search engine).

The following sections describe how to setup a choice loader for each
of these use-cases.

Lazy loading for Choice Lists
-----------------------------

The most basic way to add lazy loading to your choice lists is to implement the
:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\CallbackChoiceLoader` class.
It accepts a callback as its only argument that will be called when the form
needs the data provided by the choice loader (e.g. when the form is rendered).

First, define the `choice_loader` option for the `ChoiceType` and use the
`CallbackChoiceLoader` class to set the callable that's executed to get the
list of choices::

use AppBundle\Entity\Category;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

$builder->add('displayMode', ChoiceType::class, array(
'choice_loader' => new CallbackChoiceLoader(function() {
return Category::getDisplayModes();
},
));

Creating a Choice Loader Class
------------------------------
124 changes: 100 additions & 24 deletions reference/forms/types/choice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ This will create a ``select`` drop-down like this:
.. image:: /_images/reference/form/choice-example1.png
:align: center

If the user selects ``No``, the form will return ``false`` for this field. Similarly,
if the starting data for this field is ``true``, then ``Yes`` will be auto-selected.
In other words, the **value** of each item is the value you want to get/set in PHP
code, while the **key** is what will be shown to the user.
Each choice defined in the ``choices`` option consists of a **key** containing
the label (e.g. ``Yes``) that will be shown to the user and a **value**
containing the PHP data (e.g. ``true``) you want to retrieve from the field.
This means that if you manually set the field data to ``true``, the user will
see ``Yes`` as the selected choice. If the user selects ``No``, the returned
data will be ``false``.

.. caution::

Expand All @@ -84,41 +86,101 @@ code, while the **key** is what will be shown to the user.
and will be removed in 3.0. To read about the old API, read an older version of
the docs.

.. note::

The **value** (e.g. ``true``) of a choice is converted to a string and used
in the ``value`` attribute in HTML and submitted in the POST/PUT requests.
In cases where one of the values can't be converted to a string
(e.g. ``null`` like in the example above), the values will be rendered
as incrementing integers. You should consider it as well when dealing with
the ``empty_data`` option::

$builder->add('isAttending', 'choice', array(
'choices' => array(
'Maybe' => null,
'Yes' => true,
'No' => false,
),
'choices_as_values' => true,
'data' => true, // pre selected choice
'empty_data' => '1', // default submitted value
));

When the ``multiple`` option is ``true`` the submitted data is an array of
strings, you should the set the ``empty_value`` option accordingly.
Also note that as a scalar ``false`` data as string **value** is by default
``"0"`` to avoid conflict with placeholder value which is always an empty
string.

Advanced Example (with Objects!)
--------------------------------

This field has a *lot* of options and most control how the field is displayed. In
this example, the underlying data is some ``Category`` object that has a ``getName()``
method::

$builder->add('category', 'choice', [
'choices' => [
$builder->add('category', 'choice', array(
'choices' => array(
new Category('Cat1'),
new Category('Cat2'),
new Category('Cat3'),
new Category('Cat4'),
],
),
'choices_as_values' => true,
'choice_label' => function($category, $key, $index) {
/** @var Category $category */
'choice_label' => function(Category $category, $key, $value) {
return strtoupper($category->getName());
},
'choice_attr' => function($category, $key, $index) {
return ['class' => 'category_'.strtolower($category->getName())];
'choice_attr' => function(Category $category, $key, $value) {
return array('class' => 'category_'.strtolower($category->getName()));
},

'group_by' => function($category, $key, $index) {
'group_by' => function(Category $category, $key, $value) {
// randomly assign things into 2 groups
return rand(0, 1) == 1 ? 'Group A' : 'Group B';
},
'preferred_choices' => function($category, $key, $index) {
return $category->getName() == 'Cat2' || $category->getName() == 'Cat3';
'preferred_choices' => function(Category $category, $key, $value) {
return 'Cat2' === $category->getName() || 'Cat3' === $category->getName();
},
]);
));

You can also customize the `choice_name`_ and `choice_value`_ of each choice if
you need further HTML customization.

.. caution::

When dealing with objects as choices, if you need to set the
``empty_data`` option, you may need to override the ``choice_value``.
In the example above, the default values are incrementing integers if the
``Category`` class does not implement ``toString`` method.
To get full control of the string values use the `choice_value`_ option::

$builder->add('category', 'choice', array(
'choices' => array(
new Category('Cat1'),
new Category('Cat2'),
new Category('Cat3'),
new Category('Cat4'),
),
'choices_as_values' => true,
'choice_value' => function(Category $category = null) {
if (null === $category) {
return '';
}

return strtolower($category->getName());
},
'choice_label' => function(Category $category, $key, $value) {
return strtoupper($category->getName());
},
'multiple' => true,
'empty_data' => array('cat2'), // the default submitted value, matches
// a value of the choice_value option.
// passed as an array because the multiple
// option is true.
));

Note that `choice_value`_ option set as a callable can get passed ``null``
when no data is preset or submitted.

.. _forms-reference-choice-tags:

.. include:: /reference/forms/types/options/select_how_rendered.rst.inc
Expand Down Expand Up @@ -162,18 +224,27 @@ Field Options
choices
~~~~~~~

**type**: ``array`` **default**: ``array()``
**type**: ``array`` or ``\Traversable`` **default**: ``array()``

This is the most basic way to specify the choices that should be used
by this field. The ``choices`` option is an array, where the array key
is the item's label and the array value is the item's value::
is the choice's label and the array value is the choice's data::

$builder->add('inStock', 'choice', array(
'choices' => array('In Stock' => true, 'Out of Stock' => false),
'choices' => array(
'In Stock' => true,
'Out of Stock' => false,
),
// always include this
'choices_as_values' => true,
));

The field will try to cast the choice values (e.g. ``true`` and ``false``) into
strings to be rendered in HTML (in this case, ``"0"`` and ``"1"```). In the case
that one of the values can't be casted to a string, the values will be rendered
as incrementing integers. You can also customize these strings by using
the `choice_value`_ option.

.. include:: /reference/forms/types/options/choice_attr.rst.inc

.. _reference-form-choice-label:
Expand Down Expand Up @@ -231,9 +302,14 @@ choice_loader

**type**: :class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`

The ``choice_loader`` can be used to only partially load the choices in cases where
a fully-loaded list is not necessary. This is only needed in advanced cases and
would replace the ``choices`` option.
The ``choice_loader`` can be used to load the choices from a data source with
custom logic (e.g. query language) such as a database or a search engine.
The list will be fully loaded to display the form, but on submission, only the
submitted choices will be loaded.

Also, the :class:``Symfony\\Component\\Form\\ChoiceList\\Factory\\ChoiceListFactoryInterface``
will cache the choice list so the same :class:``Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface``
can be used in different fields with more performance (reducing N queries to 1).

.. include:: /reference/forms/types/options/choice_name.rst.inc

Expand All @@ -252,8 +328,8 @@ choices_as_values

The ``choices_as_values`` option was added to keep backward compatibility with the
*old* way of handling the ``choices`` option. When set to ``false`` (or omitted),
the choice keys are used as the underlying value and the choice values are shown
to the user.
the choice keys are used as the view value and the choice values are shown
to the user as the label.

* Before 2.7 (and deprecated now)::

Expand Down
28 changes: 20 additions & 8 deletions reference/forms/types/options/choice_attr.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ choice_attr
.. versionadded:: 2.7
The ``choice_attr`` option was introduced in Symfony 2.7.

**type**: ``array``, ``callable`` or ``string`` **default**: ``array()``
**type**: ``array``, ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``array()``

Use this to add additional HTML attributes to each choice. This can be an array
of attributes (if they are the same for each choice), a callable or a property path
(just like `choice_label`_).
Use this to add additional HTML attributes to each choice. This can be used as
a callable or a property path (just like `choice_label`_).

If an array, the keys of the ``choices`` array must be used as keys::
Also, if used as an array, the keys of the ``choices`` array must be used as keys::

$builder->add('attending', 'choice', array(
'choices' => array(
Expand All @@ -19,8 +18,21 @@ If an array, the keys of the ``choices`` array must be used as keys::
'Maybe' => null,
),
'choices_as_values' => true,
'choice_attr' => function($val, $key, $index) {
// adds a class like attending_yes, attending_no, etc
return ['class' => 'attending_'.strtolower($key)];
'choice_attr' => array(
// will be used for the second choice
'No' => array('class' => 'singular_choice_option');
),
));

$builder->add('attending', 'choice', array(
'choices' => array(
'Yes' => true,
'No' => false,
'Maybe' => null,
),
'choices_as_values' => true,
'choice_attr' => function() {
// will be used for all choices
return array('class' => 'choice_option');
},
));
55 changes: 34 additions & 21 deletions reference/forms/types/options/choice_label.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@ choice_label
.. versionadded:: 2.7
The ``choice_label`` option was introduced in Symfony 2.7.

**type**: ``string``, ``callable`` or ``false`` **default**: ``null``
**type**: ``string``, ``callable``, :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` or ``false`` **default**: ``null``

Normally, the array key of each item in the ``choices`` option is used as the
text that's shown to the user. The ``choice_label`` option allows you to take
more control::
more control.

If your choice values are objects (default in ``EntityType``), then ``choice_label``
can be a string :ref:`property path <reference-form-option-property-path>`. Imagine you have some
``Status`` class with a ``getDisplayName()`` method::

$builder->add('attending', 'choice', array(
'choices' => array(
new Status(Status::YES),
new Status(Status::NO),
new Status(Status::MAYBE),
),
'choices_as_values' => true,
'choice_label' => 'displayName',
));

If set as a callable, your function is called for each choice, passing the
model data ``$choice`` and the ``$key`` from the choices array (the default
label)::

$builder->add('attending', 'choice', array(
'choices' => array(
Expand All @@ -17,37 +35,32 @@ more control::
'maybe' => null,
),
'choices_as_values' => true,
'choice_label' => function ($value, $key, $index) {
if ($value == true) {
'choice_label' => function ($choice, $key, $value) {
if (true === $choice) {
return 'Definitely!';
}

return strtoupper($key);

// or if you want to translate some key
//return 'form.choice.'.$key;
},
));

This method is called for *each* choice, passing you the choice ``$value`` and the
``$key`` from the choices array (``$index`` is related to `choice_value`_). This
will give you:
The example above would output:

.. image:: /_images/reference/form/choice-example2.png
:align: center

If your choice values are objects, then ``choice_label`` can also be a
:ref:`property path <reference-form-option-property-path>`. Imagine you have some
``Status`` class with a ``getDisplayName()`` method::

$builder->add('attending', 'choice', array(
'choices' => array(
new Status(Status::YES),
new Status(Status::NO),
new Status(Status::MAYBE),
),
'choices_as_values' => true,
'choice_label' => 'displayName',
));

If set to ``false``, all the tag labels will be discarded for radio or checkbox
inputs. You can also return ``false`` from the callable to discard certain labels.

.. caution::

If you want to pass a string property path wich is also a callable (e.g 'range'),
the component will treat it as a callable. You should pass a :class:``Symfony\\Component\\PropertyAccess\\PropertyPath``
object to ensure the expected behavior::

use Symfony\Component\PropertyAccess\PropertyPath;

'choice_label' => new PropertyPath('range'),
14 changes: 8 additions & 6 deletions reference/forms/types/options/choice_name.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ choice_name
.. versionadded:: 2.7
The ``choice_name`` option was introduced in Symfony 2.7.

**type**: ``callable`` or ``string`` **default**: ``null``
**type**: ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``null``

Controls the internal field name of the choice. You normally don't care about this,
but in some advanced cases, you might. For example, this "name" becomes the index
of the choice views in the template.
Controls the internal field name of the choice. You normally don't care about
this, but in some advanced cases, you might. For example, this "name" becomes
the index of the choice views in the template.

This can be a callable or a property path. See `choice_label`_ for similar usage.
If ``null`` is used, an incrementing integer is used as the name.
This can be a callable or a property path. Both needs to return a non empty
string, if ``null`` is used, an incrementing integer is used as the name.

See `choice_label`_ for similar usage.
Loading