Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Form] ViewData from a submitted form is not an object #20880

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

Closed
MetalArend opened this issue Dec 12, 2016 · 24 comments
Closed

[Form] ViewData from a submitted form is not an object #20880

MetalArend opened this issue Dec 12, 2016 · 24 comments
Labels

Comments

@MetalArend
Copy link
Contributor

Symfony: 3.2

What is the reasoning behind checking if the viewdata is an instance of the data_class in Form.php? Is this check preventing some situations that would be bad?

I got the corresponding LogicException in a situation that issued some strange demands on my code. Simplest code is in the zip in attachment: a ValueObject that encapsulates a string, and a formtype using this VO.

I used a ModelTransformer to convert my data from string to VO and back. The error demands that I set my data_class to null, or add a ViewTransformer. I added the ViewTransformer, as a ReversedTransformer from my ModelTransformer (not sure why ViewData would need to be an object). That did not work, so I changed the data_class to another option "data_class_my", to test if this would help. It did, untill I added some data to my formtype. I had to explicitly set the data_class to null, which makes no sense to do, as all seems to work if I simply remove the if block in Form.php on line 358.

  1. Why would Symfony tell me to convert my norm data to view data which is an object, when the view data I receive from the submitted form is never going to be an object? It seems unneeded to actually add a view data transformer there.
  2. Why can I not use data_class to tell my formtype to convert it to an object?

I know I can do this some other way with datamappers, DTO's, and so on..., but I'm really puzzled why this is not working. Is this actually a bug, or is it avoiding some more terrible situations?

Symfony.zip

Did a little digging, @webmozart, do you remember what is going on here? It was added in 2012, so might be hard to remember...

@xabbuh xabbuh added the Form label Dec 12, 2016
@MetalArend
Copy link
Contributor Author

MetalArend commented Dec 13, 2016

Some more investigation:

Setup 1:
No transformers
No "data_class" or "data_class_my" options required and defaulted in ObjectType
No "data" or "data_class" or "data_class_my" option in the form configuration in controller

Results in no prefill. I provided some text in the field, submitted the form and got that text as a string from getData() - works correctly.

Setup 2:
Clean setup (everything disabled)
Either set one of the following

  • Add only "data_class" option with Object::class in the controller
  • Add only "data" option with new Object in the controller
  • Add both of the previous

Results in a prefilled field with the text provided by the Object. After submitting, the getData() returns a string. Implicit data_class from data gets ignored, eplicit data_class gets ignored as well, ObjectType does nothing with it.

Setup 3:
Clean setup (everything disabled)

  • Add only "data_class" option with Object::class in the ObjectType as default
  • Add the Model Transformer

No prefill, filled in text, got Object with text from getData() - works correctly.

Setup 4:
Same as setup 3, but we add the "data" option with new Object in the controller.

This triggers the error "The form's view data is expected to be an instance of class Symfony\BugBundle\ValueObject\Object, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\BugBundle\ValueObject\Object."

Setup 4b:
Same as setup 4, trying to fix the error
I change the data_class in the controller by overwriting it with null, and inside the ObjectType I enable the other option "data_class_my", so I can determine the class inside my ObjectType.

This is the easiest solution. It's not clear to me why I would not use data_class for this, and why it would be blocked to do so? It works, but it seems like ignoring the problem, and moving it to some safer ground.

Setup 4c:
Same as setup 4, trying the other way mentioned in the error.
I add the ViewTransformer. It is the complete reverse from the ModelTransformer, and could be defined as ReversedTransformer(ModelTransformer()).

I can prefill with the Object data. But when I submit, the reverseTransform from my ViewTransformer is failing, as it receives not an object, but a string as $viewData. Is this expected? This also means I cannot simply use the ReversedTransformer. I would expect to not need a ViewTransformer however, so I tried to remove the if block, removed the ViewTransformer and submitted my form again. And it succeeds! So why is this error holding me back to do this? Is it needed for some other use cases?

@HeahDude
Copy link
Contributor

