DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph.
With Composer:
composer require myclabs/deep-copy
- How do you create copies of your objects?
$myCopy = clone $myObject;
- How do you create deep copies of your objects (i.e. copying also all the objects referenced in the properties)?
You use __clone()
and implement the behavior yourself.
- But how do you handle cycles in the association graph?
Now you're in for a big mess :(
DeepCopy recursively traverses all the object's properties and clones them. To avoid cloning the same object twice it keeps a hash map of all instances and thus preserves the object graph.
To use it:
use function DeepCopy\deep_copy;
$copy = deep_copy($var);
Alternatively, you can create your own DeepCopy
instance to configure it differently for example:
use DeepCopy\DeepCopy;
$copier = new DeepCopy(true);
$copy = $copier->copy($var);
Or you may want to roll your own deep copy function:
namespace Acme;
use DeepCopy\DeepCopy;
function deep_copy($var)
{
static $copier = null;
if (null === $copier) {
$copier = new DeepCopy(true);
}
return $copier->copy($var);
}
You can add filters to customize the copy process by adding filters:
$copier = new DeepCopy();
$copier->addFilter($filter, $matcher);
During the copy process, when a property is matched by a matcher, then the filter associated to this matcher is applied. By default, when a filter is applied this stops the process, i.e. the next matcher-filter pair will not be checked, unless the filter is implemented as a chainable filter.
Some generic filters and matchers are already provided.
Matcher
applies on a object attributeTypeMatcher
applies on any element found in graph, including array elements
The PropertyNameMatcher
will match a property by its name:
use DeepCopy\Matcher\PropertyNameMatcher;
// Will apply a filter to any property of any objects named "id"
$matcher = new PropertyNameMatcher('id');
The PropertyMatcher
will match a specific property of a specific class:
use DeepCopy\Matcher\PropertyMatcher;
// Will apply a filter to the property "id" of any instances of the class "MyClass"
$matcher = new PropertyMatcher('MyClass', 'id');
The TypeMatcher
will match any element by its type (instance of a class or any value that could be
parameter of gettype()
function):
use DeepCopy\TypeMatcher\TypeMatcher;
use Doctrine\Common\Collections\Collection;
// Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection
$matcher = new TypeMatcher(Collection::class);
Filter
applies a transformation to the object attribute matched byMatcher
TypeFilter
applies a transformation to any element matched byTypeMatcher
Except a few exceptions (when the filter is a chainable filter like
DoctrineProxyFilter
), matching a filter will stop the chain of filters (i.e. the next
ones will not be applied).
Let's say for example that you are copying a database record (or a Doctrine entity), so you want the copy not to have any ID:
use DeepCopy\DeepCopy;
use DeepCopy\Matcher\PropertyNameMatcher;
use DeepCopy\Filter\SetNullFilter;
$object = MyClass::load(123);
echo $object->id; // 123
$copier = new DeepCopy();
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copy = $copier->copy($object);
echo $copy->id; // null
If you want a property to remain untouched (for example, an association to an object):
use DeepCopy\DeepCopy;
use DeepCopy\Filter\KeepFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$copier->addFilter(
new KeepFilter(),
new PropertyMatcher(MyClass::class, 'category')
);
$copy = $copier->copy($object); // $object is an instance of MyClass
// $copy->category has not been touched
This filters allows to copy a Doctrine entity:
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
use DeepCopy\Matcher\PropertyTypeMatcher;
use Doctrine\Common\Collections\Collection;
$copier = new DeepCopy();
$copier->addFilter(
new DoctrineCollectionFilter(),
new PropertyTypeMatcher(Collection::class)
);
$copy = $copier->copy($object);
If you use Doctrine and want to copy an entity who contains a Collection
that you want to be reset, you can use this
filter:
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$copier->addFilter(
new DoctrineEmptyCollectionFilter(),
new PropertyMatcher(MyClass::class, 'myProperty')
);
$copy = $copier->copy($object);
// $copy->myProperty will return an empty collection
If you use Doctrine and use cloning on lazy loaded entities, you might encounter errors mentioning missing fields on a Doctrine proxy class (...\__CG__\Proxy). You can use this filter to load the actual entity behind the Doctrine proxy class.
Make sure, though, to put this as one of your very first filters in the filter chain so that the entity is loaded before other filters are applied!
This filter won't stop the chain of filters (i.e. the next ones may be applied).
use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
use DeepCopy\Matcher\PropertyNameMatcher;
$copier = new DeepCopy();
$copier->addFilter(new DoctrineProxyFilter(), new DoctrineProxyMatcher());
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copy = $copier->copy($object);
// $copy should now contain a clone of all entities, including those that were not yet fully loaded.
- If you want to replace the value of a property:
use DeepCopy\DeepCopy;
use DeepCopy\Filter\ReplaceFilter;
use DeepCopy\Matcher\PropertyMatcher;
$copier = new DeepCopy();
$copier->addFilter(
new ReplaceFilter(
function ($currentValue): string {
return $currentValue . ' (copy)'
}
),
new PropertyMatcher(MyClass::class, 'title')
);
$copy = $copier->copy($object); // $object is an instance of MyClass
// $copy->title will contain the data returned by the callback, e.g. 'The title (copy)'
- If you want to replace whole element:
use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ReplaceFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
$copier = new DeepCopy();
$copier->addFilter(
new ReplaceFilter(
function (MyClass $myClass): string {
return get_class($myClass)
}
),
new TypeMatcher(MyClass::class)
);
$copy = $copier->copy([new MyClass, 'some string', new MyClass]);
// $copy will contain ['MyClass', 'some string', 'MyClass']
Stop DeepCopy from recursively copying element, using standard clone
instead:
use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ShallowCopyFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
use Mockery as m;
$copier = new DeepCopy();
$copier->addTypeFilter(
new ShallowCopyFilter,
new TypeMatcher(m\MockInterface::class)
);
$myServiceWithMocks = new MyService(
m::mock(MyDependency1::class),
m::mock(MyDependency2::class)
);
$copy = $copier->copy($myServiceWithMocks)
// All mocks will be just cloned, not deep copied
This package is distributed under the MIT license.
Running the tests is simple:
vendor/bin/phpunit
Get professional support via the Tidelift Subscription.