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

DeepCopy ignores Doctrine2 proxies #18

Closed
mrthehud opened this issue Aug 27, 2015 · 27 comments
Closed

DeepCopy ignores Doctrine2 proxies #18

mrthehud opened this issue Aug 27, 2015 · 27 comments

Comments

@mrthehud
Copy link

If the relation is fully hydrated (ie, no proxy) everything is good, and my "id" property matcher can set the property to null.

If, however, the relation isn't retrieved in the query, the related object is Proxy instance, and DeepCopy won't set the ID of that object to null.

Has anyone had this problem, and has anyone got a workaround, other than hydrating everything before copying (which might be the most sensible thing to do anyway)?

@mnapoli
Copy link
Member

mnapoli commented Aug 28, 2015

Yep, it was already reported in #15 but in a less explicit way so I've closed it in favor of this issue.

@vbartusevicius
Copy link

Can confirm that I have same issue.
Suggested solution in linked issue to $this->entityManager->getUnitOfWork()->clear(); is really bad.

@fsevestre
Copy link
Contributor

We have this issue too, any news ?

@mnapoli
Copy link
Member

mnapoli commented Oct 26, 2015

Nothing new, I'd gladly merge new filters (DoctrineProxyFilter?) and matchers (DoctrineProxyMatcher?) if anyone is up to doing it. Please note that the current architecture of this package allows you to solve that problem already (thanks to the extension points). The idea is that you can make this package fit all your needs (e.g. support any kind of 3rd party library, not only Doctrine). But I'm OK to merge Doctrine support in the main package.

@vbartusevicius
Copy link

AFAIK, there is no way You cound extract original Entity from Proxy. It's just how Doctrine works.

@mnapoli
Copy link
Member

mnapoli commented Oct 26, 2015

if ($object instanceof \Doctrine\Common\Persistence\Proxy) {
    $object->__load();
}

That will trigger loading the object behind the proxy (see https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Persistence/Proxy.php#L51). Then maybe also it's necessary to ignore the proxy's own properties? (https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Proxy/ProxyGenerator.php#L87-L101)

@mnapoli
Copy link
Member

mnapoli commented Oct 26, 2015

You cound extract original Entity from Proxy

FYI the proxy is the same object as the original entity. The proxy will intercept all method/property calls to load the properties in the same object.

@vbartusevicius
Copy link

Try this at Your own, and You will see.
The ::__load(); will hydrate the inner Entity - that's it, object's instance will still be a Proxy.

problem is that each property in Proxy looks like this:

class GatewayResult extends BaseGatewayResult implements \Doctrine\ORM\Proxy\Proxy
{
private $_entityPersister;
private $_identifier;
public $__isInitialized__ = false;
....
    public function setNationality($nationality)
    {
        $this->__load();
        return parent::setNationality($nationality);
    }

    public function getNationality()
    {
        $this->__load();
        return parent::getNationality();
    }
...
}

Problem is, that firstly You need to hydrate inner object, and only then You can skip Proxy fields, like:

private $_entityPersister;
private $_identifier;
public $__isInitialized__ = false;

That's why originally I created a #22 to override how refrection is created:

    protected function getReflection($object)
    {
        return new \ReflectionObject($object);
    }

@vbartusevicius
Copy link

FYI the proxy is the same object as the original entity.

Proxy is NOT the same as original Entity. It extends original entity. It even 'lives' in other namespace: Proxies\__CG__

@mnapoli
Copy link
Member

mnapoli commented Oct 26, 2015

Problem is, that firstly You need to hydrate inner object, and only then You can skip Proxy fields, like:

Yep that's exactly what I said in my comment above ;) It's doable with filters and I'm accepting pull requests that fix this correctly.

Proxy is NOT the same as original Entity. It extends original entity.

You are confusing classes and objects :) The entity and the proxy are the same object (aka instance). They are indeed not the same class, but that doesn't matter because I only said that to explain that your sentence "You cound extract original Entity from Proxy" makes no sense in this context.

@vbartusevicius
Copy link

What I was trying to say that it is same only when You interact with it. It's not the same when You have a Reflection.

@mnapoli
Copy link
Member

mnapoli commented Oct 26, 2015

I don't see why it wouldn't work with a ReflectionObject on the proxy (i.e. child class)? Private properties of parent classes are copied. What's needed is just to ignore properties of the Proxy class.

@andreaslarssen
Copy link

So, that's a +1 from me

@eved42
Copy link

eved42 commented Jul 22, 2016

I have the same issue on every object entity that I want to clone, including ones in ArrayCollection.
How to fix this ?

@knifesk
Copy link

knifesk commented Dec 14, 2016

