diff --git a/form/choice_loaders.rst b/form/choice_loaders.rst new file mode 100644 index 00000000000..4f54404ee18 --- /dev/null +++ b/form/choice_loaders.rst @@ -0,0 +1,50 @@ +.. index:: + single: Forms; Choice Loaders + +Choice Loaders +============== + +The built-in :doc:`choice Field Type ` 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 +------------------------------ diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index 69830ddf7c5..04dfa73a0fd 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -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:: @@ -84,6 +86,32 @@ 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!) -------------------------------- @@ -91,34 +119,68 @@ This field has a *lot* of options and most control how the field is displayed. I 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 @@ -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: @@ -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 @@ -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):: diff --git a/reference/forms/types/options/choice_attr.rst.inc b/reference/forms/types/options/choice_attr.rst.inc index 4b4d8f187ac..561c8959e57 100644 --- a/reference/forms/types/options/choice_attr.rst.inc +++ b/reference/forms/types/options/choice_attr.rst.inc @@ -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( @@ -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'); }, )); diff --git a/reference/forms/types/options/choice_label.rst.inc b/reference/forms/types/options/choice_label.rst.inc index afd833067b6..8077deb6bc4 100644 --- a/reference/forms/types/options/choice_label.rst.inc +++ b/reference/forms/types/options/choice_label.rst.inc @@ -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 `. 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( @@ -17,10 +35,11 @@ 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 @@ -28,26 +47,20 @@ more control:: }, )); -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 `. 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'), diff --git a/reference/forms/types/options/choice_name.rst.inc b/reference/forms/types/options/choice_name.rst.inc index 45a228489a3..64432a74000 100644 --- a/reference/forms/types/options/choice_name.rst.inc +++ b/reference/forms/types/options/choice_name.rst.inc @@ -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. diff --git a/reference/forms/types/options/choice_value.rst.inc b/reference/forms/types/options/choice_value.rst.inc index 78ab46650ce..aded9befa6c 100644 --- a/reference/forms/types/options/choice_value.rst.inc +++ b/reference/forms/types/options/choice_value.rst.inc @@ -4,7 +4,7 @@ choice_value .. versionadded:: 2.7 The ``choice_value`` option was introduced in Symfony 2.7. -**type**: ``callable`` or ``string`` **default**: ``null`` +**type**: ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``null`` Returns the string "value" for each choice, which must be unique across all choices. This is used in the ``value`` attribute in HTML and submitted in the POST/PUT requests. @@ -30,4 +30,14 @@ for each choice or ``null`` in some cases, which you need to handle: ``value`` attribute of options is generated. This is not a problem unless you rely on the option values in JavaScript. See `issue #14825`_ for details. +.. caution:: + + If you want to pass a string property path which is also a callable (e.g 'end'), + 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_value' => new PropertyPath('end'), + .. _`issue #14825`: https://github.com/symfony/symfony/pull/14825 diff --git a/reference/forms/types/options/group_by.rst.inc b/reference/forms/types/options/group_by.rst.inc index 3628a39d388..f0eba9b803d 100644 --- a/reference/forms/types/options/group_by.rst.inc +++ b/reference/forms/types/options/group_by.rst.inc @@ -4,7 +4,7 @@ group_by .. versionadded:: 2.7 The ``group_by`` option was introduced in Symfony 2.7. -**type**: ``array``, ``callable`` or ``string`` **default**: ``null`` +**type**: ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``null`` You can easily "group" options in a select simply by passing a multi-dimensional array to ``choices``. See the :ref:`Grouping Options ` @@ -23,8 +23,8 @@ Take the following example:: '1 month' => new \DateTime('+1 month'), ), 'choices_as_values' => true, - 'group_by' => function($val, $key, $index) { - if ($val <= new \DateTime('+3 days')) { + 'group_by' => function($choice, $key, $value) { + if ($choice <= new \DateTime('+3 days')) { return 'Soon'; } else { return 'Later'; @@ -39,5 +39,6 @@ a "Later" group: :align: center If you return ``null``, the option won't be grouped. You can also pass a string -"property path" that will be called to get the group. See the `choice_label`_ for -details about using a property path. +"property path" that will be called to get the group name. + +See the `choice_label`_ for details about using a property path or a callable. diff --git a/reference/forms/types/options/preferred_choices.rst.inc b/reference/forms/types/options/preferred_choices.rst.inc index 8393bd8cf4f..94f63d1d377 100644 --- a/reference/forms/types/options/preferred_choices.rst.inc +++ b/reference/forms/types/options/preferred_choices.rst.inc @@ -1,7 +1,7 @@ preferred_choices ~~~~~~~~~~~~~~~~~ -**type**: ``array``, ``callable`` or ``string`` **default**: ``array()`` +**type**: ``array``, ``\Traversable``, ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``array()`` This option allows you to move certain choices to the top of your list with a visual separator between them and the rest of the options. If you have a form of languages, @@ -32,9 +32,9 @@ be especially useful if your values are objects:: '1 month' => new \DateTime('+1 month'), ), 'choices_as_values' => true, - 'preferred_choices' => function ($val, $key) { + 'preferred_choices' => function ($choice, $key, $value) { // prefer options within 3 days - return $val <= new \DateTime('+3 days'); + return $choice <= new \DateTime('+3 days'); }, )); @@ -46,6 +46,8 @@ This will "prefer" the "now" and "tomorrow" choices only: Finally, if your values are objects, you can also specify a property path string on the object that will return true or false. +See the `choice_label`_ for details about using a property path or a callable. + The preferred choices are only meaningful when rendering a ``select`` element (i.e. ``expanded`` false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. ``-------------------``). This can be customized