Skip to content

[Form] allow entry_options option of CollectionType to be dynamic #17968

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

Conversation

HeahDude
Copy link
Contributor

@HeahDude HeahDude commented Mar 1, 2016

Q A
Branch master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? yes
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR symfony/symfony-docs#6319
  • Deprecate usage of prototype_data option in favor of prototype_options.
  • The ResizeFormListener now passes each entry of the collection to the entry_options option if it is a callable.

@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 1, 2016

To go further with this implementation, I think CollectionList and ChoiceList should use a common underlying ListInterface which would represent an editable list of form type entries, this should simplify things to deal with #9310.

@HeahDude HeahDude force-pushed the feature-collection_type-refactoring branch 2 times, most recently from 34d8ee6 to ccb9c25 Compare March 1, 2016 08:31
'bar',
);

$attr = array(
Copy link
Contributor

Choose a reason for hiding this comment

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

I would name this $expectedAttr and move it below the create() call to make the intent of the test clear. Usually, tests should be structured into the following sections (which may in part be missing):

  1. Fixture setup
  2. Execution of the test
  3. Test verification
  4. Cleanup

The create() statement belongs to 2, while $attr ($expectedAttr) is already part of the test verification. Hence I'd move it below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood, thanks, I'm fixing it.

@webmozart
Copy link
Contributor

Hey @HeahDude, thanks for working on this :) Before you go any further, I'd finish the PR as is and start any new work in a new PR. This looks good so far.

@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 1, 2016

It's as easy as update title and description and open a new PR for next commits ;)

Thank you very much for your encouraging feedback :)

@HeahDude HeahDude changed the title [RFC] [Form] CollectionType refactoring [Form] allow entry_options option of CollectionType to be dynamic Mar 1, 2016
@linaori
Copy link
Contributor

linaori commented Mar 1, 2016

What does this PR actually solve?

@HeahDude HeahDude force-pushed the feature-collection_type-refactoring branch 2 times, most recently from f7a848d to 4cd9d83 Compare March 1, 2016 10:22
@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 1, 2016

@iltar The CollectionType can nest any other form types including itself.
Some form types have options that might be useful to set depending on the entry that populates its data.

Example:

