DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph.
Install with Composer:
composer require myclabs/deep-copy
Use it:
useDeepCopy\DeepCopy;
$copier = newDeepCopy();
$myCopy = $copier->copy($myObject);- 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 map of source objects to their copies and thus preserves the object graph.
To use it:
usefunctionDeepCopy\deep_copy;
$copy = deep_copy($var);Alternatively, you can create your own DeepCopy instance to configure it differently for example:
useDeepCopy\DeepCopy;
$copier = newDeepCopy(true);
$copy = $copier->copy($var);You may want to roll your own deep copy function:
namespaceAcme;
useDeepCopy\DeepCopy;
functiondeep_copy($var)
{
static$copier = null;
if (null === $copier) {
$copier = newDeepCopy(true);
}
return$copier->copy($var);
}You can add filters to customize the copy process.
The method to add a filter is DeepCopy\DeepCopy::addFilter($filter, $matcher),
with $filter implementing DeepCopy\Filter\Filter
and $matcher implementing DeepCopy\Matcher\Matcher.
We provide some generic filters and matchers.
DeepCopy\Matcherapplies on a object attribute.DeepCopy\TypeMatcherapplies on any element found in graph, including array elements.
The PropertyNameMatcher will match a property by its name:
useDeepCopy\Matcher\PropertyNameMatcher;
// Will apply a filter to any property of any objects named "id"$matcher = newPropertyNameMatcher('id');The PropertyMatcher will match a specific property of a specific class:
useDeepCopy\Matcher\PropertyMatcher;
// Will apply a filter to the property "id" of any objects of the class "MyClass"$matcher = newPropertyMatcher('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):
useDeepCopy\TypeMatcher\TypeMatcher;
// Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection$matcher = newTypeMatcher('Doctrine\Common\Collections\Collection');DeepCopy\Filterapplies a transformation to the object attribute matched byDeepCopy\MatcherDeepCopy\TypeFilterapplies a transformation to any element matched byDeepCopy\TypeMatcher
By design, matching a filter will stop the chain of filters (i.e. the next ones will not be applied).
Using the (ChainableFilter) won't stop the chain of filters.
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:
useDeepCopy\DeepCopy;
useDeepCopy\Filter\SetNullFilter;
useDeepCopy\Matcher\PropertyNameMatcher;
$object = MyClass::load(123);
echo$object->id; // 123$copier = newDeepCopy();
$copier->addFilter(newSetNullFilter(), newPropertyNameMatcher('id'));
$copy = $copier->copy($object);
echo$copy->id; // nullIf you want a property to remain untouched (for example, an association to an object):
useDeepCopy\DeepCopy;
useDeepCopy\Filter\KeepFilter;
useDeepCopy\Matcher\PropertyMatcher;
$copier = newDeepCopy();
$copier->addFilter(newKeepFilter(), newPropertyMatcher('MyClass', 'category'));
$copy = $copier->copy($object);
// $copy->category has not been touchedIf you use cloning on proxy classes, you might want to apply two filters for:
- loading the data
- applying a transformation
You can use the ChainableFilter as a decorator of the proxy loader filter, which won't stop the chain of filters (i.e.
the next ones may be applied).
useDeepCopy\DeepCopy;
useDeepCopy\Filter\ChainableFilter;
useDeepCopy\Filter\Doctrine\DoctrineProxyFilter;
useDeepCopy\Filter\SetNullFilter;
useDeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
useDeepCopy\Matcher\PropertyNameMatcher;
$copier = newDeepCopy();
$copier->addFilter(newChainableFilter(newDoctrineProxyFilter()), newDoctrineProxyMatcher());
$copier->addFilter(newSetNullFilter(), newPropertyNameMatcher('id'));
$copy = $copier->copy($object);
echo$copy->id; // nullIf you use Doctrine and want to copy an entity, you will need to use the DoctrineCollectionFilter:
useDeepCopy\DeepCopy;
useDeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
useDeepCopy\Matcher\PropertyTypeMatcher;
$copier = newDeepCopy();
$copier->addFilter(newDoctrineCollectionFilter(), newPropertyTypeMatcher('Doctrine\Common\Collections\Collection'));
$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 the
DoctrineEmptyCollectionFilter
useDeepCopy\DeepCopy;
useDeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
useDeepCopy\Matcher\PropertyMatcher;
$copier = newDeepCopy();
$copier->addFilter(newDoctrineEmptyCollectionFilter(), newPropertyMatcher('MyClass', 'myProperty'));
$copy = $copier->copy($object);
// $copy->myProperty will return an empty collectionIf 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 the DoctrineProxyFilter 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!
We recommend to decorate the DoctrineProxyFilter with the ChainableFilter to allow applying other filters to the
cloned lazy loaded entities.
useDeepCopy\DeepCopy;
useDeepCopy\Filter\Doctrine\DoctrineProxyFilter;
useDeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
$copier = newDeepCopy();
$copier->addFilter(newChainableFilter(newDoctrineProxyFilter()), newDoctrineProxyMatcher());
$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:
useDeepCopy\DeepCopy;
useDeepCopy\Filter\ReplaceFilter;
useDeepCopy\Matcher\PropertyMatcher;
$copier = newDeepCopy();
$callback = function ($currentValue) {
return$currentValue . ' (copy)'
};
$copier->addFilter(newReplaceFilter($callback), newPropertyMatcher('MyClass', 'title'));
$copy = $copier->copy($object);
// $copy->title will contain the data returned by the callback, e.g. 'The title (copy)'- If you want to replace whole element:
useDeepCopy\DeepCopy;
useDeepCopy\TypeFilter\ReplaceFilter;
useDeepCopy\TypeMatcher\TypeMatcher;
$copier = newDeepCopy();
$callback = function (MyClass$myClass) {
returnget_class($myClass);
};
$copier->addTypeFilter(newReplaceFilter($callback), newTypeMatcher('MyClass'));
$copy = $copier->copy([newMyClass, 'some string', newMyClass]);
// $copy will contain ['MyClass', 'some string', 'MyClass']The $callback parameter of the ReplaceFilter constructor accepts any PHP callable.
Stop DeepCopy from recursively copying element, using standard clone instead:
useDeepCopy\DeepCopy;
useDeepCopy\TypeFilter\ShallowCopyFilter;
useDeepCopy\TypeMatcher\TypeMatcher;
useMockeryasm;
$this->deepCopy = newDeepCopy();
$this->deepCopy->addTypeFilter(
newShallowCopyFilter,
newTypeMatcher(m\MockInterface::class)
);
$myServiceWithMocks = newMyService(m::mock(MyDependency1::class), m::mock(MyDependency2::class));
// All mocks will be just cloned, not deep copiedThe following structures cannot be deep-copied with PHP Reflection. As a result they are shallow cloned and filters are not applied. There is two ways for you to handle them:
- Implement your own
__clone()method - Use a filter with a type matcher
DeepCopy is distributed under the MIT license.
Running the tests is simple:
vendor/bin/phpunitGet professional support via the Tidelift Subscription.



