-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
[Form] allow entry_options
option of CollectionType to be dynamic
#17968
Conversation
To go further with this implementation, I think |
34d8ee6
to
ccb9c25
Compare
'bar', | ||
); | ||
|
||
$attr = array( |
There was a problem hiding this comment.
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):
- Fixture setup
- Execution of the test
- Test verification
- Cleanup
The create()
statement belongs to 2, while $attr
($expectedAttr
) is already part of the test verification. Hence I'd move it below.
There was a problem hiding this comment.
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.
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. |
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 :) |
entry_options
option of CollectionType to be dynamic
What does this PR actually solve? |
f7a848d
to
4cd9d83
Compare
@iltar The 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 |
$entryOptionsNormalizer = function (Options $options, $value) { | ||
$value['block_name'] = 'entry'; | ||
$entryOptionsNormalizer = function (Options $options, $entryOptions) { | ||
if (is_array($entryOptions)) { |
There was a problem hiding this comment.
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')
)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
I see, that indeed looks like a nice feature to have! |
}; | ||
|
||
$prototypeOptionsNormalizer = function (Options $options, $value) { | ||
$entryOptions = is_array($options['entry_options']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same issue here
4cd9d83
to
bff11e5
Compare
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 |
Ok @stof, comments addressed :) |
bff11e5
to
e963175
Compare
e963175
to
457f28e
Compare
Ok, since it would be too much conflict to harden options in 2.x and 3.0 is not LTS, I've squashed Also I've changed the scope of 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))); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and dynamic entry options.
Status: Needs work |
@webmozart What do you think about merging c969206 in 3.1, since In that case I'll try to keep working on |
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. |
There was a problem hiding this comment.
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 [...]
Status: Needs Work |
@HeahDude What's the status of this PR? |
Not ready for 3.2 sadly. I'll keep working on that for sure! |
@HeahDude Last call for 3.3 :) |
I think this one will be easier to implement in symfony 4.x :). Closing for now. |
prototype_data
option in favor ofprototype_options
.ResizeFormListener
now passes each entry of the collection to theentry_options
option if it is a callable.