Skip to content
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] Finally document Data Mappers #6900

Closed
wants to merge 1 commit into from

Conversation

wouterj
Copy link
Member

@wouterj wouterj commented Aug 21, 2016

It's probably the biggest undocumented feature in Symfony for years. Today, I was sad I had to tell yet another beginner "there is this awesome feature, but it's undocumented...". Let's finally document this great feature!

/fixes #5459
/ping @webmozart I would like your feedback on this article. The difference between transformers and mappers is hard to understand and we have to describe it pitch perfect to avoid confusion.

@wouterj wouterj added the Form label Aug 21, 2016
@wouterj wouterj force-pushed the issue-5439/form-data-mappers branch 2 times, most recently from cdc959c to 1da6c48 Compare August 22, 2016 07:28
reference. It recieves the form fields in a :phpclass:`RecursiveIteratorIterator`
and the view data.

When an errors occurs, a
Copy link

@snoek09 snoek09 Aug 27, 2016

Choose a reason for hiding this comment

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

typo; When an error occurs

single: Form; Data mappers

How to Use Data Mappers
=======================

This comment was marked as resolved.

the form. They are responsible for mapping the data to the fields and back. The
built-in data mapper uses the :doc:`PropertyAccess component </components/property_access>`
and will fit most cases. However, you can create your own data mapper for the
other cases, for instance when dealing with immutable objects.
Copy link
Member

Choose a reason for hiding this comment

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

However, you can create your own data mapper that could, for example, pass data to immutable objects via their constructor.


* **Data transformers** change the representation of a value. E.g. from
``"2016-08-12"`` to a ``DateTime`` instance;
* **Data mappers** map data (e.g. an object) to form fields.
Copy link
Member

Choose a reason for hiding this comment

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

... to form fields, and vice versa.

Nice, short explanation between the two


Changing a ``YYYY-mm-dd`` string value to a ``DateTime`` instance is done by a
data transformer. Mapping this ``DateTime`` instance as value of a property on
the entity is done by a data mapper.
Copy link
Member

Choose a reason for hiding this comment

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

Mapping this ``DateTime`` instance to a property on your object (e.g. by calling a setter or some other method) is done by a data mapper.

Creating a Data Mapper
----------------------

Assume you're saving a set of colors in the database. For this, you're using an
Copy link
Member

Choose a reason for hiding this comment

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

Suppose that you want to save a set of colors to the database.

}

The form type should be allowed to edit a color. As the color object is
immutable, a new color object has to be created each time one of the values is
Copy link
Member

Choose a reason for hiding this comment

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

As the color -> But because you've decided to make the Color object immutable, a new...

$green = $forms['green']->getData();
$blue = $forms['blue']->getData();

$data = new Color($red, $green, $blue);
Copy link
Member

Choose a reason for hiding this comment

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

woh! I pass by reference so rarely, that I'm honestly shocked that you can simply override the $data variable and this works! If so (I'm sure it does - Bernhard's blog post uses this same method), maybe we should add a quick comment:

// Overriding $data is enough, since it is passed by reference.

@weaverryan
Copy link
Member

Status: Reviewed

This is wonderful! I left minor comments, but this is ready to go :)

👍


The data passed to the mapper is *not yet validated*. This means that your
objects should allow being created in an invalid state in order to produce
user-friendly errors in the form.
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to explain what you have to do when your object's constructor raises exceptions in case some assertions are not fulfilled (@webmozart didn't address that concern in his blog post neither and that's the reason I am not sure if we should really cover it here or rather advise to use DTOs instead).

Copy link
Contributor

@HeahDude HeahDude Sep 21, 2016

Choose a reason for hiding this comment

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

This is also why scalar type hints in PHP7 cause some troubles. The form maps submitted data before validating its underlying data.

