From 5430cec472564e224e3cfddc6166ac77ecec1feb Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Wed, 23 Mar 2016 13:55:17 +0100 Subject: [PATCH] [Form] Fix `ChoiceType` options --- reference/forms/types/choice.rst | 155 +++++++++++++----- .../forms/types/options/choice_attr.rst.inc | 28 +++- .../forms/types/options/choice_label.rst.inc | 54 +++--- .../forms/types/options/choice_name.rst.inc | 14 +- .../forms/types/options/choice_value.rst.inc | 41 ++++- .../forms/types/options/group_by.rst.inc | 11 +- .../types/options/preferred_choices.rst.inc | 8 +- 7 files changed, 224 insertions(+), 87 deletions(-) diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index 93fb4d4ffe2..e2834dc1805 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -70,10 +70,14 @@ 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. +The model data of this field, the **choice** may be any of the ``choices`` option +values, while **keys** are used as default label that the user will see and select. + +If the starting data for this field is ``true``, then ``Yes`` will be auto-selected. +In other words, each value of the ``choices`` option is the **choice** data you +want to deal with in PHP code, while the **key** is the default label that will be +shown to the user and the **value** is the string that will be submitted to the +form and used in the template for the corresponding html attribute. .. caution:: @@ -83,6 +87,31 @@ 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:: + + Pre selected choices will depend on the **data** passed to the field and + the values of the ``choices`` option. However submitted choices will depend + on the **string** matching the **choice**. In the example above, the default + values are incrementing integers because ``null`` cannot be casted to string. + You should consider it as well when dealing with ``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!) -------------------------------- @@ -90,33 +119,66 @@ 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, you should be careful about how + string values are set to use them with the `empty_data` option. + In the example above, the default values are incrementing integers if the + ``Category`` class does not implement ``toString`` method. + To get a 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'), // default submitted value + // an array because of multiple option + )); + + 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 @@ -135,19 +197,19 @@ Grouping Options You can easily "group" options in a select by passing a multi-dimensional choices array:: - $builder->add('stockStatus', 'choice', [ - 'choices' => [ - 'Main Statuses' => [ + $builder->add('stockStatus', 'choice', array( + 'choices' => array( + 'Main Statuses' => array( 'Yes' => 'stock_yes', 'No' => 'stock_no', - ], - 'Out of Stock Statuses' => [ + ), + 'Out of Stock Statuses' => array( 'Backordered' => 'stock_backordered', 'Discontinued' => 'stock_discontinued', - ] - ], + ), + ), 'choices_as_values' => true, - ); + )); .. image:: /images/reference/form/choice-example4.png :align: center @@ -160,18 +222,25 @@ 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 component will try to cast the choices data to string to use it in view +format, in that case ``"0"`` and ``"1"``, but you can customize it using the +`choice_value`_ option. + .. include:: /reference/forms/types/options/choice_attr.rst.inc .. _reference-form-choice-label: @@ -229,9 +298,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 form a data source with a +custom logic (e.g query language) such as database or search engine. +The list will be fully loaded to display the form, but while 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 @@ -250,14 +324,14 @@ 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 label. * Before 2.7 (and deprecated now):: - $builder->add('gender', 'choice', array( - // Shows "Male" to the user, returns "m" when selected - 'choices' => array('m' => 'Male', 'f' => 'Female'), + $builder->add('agree', 'choice', array( + // Shows "Yes" to the user, returns "1" when selected + 'choices' => array('1' => 'Yes', '0' => 'No'), // before 2.7, this option didn't actually exist, but the // behavior was equivalent to setting this to false in 2.7. 'choices_as_values' => false, @@ -265,19 +339,18 @@ to the user. * Since 2.7:: - $builder->add('gender', 'choice', array( - // Shows "Male" to the user, returns "m" when selected - 'choices' => array('Male' => 'm', 'Female' => 'f'), + $builder->add('agree', 'choice', array( + // Shows "Yes" to the user, returns "1" when selected + 'choices' => array('Yes' => '1', 'No' => '0'), 'choices_as_values' => true, )); -In Symfony 3.0, the ``choices_as_values`` option doesn't exist, but the ``choice`` -type behaves as if it were set to true: +As of Symfony 3.0, the ``choices_as_values`` option is ``true`` by default: * Default for 3.0:: - $builder->add('gender', 'choice', array( - 'choices' => array('Male' => 'm', 'Female' => 'f'), + $builder->add('agree', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array( + 'choices' => array('Yes' => '1', 'No' => '0'), )); .. include:: /reference/forms/types/options/expanded.rst.inc 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 b92c47b6874..2bd34f603de 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 is called for each choice, passing you its model data ``$choice`` +and the ``$key`` from the choices array (default label). ``$value`` is either +`choice_value`_ option value when defined or the default string value of the choice:: $builder->add('attending', 'choice', array( 'choices' => array( @@ -17,8 +35,8 @@ 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); @@ -28,26 +46,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 fda5a4e7542..9bc4018a9b8 100644 --- a/reference/forms/types/options/choice_value.rst.inc +++ b/reference/forms/types/options/choice_value.rst.inc @@ -4,15 +4,40 @@ 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. This is used in the ``value`` attribute in HTML and submitted in the POST/PUT requests. You don't normally need to worry about this, but it might be handy when processing an API request (since you can configure the value that will be sent in the API request). -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 have the responsibility to return +a unique string for each choice, otherwise an incrementing integer is used. + +If it is a callable, it get passed each choice as only argument, supposed your choices +are some previously loaded entities in a given array:: + + $builder->add('attending', 'choice', array( + 'choices' => $entities, + 'choices_as_values' => true, + 'choice_value' => 'id', + )); + +Is equivalent to:: + + $builder->add('attending', 'choice', array( + 'choices' => $entities, + 'choices_as_values' => true, + 'choice_value' => function ($choice) { + return $choice->getId(); + }, + )); + +.. tip:: + + Those examples reproduce the default behavior of the ``EntityType``. + When your choices are already loaded you should consider using the + ``ChoiceType`` instead unless you need a lazy loading or a custom query. .. caution:: @@ -20,4 +45,14 @@ If ``null`` is used, an incrementing integer is used as the name. `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 9147a0a09bf..21d5fcffe91 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 2d841a21b2a..bd544fad257 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