-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
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
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
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
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:
getter
andsetter
, allowed to benull
orcallable
:This would give us the same possibilities as data mappers, but within the form field scope, where:
$person
is the view data, basically the underlying data to the form.$form
is the current child form that is being mapped.$name
is 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
PropertyPathMapper
into 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
ReflectionAccessor
for instance? or use thisCallbackAccessor
to 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
PropertyPathMapper
is being deprecated in favor ofDataMapper
class, which uses thePropertyPathAccessor
by default.Although
DataMapper
is now the default for each compound form, the behavior must remain the same (tests prove it). Therefore, if thegetter
orsetter
option is null (they are by default),CallbackAccessor
will fall back toPropertyPathAccessor
for 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?