Hi @MetalArend, I will try to answer, IMO everything looks expected here.

Basically a compound form can have either an array or an object as underlying data. Only its children will reach an actual view data as string (in most cases the data that shows up in an input).

Internally the compound form uses its view data to map the data of the children.

So if the view data is an array because of your model transformer, setting the data_class to null is the way to go, that's what this exception is telling you.

I got the corresponding LogicException in a situation that issued some strange demands on my code. Simplest code is in the zip in attachment: a ValueObject that encapsulates a string, and a formtype using this VO.

I used a ModelTransformer to convert my data from string to VO and back.

The simplest way to handle your case IMO is to implement the DataMapperInterface instead of a transformer. You will get full control of what happen in this case, but the mapper will also get passed the view data when calling mapDataToForm(), so be careful when using transformers (even if it's still manageable though, I've already had such case: model transform + custom mapping using VO).

See also the discussion in #20641.

Setup 3:
Clean setup (everything disabled)

Add only "data_class" option with Object::class in the ObjectType as default
Add the Model Transformer
No prefill, filled in text, got Object with text from getData() - works correctly.

Setup 4:
Same as setup 3, but we add the "data" option with new Object in the controller. This triggers the >error "The form's view data is expected to be an instance of class >Symfony\BugBundle\ValueObject\Object, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\BugBundle\ValueObject\Object."

In setup 3, the pre set data is nullso it does not trigger any transformation, all is expected here.

This is the easiest solution. It's not clear to me why I would not use data_class for this, and why it would be blocked to do so?

data_class option is used when the model is null so the component can construct a new object before mapping.