Hi! This problem resurged to me (I created #15 a year ago) and clearing the unit of work and re-querying the entity does not solve my problem anymore, because the new query also returns a proxy as well. I'm going to try to solve the root of problem. Wish me luck 😀

@eved42
Copy link

eved42 commented Dec 15, 2016

About this, I solved this problem by cloning every entity in a collection.
For example, I wanted to clone a catalog of products.

My catalog has a collection of categories, and each category has a collection of products.
Instead of cloning directly my catalog, first I clone each category and then each product.

// I want to clone the tree structure of $origin into $catalog
foreach ($origin->getCategories() as $cat) {
    $clone = clone $cat;
    $clone->setCatalogId($catalog->getId());
    $clone->setCatalog($catalog);

    foreach ($cat->getProducts() as $prod) {
        $cp = clone $prod;
        $cp->setCatalogId($catalog->getId());
        $clone->addProduct($cp);
    }

    // [...] some treatments to clone parent categories...

    $catalog->addCategory($clone);
    $em->persist($clone);	// save clone category
    $em->flush();		// update database

    foreach ($clone->getProducts() as $p) {
	$p->setCategoryId($clone->getId());
	$p->setCategory($clone);
	$em->persist($p);		// save clone product
	$em->flush();			// update database
    }
}

$em->persist($catalog);
$em->flush();

For huge catalogs, I had to modify the script execution time to make it work :

$default = ini_get('max_execution_time');
ini_set('max_execution_time', 300);    // = 5 minutes
// cloning code
ini_set('max_execution_time', $default);

It works for me.

@vbartusevicius
Copy link

@eved42 - don't forget, that you're also cloning the the entire EntityManager with UnitOfWork inside. This way you can easily end-up out of memory.

@knifesk
Copy link

knifesk commented Dec 15, 2016

@vbartusevicius that's exactly the problem I'm facing. The library tries to clone a huge graph with all that references to the EntityManager and it runs out of memory. If I set the memory limit to -1 then the script dies at the 60 seconds of execution. Clearly that is no quality software

@mnapoli
Copy link
Member

mnapoli commented Dec 16, 2016

You could write filters and matchers (https://github.com/myclabs/DeepCopy#going-further) to filter out the properties that reference to the entity manager (that's what filters and matchers are for).

Also make sure you are using the existing Doctrine collection filters too: https://github.com/myclabs/DeepCopy#doctrinecollectionfilter

@knifesk
Copy link

knifesk commented Dec 23, 2016

Ok, while trying to solve my original problem I found out that DeepCopy is not having issues with cloning proxies. My memory limit problem was a circular reference in my model (which isn't wrong, just needed a KeepFilter in one attribute), but a deep debug on the library revealed that the proxies doesn't hold references to the entityManager as @vbartusevicius suggested.

There is 3 attributes in a proxy that doesn't belong to the original entities: __isInitialized__, __initializer__ and __cloner__ none of which contains references to the previous mentioned objects.
There is almost no performance issues by cloning this attributes, as initializer is a closure, isInitialized is just a boolean and cloner is only defined when the entity haves a __clone method defined (not 100% sure, so don't quote me on this), but I don't think that cloning this attributes is a good idea, so I added this filter lines to my code and everything works perfectly.

$deepCopy->addFilter(new KeepFilter(), new PropertyNameMatcher('__isInitialized__'));
$deepCopy->addFilter(new KeepFilter(), new PropertyNameMatcher('__initializer__'));
$deepCopy->addFilter(new KeepFilter(), new PropertyNameMatcher('__cloner__'));

In my code I'm doing a previous query to the database, then I fetch the entity to clone (it returns as an instance of DoctrineProxies\__CG__\) and it doesn't have problems when I clone it and then persist it.

I debugged the memory consumption and generated a graph of all the functions that are getting called and how much memory is this taking. Have a look!

http://imgur.com/a/W01N6

@mnapoli
Copy link
Member

mnapoli commented Jan 26, 2017

Congrats to @malc0mn for solving this 2 year old issue with 2 actual lines of code :)

@mnapoli mnapoli closed this as completed Jan 26, 2017
@fsmeier
Copy link

fsmeier commented Jun 7, 2017

Correct me if I am wrong (and totally stupid), but could it be that the ProxyFilter (it matches every property since it is not checked) is executed and returns after it at this line ? So no other filter is executed?

I have an ProxyClass of an entity and i am using it the following way:

$copyHelper = new DeepCopy();
$copyHelper->addFilter(new DoctrineProxyFilter(), new DoctrineProxyMatcher());
$copyHelper->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));

the second filter is never called on the property because the DoctrineProxyFilter matched first.

Best,
Florian

@mnapoli
Copy link
Member

mnapoli commented Jun 11, 2017

@IMM0rtalis yes that sounds correct, but I'm not sure what the problem is, the issue has been closed, maybe you should open a separate ticket to explain the problem?

@alvery
Copy link

alvery commented Aug 20, 2018

The problem is not solved. I can't affect doctrine to load true model instead of proxies (without unit of work hack). So when copyObject method reachs this proxy model it just returns object and will not follow all it's properties, because proxy model has __clone method. If I will not use flag useCloneMethod it will not copy correctly PersistentCollection.

if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { return $newObject; }

Any ideas? Maybe I'm missing something..

@theofidry
Copy link
Collaborator

theofidry commented Aug 20, 2018

@alvery did you see that you need to register a filter for it? (see https://github.com/myclabs/DeepCopy#doctrineproxyfilter-filter or #18 (comment))

@alvery
Copy link

alvery commented Aug 20, 2018

@theofidry Sure, it's the next line after new DeepCopy(true) statement. Maybe this is kind a new behaviour. From composer.lock:

"doctrine/annotations": "^1.0", "doctrine/doctrine-bundle": "^1.6", "doctrine/orm": "^2.4.5",

@theofidry
Copy link
Collaborator

The filter should kick in before if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { return $newObject; }.

Just make sure you are on the last version and if the problem persist I would need a reproducer please, I cannot really check it at the moment.

Also just in case since you mentioned a collection, there is more than one doctrine filter so make sure you have the right one registered

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

No branches or pull requests

10 participants