8000 [Form] Add "choice_labels", "choice_values", "choice_attr" and "group_by" options to ChoiceType · Issue #4067 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Add "choice_labels", "choice_values", "choice_attr" and "group_by" options to ChoiceType #4067

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
webmozart opened this issue Apr 22, 2012 · 43 comments

Comments

@webmozart
Copy link
Contributor

Depends on: #3879, #3968 (OptionsParser::replaceDefaults is required)

I want to add three options to the ChoiceType that abstract functionality of EntityType for use in all choice fields. See also #3479.

The new options are:

"choice_labels"

If given, the "choices" option is interpreted as storing the choices in the values instead of the keys. This way, choices can be created that select between values that are not allowed to be passed as array keys (any non-integer or non-string value).

"choice_labels" can be:

  • an array with a label for each choice
  • a callable that is executed for each choice and returns the label
  • a property path string, if the choices are objects
<?php
// The following two are equivalent
$builder->add('foo', 'choice', array(
    'choices' => array(0 => 'no', 1 => 'yes'),
));
$builder->add('foo', 'choice', array(
    'choices' => array(0, 1),
    'choice_labels' => array('no', 'yes'),
));

// The following ones cannot be expressed without the new option
$builder->add('foo', 'choice', array(
    'choices' => array(null, false, true),
    'choice_labels' => array('maybe', 'no', 'yes'),
));
$builder->add('foo', 'choice', array(
    'choices' => array($obj1, $obj2),
    'choice_labels' => 'name',
));
$builder->add('foo', 'choice', array(
    'choices' => array($obj1, $obj2),
    'choice_labels' => function ($obj) {
        return 'Label for ' . $obj->name;
    },
));
"choice_values"

Like "choice_labels", but stores/generates the values that are stored in the "value" attributes of the generated HTML. Values must be strings and unique (i.e. no two choices must have the same value).

By default, the choices are used as values, if possible. If not possible, integer values are generated.

Like "choice_labels", this option can be an array, a callable or a string (if choices are objects).

<?php
$builder->add('foo', 'choice', array(
    'choices' => array($obj1, $obj2),
    'choice_labels' => 'name',
    'choice_values' => 'id',
));
"choice_attr"

Like choice_values.

"group_by"

The "group_by" option stores a callable that returns the choice group for each choice or a property path if the choices are objects.

<?php
$builder->add('favNumber', 'choice', array(
    'choices' => range(0, 100),
    'choice_labels' => range(0, 100),
    'group_by' => function ($choice) {
        return $choice % 2 == 0 ? 'Even' : 'Odd',
    }
));
"preferred_choices"

The "preferred_choices" option allows to pass the values of the preferred choices as array, a callable or a property path if the choices are objects.

$builder->add('foo', 'choice', array(
    'choices' => array(null, false, true),
    'choice_labels' => array('maybe', 'no', 'yes'),
    'preferred_choices' => array(true, false),
));
$builder->add('foo', 'choice', array(
    'choices' => array($obj1, $obj2),
    'preferred_choices' => 'preferred', // i.e. isPreferred()
));
$builder->add('foo', 'choice', array(
    'choices' => array($obj1, $obj2),
    'preferred_choices' => function ($obj) {
        return $obj instanceof PreferredSomething;
    },
));

Poll

If the "choices" option is given as hierarchical array sorting each choice into a choice group, there are two possible ways for associating choices with their labels/values. Which one do you prefer?

A. Duplicate the hierarchy
<?php
$builder->add('favNumber', 'choice', array(
    'choices' => array(
        'Even' => array($obj0, $obj2, $obj4),
        'Odd' => array($obj1, $obj3, $obj5),
    ),
    'choice_labels' => array(
        'Even' => array('Zero', 'Two', 'Four'),
        'Odd' => array('One', 'Three', 'Five'),
    ),
    'choice_values' => array(
        'Even' => array(0, 2, 4),
        'Odd' => array(1, 3, 5),
    ),
));
B. Array-index based
<?php
$builder->add('favNumber', 'choice', array(
    'choices' => array(
        'Even' => array(0 => $obj0, 2 => $obj2, 4 => $obj4),
        'Odd' => array(1 => $obj1, 3 => $obj3, 5 => $obj5),
    ),
    'choice_labels' => array(
       array(0 => 'Zero', 1 => 'One', 2 => 'Two', 3 => 'Three', 4 => 'Four', 5 => 'Five'),
    ),
    'choice_values' => array(
        array(0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5),
    ),
));