There are globally two cases:

  1. The form is compound:
    a. Simple form: The model and view data is an array => data_class is null,
    b. Object form: the model and view data is an object => data_class is the class of that object
    c. Complex form: normalisation is needed that implies transformation => data_class should be null. It can happens with an object being represented as string in one field (think of the ChoiceType selecting objects.

  2. The form is NOT compound: data_class should be null as view data will always be a string or null.

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

@HeahDude I don't get it... Isn't view data supposed to be what you can receive from an HTTP requests? And what you use to set values of html input fields? How an object can be used for that?

BTW - I asked also a question on StackOverflow about the difference between model/norm/view data: http://stackoverflow.com/questions/41116099/form-norm-data-vs-view-data-whats-the-difference

Could you explain what is the difference between these three types of data in the context of what you say above?

@MetalArend
Copy link
Contributor Author

Wow, thanks for the complete explanation, @HeahDude.

So, I checked if I was using a compound form, but it's actually the only thing that stays all the time on false. So all my examples were on a not compound form. However, that is on purpose.

Noted that a DataMapper might be a solution, but I still want to see what is going on with the ViewTransformer. So, instead of all these different setups, let me explain which specific situation feels strange to me.

Two questions remain...

First: my form is not compound, and I'm fine with my view data always being a string or null. What I'm confused about, is that if I set my data_class to something not null, the only thing that seems to go wrong is that if statement, 'cause when I remove that, all works correctly. So I'm still wondering which use case it is protecting, or if it might be obsolete to protect this?

Second: the error thrown should probably have some extra check on being a compound field or not, because in case of a not compound field setting the data_class to not null, adding a view transformer will never work correctly. The error tells me to create a ViewTransformer that will change my normData to viewData that is an object. Adding such a ViewTransformer will remove the error, but submitting will trigger the reverseTransform, as it will be coded as suspecting the viewData to be an object, while it seems clear that will never be the case (if I understood your explanation correctly).

@HeahDude
Copy link
Contributor

@Isinlor

Isn't view data supposed to be what you can receive from an HTTP requests? And what you use to set values of html input fields? How an object can be used for that?

Your statement is true for non compound forms only.

@MetalArend

First: my form is not compound, and I'm fine with my view data always being a string or null. What I'm confused about, is that if I set my data_class to something not null, the only thing that seems to go wrong is that if statement, 'cause when I remove that, all works correctly. So I'm still wondering which use case it is protecting, or if it might be obsolete to protect this?

You shouldn't use data_class in such case since the view data cannot be an object in that case, the view transformer or a custom mapper should be responsible of creating/updating an entity.

Second: the error thrown should probably have some extra check on being a compound field or not, because in case of a not compound field setting the data_class to not null, adding a view transformer will never work correctly. The error tells me to create a ViewTransformer that will change my normData to viewData that is an object. Adding such a ViewTransformer will remove the error, but submitting will trigger the reverseTransform, as it will be coded as suspecting the viewData to be an object, while it seems clear that will never be the case (if I understood your explanation correctly).

The component is not able to decide such things, the dev is responsible for the configuration, and this case is exactly why I suggested to use a custom DataMapper. Again I'll take the ChoiceType as an example because it's one of the more complex core type, if it's not multiple and its data is an object (with choices as an array of objects), data_class is still null and the form may be compound depending on the expanded option, since using radios is done while adding children to the form and it's not the case with a select input.

IMHO this is more a documentation issue that I'll try to address early next year (which is soon now).

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

@HeahDude If I understand correctly then concept of view data is actually overloaded? Besides being view data it is also some kind of internal mapping data? Shouldn't it be explicitly separated?

Also I found article by webmozart about DataMapper and value objects in Symfony forms. Should be helpful.

@MetalArend
Copy link
Contributor Author

That link is interesting, but there's nothing there that helps to understand why adding a perfectly correct ViewTransformer is not helping at all.

If you look at this page: http://symfony.com/doc/current/form/data_transformers.html, the explanation of normData, viewData and modelData looks very clear to me. From that, I get that if I have a ViewTransformer where the transform ... eh ... transforms my normData=string to viewData=object, the reverseTransform should transform viewData=object to normData=string. The error tells me that adding a ViewTransformer with a transform method like here mentioned will solve the issue, and while that is correct, it also introduces a new issue, because the reverseTransform will trigger a new error after submitting the form. Why? Because it seems that the reverseTransform never gets anything else than a string.

@HeahDude
Copy link
Contributor

Because it seems that the reverseTransform never gets anything else than a string.

the reverse transformation of a view transformer expects either null or:

  1. a string for simple form
  2. an array of string for compound form

@MetalArend
Copy link
Contributor Author

the reverse transformation of a view transformer expects either null or a string for simple form or an array of string for compound form

Why is the error doing an instanceof from the viewData at L358, if viewData will only be null, a string or an array?

If I look at that $viewData after the ->normToView() method call at line 353, it is the string that I would suspect it to be, but the error wants me to change it to the object specified in data_class, making me write a ViewTransformer transforming to an object, while you tell me the reverseTransform method of any ViewTransformer will never make it passed an object as input.

Is there a good explanation on why this check needs to be there? And if it needs to be there, does it make sense to actually use it to check the view_data(previously client_data) against the data_class option, instead of checking the model data against data_class?

@HeahDude
Copy link
Contributor

Ok after discussing it again in private on the new Symfony Devs slack with @MetalArend, it looks like this issue is a kind of duplicate of #20526, therefore I propose to close here.

@MetalArend
Copy link
Contributor Author

So, the possible solutions are:

  • to use a datamapper
  • explicitly set the data_class to null
  • use empty_data

But the two original questions still remain.

There seems to be no way to use data_class AND create a ViewTransformer that actually will work correctly in the same FormType. So I would not mark this closed, as this was the actual problem. We can solve it by working around the problem, but the problem seems to be still there.

@HeahDude
Copy link
Contributor

HeahDude commented Dec 14, 2016

One can "use data_class AND create a ViewTransformer" but the data_class option should match what's the input of the reverse transform of the transformer not the model data.

EDIT:
input of the reverse transform of the transformer === output of the view transformer transform

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

@HeahDude

Internally the compound form uses its view data to map the data of the children.

I've done little bit more research:

As you say the compound option determines if PropertyPathMapper will be used. That's OK.
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)