What we have to do in such case, is to validate data before mapping it ourselves :( I think he talked about it in his last conferences (suggestion to use custom data mappers for PHP 7).

Not sure what's the best way to approach this problem though, a "caution" note might be enough after all.

Copy link
Contributor

@ogizanagi ogizanagi Sep 21, 2016

Choose a reason for hiding this comment

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

If the underlying fields use proper form types (like an IntegerType for an object property typehinted int in the constructor), the field itself will throw a TransformationFailedException which will be rendered as an error on the view on this particular field.
Important part is that the field won't be mapped back and (IIRC) keeps original value. Thus, in most cases, I think it won't cause any issue with scalar typehints.

See ogizanagi/symfony@4f76c21 and FormTypeTest.php#L745-L752
(it doesn't use a scalar typehint, but a \DomainException is thrown if Money::amount isn't numeric. The case is similar).

Appart from the above point, catching the error and throwing a TranformationFailedException ourself is a good safe guard (I recommend to use the invalid_message option in order to render a less cryptic error message, but that's limited).

Copy link
Contributor

Choose a reason for hiding this comment

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

@ogizanagi Your test does not cover what I'm talking about. When clear missing is true or if you submit an empty string, the returned model data will be null and the call of the setter will end with a php error because of the type hint which does not trigger a casting of null even if strict_type is false although it does cast a string to an int.

Copy link
Contributor

@ogizanagi ogizanagi Sep 24, 2016

Choose a reason for hiding this comment

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

@HeahDude: Right, it won't cover everything anyway. However, regarding what you're talking about, maybe it would be interesting to make the transformers use the required option (or a new one if necessary for BC) in order to disallow the transformation and throw a TransformationFailedException('A number is required') in case the value is an empty string.

@xabbuh
Copy link
Member

xabbuh commented Sep 21, 2016

@HeahDude Would be nice to get your feedback on this too.

{
$resolver->setDefaults(array(
// when creating a new color, the initial data should be null
'empty_data' => null,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the default unless data_class is set or if some data prepopulates the form

Copy link
Member Author

Choose a reason for hiding this comment

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

Afaics, the default it '' and not null. I think I might added this because of that

}
}

All data mappers have to implement :class:`Symfony\\Component\\Form\\DataMapperInterface`.
Copy link
Contributor

Choose a reason for hiding this comment

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

You should maybe introduce the interface before showing an implementation

You can also implement ``DataMapperInterface`` in the ``ColorType`` and add
the ``mapDataToForms()`` and ``mapFormsToData()`` in the form type directly
to avoid creating a new class. You'll then have to call
``$builder->setDataMapper($this)``.
Copy link
Contributor

Choose a reason for hiding this comment

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

The example should do it like that since this does not makes sense to reuse it elsewhere, and the note could then say that you can handle it in a specific class to easily share it between types

Copy link
Member Author

Choose a reason for hiding this comment

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

I started doing this, but I rejected it. If we directly implement it like that, the structure of the article gets less clear (now it's create DTO, create mapper, use mapper).

@@ -13,9 +13,15 @@ to render the form, and then back into a ``DateTime`` object on submit.

.. caution::

When a form field has the ``inherit_data`` option set, Data Transformers
When a form field has the ``inherit_data`` option set, data transformers
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure about that one?

use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

class ColorMapper extends DataMapperInterface
Copy link
Member

Choose a reason for hiding this comment

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

implements

@mpdude
Copy link
Contributor

mpdude commented Oct 27, 2016

Awesome that finally some work has begun to fix this!

What the new section still does not explain is how DataMappers fit into the picture given at the events and data transformers page.

More specifically, does "data mapping" occur at a single point during the event flow? Could we add a note there saying that "at this point, a DataMapper (link) is used to ..."?

And with regard to data transformers, is the DataMapper responsible for reading/writing "model data" to/from the "model object"? Or does it also get in touch with the "norm" and "view" data?

@mpdude
Copy link
Contributor

mpdude commented Oct 27, 2016

Skimming the code at https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php#L307, it looks as if the following happens:

  • setData($modelData) called on a Form
  • Dispatch FormEvents::PRE_SET_DATA which can change/replace the $modelData
  • Model transformer: transform() method to obtain normData from modelData
  • View transformer: transform() method to obtain viewData from normData
  • Data mapper: mapDataToForms() to map viewData to the form and its child elements
  • Dispatch FormEvents::POST_SET_DATA.

And then at https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php#L496:

  • submit($submittedData) called on a Form
  • Dispatch FormEvents::PRE_SUBMIT which can change/replace the $submittedData
  • Somehow the $submittedData is put on the form elements (I don't see it)
  • Data mapper: mapFormsToData() to read from form elements and put data onto viewData
  • View transformer: reverseTransform() viewData to normData
  • Dispatch FormEvents::SUBMIT
  • Model transformer: reverseTransform() normData to modelData
  • View transformer: transform()``modelData to viewData
  • Dispatch FormEvents::POST_SUBMIT

@B-Galati
Copy link
Contributor

@wouterj Is this PR dead ?

@HeahDude
Copy link
Contributor

For the record, here's a use case https://github.com/EnMarche/en-marche.fr/pull/1324/files#diff-8a3dda9de49dd1624e55e7e7d409853cR17.

I could have used a model transformer to handle string to array('entry' => string) conversion and let the property path mapper do its work.
But here's the point, I don't want to transform the value, I just want to map it. Adding a transformer has a performance cost and the default property path is already very heavy, using a simple mapper gets rid of both and this may count when you have thousands or millions of requests running complex code.
Using inherit_data does not play well with initialization and events which may be relied on for advanced cases.

Doing nothing won't work either as the component expects the type norm data to be an array, an object or null for a compound form, so it throws an exception on initializing data when trying to map the scalar data to the child. This cannot be changed in my case since I want to dynamically add this nested form.

Note that the form is compound by default anyway when extending AbstractType which has FormType as parent defining it. TextType originally simply extends it and override the compound option to false, and trying to add a child to it throws an exception.

There still are valid cases for using compound data (array and objects) with non compound types as long as we use a model transformer. For instance, a model date can be a text or a timestamp while the date form is compound when using select inputs or many text inputs, it then depends on a model transformer to normalize it to a date time object, a view transformer is also needed so the object is converting so a string for a single input or an array of sting that will be mapped to children choice or text fields).

Hope it helps while waiting for an improved docs.

@jaikdean
Copy link

jaikdean commented Apr 4, 2018

I'd love for this PR to be resurrected. I've been using Symfony since it was released and only learned about data mappers yesterday.

@wouterj wouterj force-pushed the issue-5439/form-data-mappers branch from 1da6c48 to bc61395 Compare May 8, 2018 16:55
@wouterj wouterj force-pushed the issue-5439/form-data-mappers branch from bc61395 to ba0526a Compare May 8, 2018 18:01
@wouterj
Copy link
Member Author

wouterj commented May 8, 2018

Sorry for ignoring this PR for that long. If I'm correct, I fixed all comments in this article. Let's do a final review + merge @symfony/team-symfony-docs

@xabbuh
Copy link
Member

xabbuh commented May 9, 2018

I think I would still add a warning that any exception being thrown in the constructor will not be handled or needs some extra code.

@javiereguiluz
Copy link
Member

@wouterj do you agree with the last comment from @xabbuh? If you do, please let's make a one final effort to finish this PR. Thank you!

@magnetik
Copy link
Contributor

Hey @wouterj do you need someone to take this over to finish it?

I'm willing to help but I'm not sure that I understood the limitation that needs to be explained.

@HeahDude
Copy link
Contributor

HeahDude commented Dec 8, 2018

Thank you @wouterj for your work. We continuated it in #10756, would be nice to have your review there. Closing here though, thanks!

@HeahDude HeahDude closed this Dec 8, 2018
xabbuh added a commit that referenced this pull request Dec 11, 2018
This PR was squashed before being merged into the 3.4 branch (closes #10756).

Discussion
----------

[Forms] Added data-mapper docs

#SymfonyConHackday2018

Improved version of #6900
Thank you @wouterj for a great job and the initial text! Thanx @HeahDude for helping me with this PR.

Commits
-------

b1cb1c0 [Forms] Added data-mapper docs
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.