Skip to content

[Form] Add new way of mapping form data using callback functions #14241

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 1 commit into from
Oct 3, 2020

Conversation

yceruto
Copy link
Member

@yceruto yceruto commented Sep 15, 2020

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:

$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 :)

fabpot added a commit to symfony/symfony that referenced this pull request Sep 16, 2020
…tions (yceruto)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[Form] Add new way of mapping data using callback functions

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| Tickets       | Fix #37597 (partially)
| License       | MIT
| Doc PR        | symfony/symfony-docs#14241

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`](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php)).

That said, 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
 * ...

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` and `setter`, allowed to be `null` or `callable`:
```php
$builder->add('name', TextType::class, [
    'getter' => function (Person $person, FormInterface $form): string {
        return $person->firstName().' '.$person->lastName();
    },
    'setter' => function (Person &$person, ?string $name, FormInterface $form): void {
        $person->rename($name);
    },
]);
```
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:

> #37614 (comment)
> [...] the property_path option defines the rules on how it's accessed. From there, the actual way it is accessed (direct property access, accessors, reflection, whatever) are hidden from view. All that matters is the property path (which is deduced from the name if not explicitly set). [...]

So splitting the default data mapper `PropertyPathMapper` into two artifacts: "[DataMapper](https://github.com/yceruto/symfony/blob/data_accessor/src/Symfony/Component/Form/Extension/Core/DataMapper/DataMapper.php)" and "[DataAccessor](https://github.com/yceruto/symfony/blob/data_accessor/src/Symfony/Component/Form/DataAccessorInterface.php)" would allow us adding multiple data accessors along the way (the code that varies in this case) without having to reinvent the wheel over and over again (the data mapper code).

You can also think about a new `ReflectionAccessor` for instance? or use this `CallbackAccessor` to map your form partially from an external API? yes, you could do it :)

Here is a view of the proposed changes:

![data_accessor](https://user-images.githubusercontent.com/2028198/91452859-16bf8f00-e84d-11ea-8564-d497c2f73730.png)

Where "DataMapper" will take care of common checks, iterates the given child forms, manages the form data and all what is needed for mapping a standard form, whereas "DataAccessor" will take care of how to read and write values to/from the underlying object or array.

## BC

The `PropertyPathMapper` is being deprecated in favor of `DataMapper` class, which uses the `PropertyPathAccessor` by default.

Although `DataMapper` is now the default for each compound form, the behavior must remains the same (tests prove it). So that if `getter` or `setter` option is null (they're by default) the `CallbackAccessor` will falls back to `PropertyPathAccessor` either for reading or writing values.

---

Sorry for the long description, but I think that sometimes it is necessary for too complex issues and big changes. Besides, now you know a little more about what these changes is about.

/cc @xabbuh as creator of [rich-model-forms-bundle](https://github.com/sensiolabs-de/rich-model-forms-bundle) and @alcaeus as you had worked on this issue.

WDYT?

Commits
-------

878effa add new way of mapping data using callback functions
@xabbuh xabbuh added this to the 5.2 milestone Sep 18, 2020
@wouterj
Copy link
Member

wouterj commented Oct 3, 2020

Thanks Yonel.

@wouterj wouterj merged commit 6aebe02 into symfony:master Oct 3, 2020
xabbuh added a commit that referenced this pull request Jul 1, 2022
…ptions (mpdude)

This PR was squashed before being merged into the 5.4 branch.

Discussion
----------

Add `getter` and `setter` in the section on basic form options

`getter` and `setter` were first documented in #14241, but only on the subpage on Data Mappers.

I usually check the general Form Options page when I look for a feature like this, so my suggestion is to mention it there as well.

Commits
-------

be84b5d Add `getter` and `setter` in the section on basic form options
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.

5 participants