Please name your preferred option in the comments.

@jfsimon
Copy link
Contributor
jfsimon commented Apr 22, 2012

I think A is more readable.

@jalliot
Copy link
Contributor
jalliot commented Apr 22, 2012

Option A because the other doesn't seem intuitive...

@datiecher
Copy link

Option A without a doubt, It may be more verbose but a lot more readable as well.

@michelsalib
Copy link

We need to be explicit. No magic or whatsoever. So A.

@everzet
Copy link
Contributor
everzet commented Apr 22, 2012

Obviously A

@immutef
Copy link
immutef commented Apr 22, 2012

A 👍

@webmozart
Copy link
Contributor Author

Just for the record, you can also explicitely put the indices, so expliciteness is no argument here. I updated the snippets above.

@Tobion
Copy link
Contributor
Tobion commented Apr 22, 2012

I think A is easier to read and also easier to build dynamically. Because in B you need to keep track of the indicies across groups. So simply if condition $group1[] = ... else $group2[] = ... doesn't work.

@lyrixx
Copy link
Member
lyrixx commented Apr 22, 2012

It's A for me :)

@Ph3nol
Copy link
Ph3nol commented Apr 22, 2012

A is so intuitive!

@egulias
Copy link
Contributor
egulias commented Apr 22, 2012

As stated @datiecher, A is more readable. So A for me.

@o
Copy link
Contributor
o commented Apr 22, 2012

A looking good for me.

@webmozart
Copy link
Contributor Author

Poll closed.

@marijn
Copy link
marijn commented Apr 22, 2012

I guess I'm too late to the party, but what about translations?

@webmozart
Copy link
Contributor Author

Translations are done in the template.

@craue
Copy link
Contributor
craue commented Jan 29, 2013

Any news on this?

@webmozart
Copy link
Contributor Author

I was working on this again, but then stopped at the feature freeze of 2.2. Hopefully 2.3 then.

@ricbra
Copy link
Contributor
ricbra commented Mar 6, 2014

2.5 perhaps?

@mvrhov
Copy link
mvrhov commented Apr 18, 2014

It seems not. As 2.5 is feature freeze. It's a shame as there is a lot of long standing issues.

@krizon
Copy link
krizon commented Jun 19, 2014

Isn't it possible to split this one into multiple smaller tasks? I would love to see a more flexible way to format choices labels, e.g. with a closure in the property field. There seem to be closed a couple of working PR's in favor of this ticket. As i understand that this tickets aims for a more generic solution it seems that this cannot be done in a timely manner.

@stof
Copy link
Member
stof commented Jun 19, 2014

@krizon the issue if we introduce specific configuration ways, we cannot remove them in favor of the generic one later, because it would be a BC break.

@craue
Copy link
Contributor
craue commented Aug 27, 2014

@webmozart: Any news on this?

@raziel057
Copy link
Contributor

I would like to see this feature in Symfony 2.6. We are a bit limited for the moment.

@sprain
Copy link
Contributor
sprain commented Sep 15, 2014

I could use this very much right now. Great idea!

@stof
Copy link
Member
stof commented Sep 18, 2014

@webmozart when do you plan to work on this again ?

@webmozart webmozart changed the title [Form] Add "choice_labels", "choice_values" and "group_by" options to ChoiceType [Form] Add "choice_labels", "choice_values", "choice_attr" and "group_by" options to ChoiceType Sep 25, 2014
@ryanotella
Copy link

@webmozart What would be the simplest way to implement this sort of feature using standard form theming? Perhaps we can point people to an example of how it might be done. It seems that part of the issue here is that you end up having to off-load very view specific data onto the entity, particularly for something like choice_attr. That is something that feels like it belongs in a form theme, except there's not currently a simple way to implement it in that way. Would it be feasible to make the choice_widget_options block theme-able? This obviously will fudge the interface between collapsed and expanded choice fields, which I suppose is the larger issue.