Also as you say the Form class is using the mapper to map the view data to the forms. That's OK too.
$this->config->getDataMapper()->mapDataToForms($viewData, $iterator);

Then PropertyPathMapper mapper is setting the data on the forms. Hmm...
$form->setData($this->propertyAccessor->getValue($data, $propertyPath));

Why PropertyPathMapper trough Form is setting ViewData on forms that expect ModelData?
public function setData($modelData);

How did the ViewData changed to ModelData in the mapper? Why ModelData are not mapped on the forms? Why data_class is applicable to ViewData and not ModelData where it would make more sense?

It looks for me like this line: $this->config->getDataMapper()->mapDataToForms($viewData, $iterator);
is a bug and all this inconsistencies are the result of the bug. There should be $modelData, shouldn't it?

@HeahDude
Copy link
Contributor

Because children are submitted and transformed before being mapped in the parent while it's being submitted.

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

@HeahDude You mean this line? https://github.com/symfony/form/blob/master/Form.php#L618

The same issue applies, doesn't it? Data mapper is setting ViewData on forms that expect ModelData.

@HeahDude
Copy link
Contributor

Yes, which is coming after this one.

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

How does it change the fact that the PropertyPathMapper is mapping ViewData to ModelData and ModelData to ViewData?

Here mapper is wrongly setting ViewData as ModelData in child forms:
https://github.com/symfony/form/blob/master/Form.php#L385
https://github.com/symfony/form/blob/master/Extension/Core/DataMapper/PropertyPathMapper.php#L57

Here mapper is wrongly getting ModelData from child forms and sets them in ViewData:
https://github.com/symfony/form/blob/master/Form.php#L618
https://github.com/symfony/form/blob/master/Extension/Core/DataMapper/PropertyPathMapper.php#L93

@HeahDude
Copy link
Contributor

Here mapper is wrongly setting ViewData as ModelData in child forms:

If the view data was not expected one would get this exception first https://github.com/symfony/form/blob/master/Form.php#L358, which is behind the cause of this issue in the first place. But again, this is expected.

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

@HeahDude Then how do you explain the inconsistencies? The view data is not supposed to be object, the "if" there is wrong. I've checked with other people and I'm not loosing my mind ( I hope ;) ). The ViewData is supposed to be strings, arrays or maybe null: http://stackoverflow.com/questions/41116099/form-norm-data-vs-view-data-whats-the-difference

The names of data as used by PropertyPathMapper and Form do not match.

@HeahDude
Copy link
Contributor

HeahDude commented Dec 14, 2016

Please just remember this: data_class option is defining the type of the view data.

EDIT:
looks like we've written our messages at the same time :) but the answer is the same too ;)

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

Do you really think that data_class defining the type of the view data makes sense? I mean everyone for the last 4 years apparently was doing that, but does that make sense? How can you send PHP objects trough HTTP protocol?

@HeahDude
Copy link
Contributor

HeahDude commented Dec 14, 2016

HTTP protocol handles arrays of strings. I used to not understand it as you, you can read here #18053 (comment) that it was so strange to me that I've proposed to change it.

But empty_data and data_class are about the view data. each value of the array is set in a child. And when the child is mapped in the parent, the parent can be an object or an array and the PropertyPathMapper is used by default.

@Isinlor
Copy link

Isinlor commented Dec 14, 2016

We are going off-topic but don't you think that what are you doing here is cargo cult programming (no offense, I really appreciate the time you are committing to the issue and in general)?

The inconsistency is there, it should be fixed or well documented if it is too late to fix it, but denying existence of the inconsistencies and illogicality of the current state is not the solution in any case.

@HeahDude
Copy link
Contributor

HeahDude commented Dec 14, 2016

You can see symfony/symfony-docs#6265 and all the issues linked to it, and that's not all of them :)

My vote goes for a better documentation, because keeping BC here can be very complex.

Trust me, since I've opened that PR, I've learned a lot, also thanks to issues like this before, any feedback there will be gladly appreciated.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants