-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[Form] Add new way of mapping data using callback functions #37968
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
Conversation
7102a5a to
7b6c4dd
Compare
43e2ff7 to
8b14495
Compare
|
I am not very familiar with the Form component but your explanations are very clear and the concept looks very interesting 👏 |
cb9cc30 to
14bc40b
Compare
alcaeus
left a comment
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 really like how you solved the problem. Splitting data mapping and data accessors makes this more maintainable.
I've left a couple of comments regarding fallback accessors. I'm not sure if we should be nesting data accessors as you've suggested in this PR; I think using some form of chain accessors that iterates over a list of accessors may be better, but I defer to the judgment of the core devs.
src/Symfony/Component/Form/Extension/Core/DataAccessor/CallbackAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
Outdated
Show resolved
Hide resolved
97cd327 to
97edd18
Compare
src/Symfony/Component/Form/Extension/Core/DataAccessor/CallbackAccessor.php
Outdated
Show resolved
Hide resolved
7d2c504 to
8272c27
Compare
alcaeus
left a comment
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.
LGTM
8272c27 to
3efcb2d
Compare
3efcb2d to
1a3bf20
Compare
fabpot
left a comment
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.
Looks great to me! I'd love to get some feedback from @xabbuh and @weaverryan if possible.
f6cd5f8 to
258e80a
Compare
| * | ||
| * @throws Exception\AccessException If unable to write the given value | ||
| */ | ||
| public function setValue(&$objectOrArray, $value, FormInterface $form): void; |
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.
can't the method return the modified value instead of using a ref?
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.
Yes, it can. But it is not mandatory to return a new value here and I would like to keep this signature quite similar to the current DataMapperInterface::mapFormsToData(iterable $forms, &$viewData) method for consistency.
src/Symfony/Component/Form/Extension/Core/DataAccessor/CallbackAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/CallbackAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/ChainAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/ChainAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php
Outdated
Show resolved
Hide resolved
258e80a to
b223d7c
Compare
b223d7c to
878effa
Compare
xabbuh
left a comment
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.
Great work! 🎉 Apart from a minor comment this looks ready to me.
|
Thank you @yceruto. |
… functions (yceruto) This PR was merged into the master branch. Discussion ---------- [Form] Add new way of mapping form data using callback functions Documenting new feature symfony/symfony#37968 I don't think we need to cover all possible situations; the current example covers 3 of them, except the second one, but it seems enough for me, wdyt? > [...] you'll have to write your own data mapper in the following situations: >* When the property path differs for reading and writing >* When several form fields are mapped to a single method >* When you need to read data based on the model's state >* When the mapping of the model depends on the submitted form data ... By the way, the second situation "when several form fields are mapped to a single method" can be achieved this way: ```php $builder ->add('foo', TextType::class, [ 'setter' => function (Foobar $foobar, string $value) use (&$foo) { $foo = $value; }, ]) ->add('bar', TextType::class, [ 'setter' => function (Foobar $foobar, string $bar) use (&$foo) { $foobar->doSomething($foo, $bar); }, ]) ; ``` Since the data mapper will follow the same order in which the fields were defined, we can trust that the foo "setter" will be executed first and so the second "setter" will have the right value at the moment this function `$foobar->doSomething($foo, $bar)` is being executed. --- ping @alcaeus :) Commits ------- 53fb3c7 Add new way of mapping form data
|
Question about setter: ($setter)($data, $form->getData(), $form);If the settter is 'setter' => fn(Person &$person, ?string $name) => $person->rename($name),but |
Replaces #37614
What this solves
Objects and Forms have different mechanisms for structuring data. When you build an object model with a lot of business logic it's valuable to use these mechanisms to better collect the data and the behavior that goes with it. Doing so leads to variant schemas; that is, the object model schema and the form schema don't match up.
You still need to transfer data between the two schemas, and this data transfer becomes a complexity in its own right. If the objects know about the form structure, changes in one tend to ripple to the other.
Currently, the Data Mapper layer separates the objects from the form, transfering data between the two and also isolating them from each other. That's fine, but at present the default data mapper has a limitation: it's very tied to one property path (see
PropertyPathMapper).That said, you'll have to write your own data mapper in the following situations:
Also, when we create a new data mapper, we usually forget about checking the status of the given data and forms. Whether the data is empty or not; throw an exception if the given data is not an object/array and whether the form field is submitted/synchronized/disabled or not. Not doing that could lead to unwanted behavior.
What this proposes
Create a new way to write and read values to/from an object/array using callback functions. This feature would be tied to each form field and would also mean a new way of mapping data, but a very convenient one, in which it won't be necessary to define a new data mapper and take into account all what it would imply when you only need to map one field in a different manner or perhaps in only one direction (writing or reading the value).
This PR adds two new options for each form type:
getterandsetter, allowed to benullorcallable:This would give us the same possibilities as data mappers, but within the form field scope, where:
$personis the view data, basically the underlying data to the form.$formis the current child form that is being mapped.$nameis the submitted data that belongs to that field.These two callbacks will be executed following the same rules as for property paths before read and write any value (e.i. early return if empty data, skip mapping if the form field is not mapped or it's disabled, etc).
What this also proposes
I based the implementation on solving this problem first:
Thus, splitting the default data mapper
PropertyPathMapperinto two artifacts: "DataMapper" and "DataAccessor" would allow us to add multiple data accessors without having to constantly reinvent the wheel (the data mapper code). This way, we can focus on varying the code that handles data access.You can also think about a new
ReflectionAccessorfor instance? or use thisCallbackAccessorto map your form partially from an external API? yes, you could do it :)Here is a view of the proposed changes:
DataMapper will take care of common checks, iterating through the given child forms, managing the form data, and all the tasks necessary for mapping a standard form. Meanwhile, DataAccessor will handle how to read and write values to/from the underlying object or array.
BC
The
PropertyPathMapperis being deprecated in favor ofDataMapperclass, which uses thePropertyPathAccessorby default.Although
DataMapperis now the default for each compound form, the behavior must remain the same (tests prove it). Therefore, if thegetterorsetteroption is null (they are by default),CallbackAccessorwill fall back toPropertyPathAccessorfor reading or writing values.Sorry for the long description, but I think it is sometimes necessary for complex issues and big changes. Additionally, now you have a better understanding of what these changes are about.
/cc @xabbuh as creator of rich-model-forms-bundle and @alcaeus as you had worked on this issue.
WDYT?