@webmozart
Copy link
Contributor Author

@ryancastle I'm currently working on an implementation. I'll hopefully upload a PR today.

@webmozart
Copy link
Contributor Author

See the PR here: #12148

@Tobion
Copy link
Contributor
Tobion commented Oct 6, 2014

So from what I see, the poll about passing labels is not relevant anymore because they are passed as generic callable or propertypath.

@webmozart
Copy link
Contributor Author

@Tobion Yes. Passing labels as separate array becomes superfluous if we flip the "choices" array.

@mvrhov
Copy link
mvrhov commented Mar 16, 2015

@webmozart: Any chances of getting this PR and the ones depending on it through so they can be in 2.7?

@webmozart
Copy link
Contributor Author

Working on it.

webmozart added a commit that referenced this issue Apr 1, 2015
…l, value, index and attribute generation (webmozart)

This PR was merged into the 2.7 branch.

Discussion
----------

[Form] Refactored choice lists to support dynamic label, value, index and attribute generation

This is a rebase of #12148 on the 2.7 branch.

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #4067, #5494, #3836, #8658, #12148
| License       | MIT
| Doc PR        | TODO

I implemented the additional options "choice_label", "choice_name", "choice_value", "choice_attr", "group_by" and "choices_as_values" for ChoiceType. Additionally the "preferred_choices" option was updated to accept callables and property paths.

The "choices_as_values" option will be removed in Symfony 3.0, where the choices will be passed in the values of the "choices" option by default. The reason for that is that, right now, choices are limited to strings and integers (i.e. valid array keys). When we flip the array, we remove that limitation. Since choice labels are always strings, we can also always use them as array keys:

```php
// Not possible currently, but possible with "flip_choices"
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
));
```

All the features described here obviously also apply to subtypes of "choice", such as "entity".

**choice_label**

Returns the label for each choice. Can be a callable (which receives the choice as first and the key of the "choices" array as second argument) or a property path.

If `null`, the keys of the "choices" array are used as labels.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'yes' => true,
        'no' => false,
        'maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_label' => function ($choice, $key) {
        return 'form.choice.'.$key;
    },
));

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

**choice_name**

Returns the form name for each choice. That name is used as name of the checkbox/radio form for this choice. It is also used as index of the choice views in the template. Can be a callable (like for "choice_label") or a property path.

The generated names must be valid form names, i.e. contain alpha-numeric symbols, underscores, hyphens and colons only. They must start with an alpha-numeric symbol or an underscore.

If `null`, an incrementing integer is used as name.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_name' => function ($choice, $key) {
        // use the labels as names
        return strtolower($key);
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_name' => 'value',
));
```

**choice_value**

Returns the string value for each choice. This value is displayed in the "value" attributes and submitted in the POST/PUT requests. Can be a callable (like for "choice_label") or a property path.

If `null`, an incrementing integer is used as value.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_value' => function ($choice, $key) {
        if (null === $choice) {
            return 'null';
        }

        if (true === $choice) {
            return 'true';
        }

        return 'false';
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_value' => 'value',
));
```

**choice_attr**

Returns the additional HTML attributes for choices. Can be an array, a callable (like for "choice_label") or a property path.
If an array, the key of the "choices" array must be used as keys.

```php
// array
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_attr' => array(
        'Maybe' => array('class' => 'greyed-out'),
    ),
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_attr' => function ($choice, $key) {
        if (null === $choice) {
            return array('class' => 'greyed-out');
        }
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_value' => 'htmlAttributes',
));
```

**group_by**

Returns the grouping used for the choices. Can be an array/Traversable, a callable (like for "choice_label") or a property path.

The return values of the callable/property path are used as group labels. If `null` is returned, a choice is not grouped.

If `null`, the structure of the "choices" array is used to construct the groups.

```php
// default
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Decided' => array(
            'Yes' => true,
            'No' => false,
        ),
        'Undecided' => array(
            'Maybe' => null,
        ),
    ),
    'choices_as_values' => true,
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'group_by' => function ($choice, $key) {
        if (null === $choice) {
            return 'Undecided';
        }

        return 'Decided';
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'group_by' => 'type',
));
```

