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

Skip to content

EntityValueResolver injecting entities when it shouldn't #50739

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
janklan opened this issue Jun 22, 2023 · 14 comments · Fixed by #54455
Closed

EntityValueResolver injecting entities when it shouldn't #50739

janklan opened this issue Jun 22, 2023 · 14 comments · Fixed by #54455

Comments

@janklan
Copy link

janklan commented Jun 22, 2023

Symfony version(s) affected

6.3.0

Description

When a route parameter name matches a relation name in a Doctrine entity, EntityValueResovler is confused without providing the id parameter, such as #[MapEntity(id: 'parameterName')]. While providing the ID works, I think the default behaviour, injecting the first found entity, is incorrect.

Because this "inject first child" behaviour is inconsistent (sometimes the injected value is null, sometimes it's a first entity i n a set), I think this is at least a bug.

How to reproduce

I wrote a minimal reproducer with a few scenarios. Check out https://github.com/janklan/map-entity-bug and follow readme to see it.

You should see something along the lines of:

image

When you click either one of the scenarios (say 2.1), you'll see a dd() result of whatever the DefaultController received:

image image

The description next to each scenario should tell you whether I expected the $child to be populated or not, and which one.

Possible Solution

When the resolver has no data to fetch stuff, it should resolve into null.

Additional Context

This issue was previously discussed in #47166 and attempts to fix it were done in #47242, but I tripped over it again today, when I found an unexpected populated entity.

Some time ago I also reported the same behaviour in SensioFrameworkBundle repo for the now-deprecated ParamConverter, where I believe it was classed as "won't fix" because of the pending deprecation. I can't see the issues anymore, so I can't give you more detail.

@HypeMC
Copy link
Contributor

HypeMC commented Jun 22, 2023

I haven't gone through all the cases, but if you look at the actual queries it makes sense, e.g. case 2.1:

SELECT t0.name AS name_1, t0.id AS id_2, t0.parent_id AS parent_id_3, t0.root_id AS root_id_4
FROM foo t0 WHERE t0.root_id = 1 AND t0.parent_id = 2 LIMIT 1

The EntityValueResovler doesn't "inject the first child", instead it tries to load an entity by criteria. Since your Foo entity has both a property named root and one named parent, it loads by that criteria. This is not a bug, but expected behavior that can be disabled with the following configuration:

doctrine:
    orm:
        controller_resolver:
            # Set to false to disable using route placeholders as lookup criteria when the primary key
            # doesn't match the argument name
            auto_mapping: false

@janklan
Copy link
Author

janklan commented Jun 22, 2023

If you look at the actual queries, it makes sense

I have looked and agree there is nothing wrong with the query itself. I'm complaining that my entity is populated with data I didn't want when I didn't ask for it.

The EntityValueResovler doesn't "inject the first child", instead it tries to load an entity by criteria

I agree my description was inaccurate, but the end effect is the same: it populates the first entity in the relation.


With all of the above said, setting doctrine.orm.controller_resolver.auto_mapping: false indeed addresses the behaviour I described as buggy. Thank you for that; I was unaware of that option.

I'm curious, how does one find this in the docs? Searching for auto_mapping finds things related to orm.doctrine.auto_mapping but not doctrine.orm.controller_resolver.auto_mapping. I found it in bin/console config:dump-reference DoctrineBundle, but I am also guilty of not looking at the dumped reference very often. It's definitely not the first place I look at to learn new things

I still think the core issue remains, because with the default auto_mapping: false, the behaviour of EntityValueResolver is inconsistent, relying on whether or not the controller argument name appears in the parameters or query part of the request. See cases 2 and 3 and the differences in the behaviour:

image

I would expect the $child to be populated in both cases according to the auto_mapping setting if that one is meant to drive the business logic.

@HypeMC
Copy link
Contributor

HypeMC commented Jun 22, 2023

I'm curious, how does one find this in the docs?

Good question, not sure what the answer is. The option is mentioned here, but I knew about it from the original PR, so I can't judge how easy it is to find out about it without having some prior knowledge.

I still think the core issue remains, because with the default auto_mapping: false, the behaviour of EntityValueResolver is inconsistent, relying on whether or not the controller argument name appears in the parameters or query part of the request. See cases 2 and 3 and the differences in the behaviour:
I would expect the $child to be populated in both cases according to the auto_mapping setting if that one is meant to drive the business logic.

Honestly, I'm having trouble understanding this part, so I can't really comment 😄 . What inconsistencies are you referring to?

@janklan
Copy link
Author

janklan commented Jun 22, 2023

Honestly, I'm having trouble understanding this part,

No wonder. I meant to ask you to have a look at cases 4 and 5. Let's blame the early morning and lack of coffee on my end. Let me take you through the key points:

Case 4:

#[Route('/case4/{root?null}/{parent?}', name: 'case4')]
public function case4(?Foo $root = null, ?Foo $parent = null, ?Foo $child = null): Response {

Calling https://127.0.0.1:8002/case4/1/2, the $child is populated even though I didn't ask for it.

The entity resolver finds a result when searching for ['root' => 1, 'parent' => 2] and injects the result into the $child no matter what.

Case 5:

#[Route('/case5/{root?null}/{parent?null}/{child?}', name: 'case5')]
public function case5(?Foo $root = null, ?Foo $parent = null, ?Foo $child = null): Response {

Calling https://127.0.0.1:8002/case5/1/2, the $child is null as expected.


There is a bit of a twist there too. The child entity relates to both root and parent. Currently, when the child ID is not provided, EntityValueResovler probably uses both root and parent values to find the child entity.

So what if the parent entity didn't relate to the provided root ID?

That's why I generated two sets of root-parent-child fixtures. Going back to "Case 4", if you provide a root and parent that are unrelated, the child is not loaded. Just open https://127.0.0.1:8002/case4/1/5 of my reproducer.

This is a correct result of the current behaviour because both root and parent IDs are used to look up a child, but no child belongs to root 1 and parent 2. Hence I'm claiming the current behaviour is not right.


So there you have it. Case 4 populates $child (or not) based on the other request parameters; that's a problem, the way I see it: a match of a relation name and a route parameter can't be reliably used to conclude that the parameter carries an ID found on the other side of the relation.

@HypeMC
Copy link
Contributor

HypeMC commented Jun 22, 2023

Calling https://127.0.0.1:8002/case4/1/2, the $child is populated even though I didn't ask for it.

That's odd, I'm getting null with auto_mapping: false.

@janklan
Copy link
Author

janklan commented Jun 22, 2023

That's odd, I'm getting null with auto_mapping: false.

Yes, but not with auto_mapping: true, which is the default setting, so in use by a ton of projects.

I think I need to gather my thoughts to re-phrase what's the problem here. I hear your point with the auto_mapping. The config comment says clearly, # Set to false to disable using route placeholders as lookup criteria when the primary key doesn't match the argument name, which correctly describes what's happening, but I still think the idea behind it has some flaws.

If nothing else, the docs might need an upgrade.

I'll come back in a few days to either explain my beef or close this.

Thanks for taking time to discuss this topic.

@stof
Copy link
Member

stof commented Jun 22, 2023

This issue is the reason why I'm advocating since years to kill this auto_mapping feature. At least, when implement the argument resolver, I managed to make it possible to disable that feature globally in the config. In the DoctrineParamConverter of SensioFrameworkExtraBundle, there was no such option.

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

@nicolas-grekas
Copy link
Member

Please check #54455 for a possible solution to these ambiguities. Can you make your code behave more predictably with this PR @janklan?

@nicolas-grekas
Copy link
Member

I played with the reproducer and I confirm all cases are fixed by #54455, provided we set framework.controller_resolver.auto_mapping to false

@janklan
Copy link
Author

janklan commented Apr 4, 2024

@nicolas-grekas Great news, sorry for the no-answer; I was planning to check it out over the weekend. Do you still need me to see what's up? If you're happy, I'm happy.

fabpot added a commit that referenced this issue May 2, 2024
… favor of mapped route parameters (nicolas-grekas)

This PR was merged into the 7.1 branch.

Discussion
----------

[DoctrineBridge] Deprecate auto-mapping of entities in favor of mapped route parameters

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? |
| Issues        | Fix #50739
| License       | MIT

Auto-mapping of entities on controllers is a foot-gun when multiple entities are listed on the signature of the controller.
This is described extensively by e.g. `@stof` in the linked issue and in a few others.

The issue is that we try to use all request attributes to call a `findOneBy`, but when there are many entities, there can be an overlap in the naming of the field/associations of both entities.

In this PR, I propose to deprecate auto-mapping and to replace it with mapped route parameters, as introduced in #54720.

If we go this way, people that use auto-mapping to e.g. load a `$conference` based on its `{slug}` will have to declare the mapping by using `{slug:conference}` instead. That makes everything explicit and keeps a nice DX IMHO, by not forcing a `#[MapEntity]` attribute for simple cases.

Commits
-------

a49f9ea [DoctrineBridge] Deprecate auto-mapping of entities in favor of mapped route parameters
@miqrogroove
Copy link

miqrogroove commented May 21, 2024

Found this issue while trying to resolve the deprecation message. Shouldn't this have a recipe? Or does it truly deserve manual configuration?? Symfony 6.4.6 here. Can't upgrade to 7 yet.

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