doctrine / mongodb-odm

The Official PHP MongoDB ORM/ODM
https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/
MIT License
1.09k stars 502 forks source link

Problem with Doctrine ORM / ODM differences in proxy class naming #2422

Closed ksowa closed 2 years ago

ksowa commented 2 years ago

Bug Report

Q A
BC Break no
Version 2.3.2

Summary

After upgrading from ODM v1.x, I noticed there is a difference in naming of proxy classes between Doctrine ORM and ODM. This wasn't the case in the 1.x. It causes some unexpected behaviors when trying to store objects loaded from ORM to the ODM source.

Current behavior

Currently when I load an object via ORM, its lazy-loaded relations are instances of proxy classes, e.g. Proxies\__CG__\App\Entity\User\Profile. It uses the __CG__ as the namespace's part - it's defined in Doctrine\Persistence\Proxy::MARKER.

On the other hand ODM expects the marker to be __PM__, as per ProxyManager\Inflector\ClassNameInflectorInterface::PROXY_MARKER. Because of that ODM fails when trying to find a mappings for such objects.

How to reproduce

Let's have two simple classes and assume their ORM and ODM mappings are correct:

class User
{
    private int $id;
    private Profile $profile;

    public function getProfile(): Profile
    {
        return $this->profile;
    }
}

class Profile
{ 
    // ...
}

When we properly load User instance from a DB and call $profile = $user->getProfile(), the $profile is going to be an instance of something like Proxies\__CG__\App\Entity\User\Profile.

If we then try to store that to MongoDB via ODM, it's going to fail with an error message similar to below.

Example:

$entityManager = \Doctrine\ORM\EntityManager::create(...);
$documentManager = \Doctrine\ODM\MongoDB\DocumentManager::create(...);

$profile = new Profile(...);
$documentManager->persist($profile);  // <<-- OK

$user = $this->entityManager->getRepository(User::class)->find(1);
$profile = $user->getProfile();

$documentManager->persist($profile);  // <<-- ERROR

Error message:

Doctrine\Persistence\Mapping\MappingException : The class 'Proxies\__CG__\App\Entity\User\Profile' was not found in the chain configured namespaces App\Entity

The problem is caused by differences between these two places:

  1. Doctrine\Persistence\Proxy::MARKER = '__CG__' (used by ORM)
  2. ProxyManager\Inflector\ClassNameInflectorInterface::PROXY_MARKER = '__PM__' (used by ODM)

Expected behavior

I would expect that both ORM and ODM use the same proxy marker, like it was in version 1.x of ODM. In that version ODM was using the Doctrine\Common\Util\ClassUtils::getRealClass() method, which was used by ORM as well.

malarzm commented 2 years ago

Sadly we can't fix this. With ODM 2.0 we decided to use https://github.com/Ocramius/ProxyManager instead of Doctrine's Proxy mechanism as what Marco wrote is way more powerful than our old solution. IIRC ORM also had plans to switch over to the new proxy mechanism which I guess would somehow fix your issue. But I don't know any details if and when ORM will do such a leap.

Offhand I'm not negating such approach worked but I do see a lot of things that could go wrong when passing uninitialized Proxies from other projects :D I'd advise to revisit this anyway.

ksowa commented 2 years ago

Thank you @malarzm for clarifying the situation.

It's not about using uninitialized objects. Even if ORM initializes it, it's still going to be an instance of a proxy class.

malarzm commented 2 years ago

It's not about using uninitialized objects. Even if ORM initializes it, it's still going to be an instance of a proxy class.

My comment was about using your approach in ODM 1.x :)

ksowa commented 2 years ago

My approach didn't change - I use initialized objects, however since they are lazy-loaded, they are instances of proxy classes.

But it's irrelevant in case of this issue report. Thanks again for your help!

ksowa commented 2 years ago

@malarzm I've done some more debugging and there's one thing which interested me. Maybe you would be able to explain the logic.

Let's take the same example as above - an instance of class Proxies\__CG__\App\Entity\User\Profile extends App\Entity\User\Profile.

For an instance of the above class:

  1. ODM calls Doctrine\Persistence\Mapping\AbstractClassMetadataFactory::getMetadataFor() with Proxies\__CG__\App\Entity\User\Profile as the parameter.
  2. This method calls loadMetadata() from the same class with the same param
  3. This one gets all the ancestors of the given class ($this->getParentClasses($name)) and tries to load their mappings in a loop (in the example it successfully loads mapping for App\Entity\User\Profile)
  4. Even if it is able to load mappings for all the ancestors, but fails at the proxy class, the whole method throws an exception

I'm wondering whether the idea behind that was to return parent's mapping in case of missing one for the given class. But I cannot tell that by just reading the code.

I'm not trying to complain about anything. I just thought I could help somehow with this piece of code.

malarzm commented 2 years ago

To my knowledge, no. The idea is that we need parent's mapping to inherit everything that was defined there, take a look at ODM's ClassMetadataFactory: https://github.com/doctrine/mongodb-odm/blob/2.3.x/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php#L127.

To solve your issue you could try one of the following:

  1. ODM provides an event for missing metadata (https://github.com/doctrine/mongodb-odm/blob/2.3.x/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php#L86). You could subscribe to it, check by yourself if you're dealing with ORM's Proxy, and fetch mapping for the real class if that's the case
  2. Decorate class resolver that is used by ODM so it can detect ORM's Proxy. Downside is that it's not designed to be an extension point from DocumentManager's perspective (https://github.com/doctrine/mongodb-odm/blob/2.3.x/lib/Doctrine/ODM/MongoDB/DocumentManager.php#L205) but you should be kinda-safe overriding it for ClassMetadataFactory (https://github.com/doctrine/mongodb-odm/blob/2.3.x/lib/Doctrine/ODM/MongoDB/DocumentManager.php#L207).
ksowa commented 2 years ago

@malarzm yeah, I've already solved my issue by implementing the subscriber you mentioned in (1). And later I started wondering why ODM tries to load parent's class mappings. Looks like I didn't realize ODM does that to support Mapped Superclasses feature.

Thank you again for the next part of clarifications!