**preferred_choices**

Returns the preferred choices. Can be an array/Traversable, a callable (like for "choice_label") or a property path.

```php
// array
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'preferred_choices' => array(true),
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'preferred_choices' => function ($choice, $key) {
        return true === $choice;
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'preferred_choices' => 'preferred',
));
```

**Technical Changes**

To properly implement all this, the old `ChoiceListInterface` class was deprecated and replaced by a new, slimmer one. The creation of choice views is now separated from choice lists. Hence a lot of logic is not executed anymore when processing (but not displaying) a form.

Internally, a `ChoiceListFactoryInterface` implementation is used to construct choice lists and choice views. Two decorators exist for this class:

* `CachingFactoryDecorator`: caches choice lists/views so that multiple fields displaying the same choices (e.g. in collection fields) use the same choice list/view
* `PropertyAccessDecorator`: adds support for property paths to a factory

**BC Breaks**

The option "choice_list" of ChoiceType now contains a `Symfony\Component\Form\ChoiceList\ChoiceListInterface` instance, which is a super-type of the deprecated `ChoiceListInterface`.

**Todos**

- [ ] Adapt CHANGELOGs
- [ ] Adapt UPGRADE files
- [ ] symfony/symfony-docs issue/PR

Commits
-------

94d18e9 [Form] Fixed CS
7e0960d [Form] Fixed failing layout tests
1d89922 [Form] Fixed tests using legacy functionality
d6179c8 [Form] Fixed PR comments
26eba76 [Form] Fixed regression: Choices are compared by their values if a value callback is given
a289deb [Form] Fixed new ArrayChoiceList to compare choices by their values, if enabled
e6739bf [DoctrineBridge] DoctrineType now respects the "query_builder" option when caching the choice loader
3846b37 [DoctrineBridge] Fixed: don't cache choice lists if query builders are constructed dynamically
03efce1 [Form] Refactored choice lists to support dynamic label, value, index and attribute generation
@fabpot fabpot closed this as completed Apr 3, 2015
@Kharestani
Copy link

Thanks for your awesomeness!
I propose you add new option "choice_labels_attr" to change choice labels attributes dynamically!
I know it might be too far but I think everything is too dynamic now and it's needed!

Thanks again.

@xabbuh
Copy link
Member
xabbuh commented Jan 20, 2016

@Kharestani Better create a new issue if you do not want your feature request to get lost (but please check first that there isn't already a similar issue).

10000

@HeahDude
Copy link
Contributor

@Kharestani see #16834

@Kharestani
Copy link

@HeahDude
My Bad!
Thanks!

@ABM-Dan
Copy link
ABM-Dan commented Feb 18, 2016

@webmozart What happened to choice_attr?

@xabbuh
Copy link
Member
xabbuh commented Feb 18, 2016

@ABM-Dan What do you mean?

@ABM-Dan
Copy link
ABM-Dan commented Feb 18, 2016

@xabbuh I couldn't find any documentation for EntityType's choice_attr. After some investigating, it turns out it's only in the docs for 2.7, with an example that suits ChoiceType, but is a bit lackluster when it comes to EntityType (for instance, mentioning that $val in the example is an actual Entity, is definetly relevant).

@xabbuh
Copy link
Member
xabbuh commented Feb 18, 2016

@ABM-Dan Can you please open a new issue on the docs repository if you think we can improve the documentation somehow?

@ABM-Dan
Copy link
ABM-Dan commented Feb 18, 2016

Will do at a later date.

@HeahDude
Copy link
Contributor

@xabbuh what about symfony/symfony-docs#6260 ?

@ABM-Dan be careful if you preset null as default data, it will throw an exception in you type hint the callable see #17736 (comment).

I've planned to open an issue in the docs for it as type hinting in the best practices, but haven't done it yet.

Ok my bad, there is no type hint yet, I read it here symfony/symfony-docs#6144.

There is some work about that, I'll try to help this week-end.

@marceloaugustocwb
Copy link

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

0