$builder->add('categories', CollectionType::class, array(
    'entry_type' => ChoiceType::class,
    'entry_options' => function ($category) {
        if (null !== $category) {
            return array(
                'choices' => $category->getTags(),
            );
        }

        return array();
    },
    ...

// or
$builder->add('metadata', CollectionType::class, array(
    'entry_type' => HiddenType::class,
    'entry_options' => function ($metadata) {
        $options = array();

        if (null !== $metadata) {
            $options['attr'] = array(
                'data-id' => $metadata->getId(),
                // ...
            );
        }

        return $options;
    },
// ...

But this will be even more powerful when CollectionType will be polymorphic as the callable will get the form type corresponding to the entry as second argument (PR coming soon).

$entryOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'entry';
$entryOptionsNormalizer = function (Options $options, $entryOptions) {
if (is_array($entryOptions)) {
Copy link
Member

Choose a reason for hiding this comment

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

this is broken in case the callable is an array (something like array($this, 'buildEntryOptions'))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof, thanks I'll fix it and add a test for this.

Copy link
Member

Choose a reason for hiding this comment

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

btw, please add a test using such a callable to prevent regressions in its support

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok looks like I got your tother comments with delay :) woking on it.

@linaori
Copy link
Contributor

linaori commented Mar 1, 2016

I see, that indeed looks like a nice feature to have!

};

$prototypeOptionsNormalizer = function (Options $options, $value) {
$entryOptions = is_array($options['entry_options'])
Copy link
Member

Choose a reason for hiding this comment

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

same issue here

@HeahDude HeahDude force-pushed the feature-collection_type-refactoring branch from 4cd9d83 to bff11e5 Compare March 1, 2016 13:02
@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 1, 2016

Btw

$builder->add('metadata', CollectionType::class, array(
    'entry_type' => HiddenType::class,
    'entry_options' => function ($metadata) {
        $options = array();

        if (null !== $metadata) {
            $options['attr'] = array(
                'data-id' => $metadata->getId(),
                // ...
            );
        }

        return $options;
    },

Should avoid the need of 'hidden' as widget option in ChoiceType, ref #6602

@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 1, 2016

Ok @stof, comments addressed :)

@HeahDude HeahDude force-pushed the feature-collection_type-refactoring branch from bff11e5 to e963175 Compare March 2, 2016 05:52
@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 2, 2016

Ok, since it would be too much conflict to harden options in 2.x and 3.0 is not LTS, I've squashed prototype_options handling. So there is now 2 commits for 2 bullets.

Also I've changed the scope of ResizeFormListener::getOptionsForEntry() from private to protected to avoid a BC break.

Doc PR adressed, ref symfony/symfony-docs#6319.

Green tests (failures still related to PHP 7 build and bootstrap form layout tests in Twig Bridge).

This one looks finished to me, needs final review. (and maybe a feature label :)

@@ -125,7 +128,7 @@ public function preSubmit(FormEvent $event)
if (!$form->has($name)) {
$form->add($name, $this->type, array_replace(array(
'property_path' => '['.$name.']',
), $this->options));
), $this->getOptionsForEntry($value)));
Copy link
Contributor

Choose a reason for hiding this comment

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

I just realize that there is a big issue with this solution. Above, in preSetData(), the method getOptionsForEntry() receives the data in the model format. For example, if we have a collection of Tag instances, a Tag instance would be passed.

Here, however, we pass the submitted data - a string (the tag name) or an array (multiple tag properties) - which are yet to be transformed back into a Tag instance. Consequently, the callback needs to be aware of either format and dynamically handle both. That won't be very intuitive nor easy to use in practice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I did not see that. So I need to add a test on submit and I think I can handle this.

Copy link
Contributor

Choose a reason for hiding this comment

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

How would you handle this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's funny because I was thinking about it last night and I wondered how was the prototype_data option handled though, and I realised while remembering your comment that you can only define the view data of a prototype. The CollectionType doc reference should be clear about it, especially if you need to transform value.

And just this morning, I see this ticket #17992...

Since there is no problem as long as the underlying data is a string value, it may not be working for an object underlying data.

I see an easy solution but it's not "pretty", it should be mentioned in the docs that the entry_options option as callable must test the string type :

'entry_options' => function ($entry) {
    $options = array(
        // set default
    );

    if (is_string($entry) {
        // $entry <=> $viewData
        // or $entry <=> $modelData <=> $viewData
        $options['global_dynamic'] = // use $entry as string
    }

    // Else entry is an instanceof `data_class` or null
    $options['model_dynamic'] = // use $entry as model data or null

    return $options;
},

But it would be better to solve this in a feature way, by adding for example an add_entry option (which may have its counterpart in a future allow_add'able ChoiceType as add_choice).

This option would be a callable getting passed the new view data as only argument and should return a new model data. Almost like empty_data but this last one is only called for an empty view data and takes the form as first argument.

Then we would be able to use add_entry call on the new view data before passing it to getOptionsForEntry.

Another solution is to try to fix this while refactoring CollectionType more globally thanks to a CollectionList support, since I'm already investigating this way.
I think this would allow to solve many old issues with this type, with at first dynamic indexing, and polymorphism.

What do you think ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of courses this is more a DX concern as one could use DataMapperInterface for that, but I'm not sure it's working while the new fields are added on PRE_SUBMIT event.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to add a test for it anyway, as for the behavior of prototype_data and empty_data on submit to avoid regressions such as #17992.

I keep investigating, I think this feature can be useful and doable even if it needs a more global refactoring of CollectionType.

It may not be finished for 3.1 though, but I'll try to work on that this week-end.

Copy link
Member

Choose a reason for hiding this comment

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

@HeahDude even with a refactoring, we will still need to add elements based on 2 different formats, as one case wants to add fields when knowing the model data and the other case when knowing the view data (and this cannot change with any refactoring, as it is part of the basic about how a collection type should work).

Btw, one of your previous comment assumes that the view data is a string. this assumption is wrong. you can perfectly have collections of objects (and then a compound form type as the entry type)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with don't misunderstood me, I would assume default view data is string when not null, or when compound an array or object.

In fact, I mentioned that ChoiceType is particular in this sense since it is always compound (whether it can be a select input with no children) and will always have a string view data.
That what I would like to ensure decoupling the data transformers from the list mapper in #18180.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact I think expanded ChoiceType is not far from the CollectionType and I was thinking about decoupling the form list creation with a factory interface as #14050 did for ChoiceType to allow dealing with polymorphic and custom indexed/named collection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

and dynamic entry options.

@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 2, 2016

Status: Needs work

@HeahDude
Copy link
Contributor Author

@webmozart What do you think about merging c969206 in 3.1, since prototype_data has never been added to the doc anyway and it could be useful to define specific attr to prototype.

In that case I'll try to keep working on CollectionType in a later PR.

@HeahDude
Copy link
Contributor Author

Status: Needs Review

for c969206

@@ -29,7 +29,9 @@ class ResizeFormListener implements EventSubscriberInterface
protected $type;

/**
* @var array
* A callable get passed each entry as only argument.
Copy link
Member

Choose a reason for hiding this comment

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

[...] gets passed [...] as the only [...]

@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 9, 2016

Status: Needs Work

@fabpot
Copy link
Member

fabpot commented Nov 9, 2016

@HeahDude What's the status of this PR?

@HeahDude
Copy link
Contributor Author

HeahDude commented Nov 9, 2016

Not ready for 3.2 sadly. I'll keep working on that for sure!

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Dec 6, 2016
@fabpot
Copy link
Member

fabpot commented Mar 22, 2017

@HeahDude Last call for 3.3 :)

@HeahDude
Copy link
Contributor Author

HeahDude commented Mar 27, 2017

I think this one will be easier to implement in symfony 4.x :). Closing for now.

@HeahDude HeahDude closed this Mar 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants