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

Better support of complex Doctrine association graphs #192

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, u
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: "packagist/myclabs/deep-copy"
tidelift: # Replace with a Tidelif URL
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
Expand Down
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph.

[![Total Downloads](https://poser.pugx.org/myclabs/deep-copy/downloads.svg)](https://packagist.org/packages/myclabs/deep-copy)
[![Integrate](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml/badge.svg?branch=1.x)](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml)
[![Total Downloads](https://poser.pugx.org/janklan/deepcopy/downloads.svg)](https://packagist.org/packages/janklan/deepcopy)
[![Integrate](https://github.com/janklan/deepcopy/actions/workflows/ci.yaml/badge.svg?branch=1.x)](https://github.com/janklan/deepcopy/actions/workflows/ci.yaml)

## Table of Contents

Expand Down Expand Up @@ -36,7 +36,7 @@ DeepCopy helps you create deep copies (clones) of your objects. It is designed t
Install with Composer:

```
composer require myclabs/deep-copy
composer require janklan/deepcopy
```

Use it:
Expand Down Expand Up @@ -379,6 +379,27 @@ $myServiceWithMocks = new MyService(m::mock(MyDependency1::class), m::mock(MyDep
```


## Persisting cloned Doctrine entities

If you're cloning Doctrine entities and are not automatically cascading the `persist` operation, you have two options:

1. Manually traverse your cloned association and persist new entities manually
2. Use the `DeepCopy::onObjectCopied` callback to process each cloned object at the end of its cloning process.

Here is an example of the `onObjectCopied` callback that would persist your entities.

```php
$copier = new DeepCopy();

/**
* @var EntityManagerInterface $entityManager
* @var DeepCopy $copier
*/
$copier->onObjectCopied = function (object $object) use ($entityManager) {
$entityManager->persist($object);
};
```

## Edge cases

The following structures cannot be deep-copied with PHP Reflection. As a result they are shallow cloned and filters are
Expand All @@ -401,6 +422,17 @@ Running the tests is simple:
vendor/bin/phpunit
```

### Support
## Acknowledgement

This is a fork of https://github.com/myclabs/DeepCopy/ - a massively popular library with millions downloads, which implies
inherent legacy issues: it needs to support old code. At some stage I needed this library more than it did and the [PR was
too heavy](https://github.com/myclabs/DeepCopy/pull/192) for timely consideration.

I decided to fork the project with a few objectives:

1. Drop older dependencies
2. Bring the code up a bit, PHP 8.2 and up. If you need to clone complex objects using older software, please refer to https://github.com/myclabs/DeepCopy/. This will be the last significant commit made on 1.x branch in this `janklan/deepcopy`.
3. Add functions that were missing - namely, the ability to automatically persist the cloned objects when cloning linked Doctrine objects
4. As a vague goal, tweaking how the filters work. The way they are split between `TypeFilter` and `(non-Type)Filter` + the fact one can be chained and the other can't, it simply didn't sit well with me.

Get professional support via [the Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-myclabs-deep-copy?utm_source=packagist-myclabs-deep-copy&utm_medium=referral&utm_campaign=readme).
Thanks @mnapoli for all your work.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "myclabs/deep-copy",
"name": "janklan/deepcopy",
"description": "Create deep copies (clones) of your objects",
"license": "MIT",
"type": "library",
Expand Down
13 changes: 13 additions & 0 deletions src/DeepCopy/DeepCopy.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ class DeepCopy
*/
private $useCloneMethod;

/**
* Custom callback executed once an object has been fully copied and all filters applied.
*
* The original purpose of this method is to be able to grab every new object and persist it if it is a Doctrine entity.
*
* @var ?\Closure(object): void
*/
public $onObjectCopied;

/**
* @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used
* instead of the regular deep cloning.
Expand Down Expand Up @@ -214,6 +223,10 @@ private function copyObject($object)
$this->copyObjectProperty($newObject, $property);
}

if ($this->onObjectCopied) {
$this->onObjectCopied->call($this, $newObject);
}

return $newObject;
}

Expand Down
31 changes: 28 additions & 3 deletions src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
*/
class DoctrineCollectionFilter implements Filter
{
/** @var array<class-string> */
private $ignoreClasses = [];

/**
* @param array<class-string> $ignoreClasses List of classes that should not be copied over to the new collection
*/
public function __construct($ignoreClasses = [])
{
$this->ignoreClasses = $ignoreClasses;
}

/**
* Copies the object property doctrine collection.
*
Expand All @@ -20,14 +31,28 @@ public function apply($object, $property, $objectCopier)
$reflectionProperty = ReflectionHelper::getProperty($object, $property);

$reflectionProperty->setAccessible(true);
$oldCollection = $reflectionProperty->getValue($object);
$collection = $reflectionProperty->getValue($object);

if (!empty($this->ignoreClasses)) {
$collection = $collection->filter(
function ($item) {
foreach ($this->ignoreClasses as $ignoredClass) {
if (is_a($item, $ignoredClass, true)) {
return false;
}
}

return true;
}
);
}

$newCollection = $oldCollection->map(
$collection = $collection->map(
function ($item) use ($objectCopier) {
return $objectCopier($item);
}
);

$reflectionProperty->setValue($object, $newCollection);
$reflectionProperty->setValue($object, $collection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,34 @@ function($item) {

$this->assertNotSame($stdClass, $objectOfNewCollection);
}

public function test_it_ignores_objects_of_classes_when_instructed()
{
$object = new stdClass();
$oldCollection = new ArrayCollection();
$oldCollection->add($stdClass = new stdClass());
$oldCollection->add(new \DateTimeImmutable()); // should not be copied to the new collection
$oldCollection->add(new \DateTime()); // should not be copied to the new collection
$object->foo = $oldCollection;

$this->assertCount(3, $object->foo);

$filter = new DoctrineCollectionFilter([\DateTimeInterface::class]); // <-- here is why

$filter->apply(
$object,
'foo',
function($item) {
return null;
}
);

$this->assertInstanceOf(Collection::class, $object->foo);
$this->assertNotSame($oldCollection, $object->foo);
$this->assertCount(1, $object->foo);

$objectOfNewCollection = $object->foo->get(0);

$this->assertNotSame($stdClass, $objectOfNewCollection);
}
}