Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

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

Merged
merged 9 commits into from
Apr 1, 2015

Conversation

webmozart
Copy link
Contributor

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:

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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

@webmozart webmozart added the Form label Mar 25, 2015
@blaugueux
Copy link
Contributor

<3

@EmmanuelVella
Copy link
Contributor

@webmozart Do you plan to add choice_translation_domain too ?

@webmozart
Copy link
Contributor Author

#6456 is fixed here now.

@aitboudad
Copy link
Contributor

@webmozart an initial work for choice_translation_domain #13651

@webmozart
Copy link
Contributor Author

#8658 is fixed here now.

// Due to a bug in OptionsResolver, the choices haven't been
// validated yet at this point. Remove the if statement once that
// bug is resolved
if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can be removed as I explained in the previous pr

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed

@webmozart
Copy link
Contributor Author

Fixed regression with #10572.

*/
interface ChoiceListInterface
interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface
Copy link
Member

Choose a reason for hiding this comment

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

Can you use a "use" statement and an alias instead of the FQCN here (like done everywhere else in Symfony)?

Copy link
Member

Choose a reason for hiding this comment

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

You must also add the deprecation notice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed. I can't add a deprecation notice here (since it's an interface), but I'm adding them everywhere else.

Copy link
Member

Choose a reason for hiding this comment

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

I don't see any call to trigger_error() in this patch, so deprecation notices are not in place as far as I can tell. Am I missing something here?

@webmozart webmozart force-pushed the issue4067 branch 2 times, most recently from 89b30e3 to 91550c9 Compare March 26, 2015 13:56

$choiceList = function (Options $options, $choiceList) use ($choiceListFactory) {
if (null !== $choiceList) {
trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED);
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't think this will be triggered if people actually use the option as it will be overwritten. so I guess this needs to be done in a normalizer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@webmozart
Copy link
Contributor Author

Apart from the CHANGELOG and UPGRADE files, this PR should be complete now.

The ticket #9738 mentioned in the previous PR can be fixed in a separate PR. I'm not sure about #10551 yet and need to get some feedback by @beberlei first.

fabpot added a commit that referenced this pull request Apr 19, 2015
This PR was merged into the 2.7 branch.

Discussion
----------

Issue4067 class triggers

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This is #14201 rebased on latest 2.7.

This PR moves the deprecation notice triggers introduced in #14050 to file level, as [suggested](#14050 (comment)) by @stof.

Commits
-------

a3917fc [Form] Removed remaining deprecation notices in the test suite
8f297c1 [Form] Moved deprecation notice triggers to file level
julienfastre added a commit to Chill-project/Activity that referenced this pull request Jul 5, 2015
the label of activity type is now handled by translatableStringHelper.

Reasons are also grouped by categories.

This is thanks to PR symfony/symfony#14050
@likeitlikeit
Copy link

@webmozart What is this ominous, yet intriguing Status class that you are referencing in the documentation? Do you have an example of how to implement such a class?

// 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',
));

@stof
Copy link
Member

stof commented Sep 1, 2015

@likeitlikeit this is an example about how it could work to choose between value objects (assuming this is the way to build your value objects)

@likeitlikeit
Copy link

@stof Is it a unicorn? It looks so beautiful...
Seriously, I would very much like to see an example of such a class, given that it's much more elegant than what I have come up with so far w.r.t. static choice fields. In fact, I'd pledge to write a short cookbook article on how to implement static choices using such an enum-like class if someone shows me how. It would save a lot of people a lot of time, I'm sure.

@xabbuh
Copy link
Member

xabbuh commented Sep 5, 2015

@ABM-Dan
Copy link

ABM-Dan commented Feb 18, 2016

Is the caching behavior documented anywhere?

@justin-oh
Copy link

justin-oh commented Sep 19, 2016

Why are we being forced to do things backwards with the choices option in 3.0?

You could have extended ChoiceType to build out a ChoiceAsValueType (or ObjectType) with all the new options and left ChoiceType alone (like you should in OOP). Instead you bastardized a standard.

Or you could have simply left choices_as_values in and allowed people to set that to true as needed.

@justin-oh
Copy link

Sorry for sounding so angry, but it's frustrating when a new feature is introduced that you're personally going to use 1% of the time, yet 100% of the time you'll have to think backwards.

This could be built out like this:

ChoiceType
Add "choice_label", "choice_name", "choice_value", "choice_attr", and "group_by" options that accept callables.

ObjectType
Extends ChoiceType to also accept property paths on the above options.

EntityType
Extends ObjectType.

(Optional) ObjectTypeInterface
For objects passed to ObjectType to define getChoiceLabel(), getChoiceName(), etc. So instead of:

$builder->add('attending', 'choice', array(
    'choices' => array(
        Status::getInstance(Status::YES),
        Status::getInstance(Status::NO),
        Status::getInstance(Status::MAYBE),
    ),
    'choice_label' => 'displayName',
    'choice_name' => 'value'
));

you have Status implement the optional interface:

$builder->add('attending', 'choice', array(
    'choices' => array(
        Status::getInstance(Status::YES),
        Status::getInstance(Status::NO),
        Status::getInstance(Status::MAYBE),
    ),
    // internally, these are mapped to the following
    // because "choices" is an array of ObjectTypeInterface
    //'choice_label' => 'getChoiceLabel',
    //'choice_name' => 'getChoiceName'
));

ChoiceType is still backwards compatible, and there's a new ObjectType that meshes well with EntityType. All the old and new functionality is there, and everyone is happy.

@symfony symfony locked and limited conversation to collaborators Sep 19, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.