Open-source project
myclabs/DeepCopy avatar
myclabs/DeepCopy

myclabs/DeepCopy: deep cloning PHP objects without losing the graph

Create deep copies (clones) of your objects

8,887 stars109 forksPHPMIT

At a glance

What is it?
DeepCopy is a PHP library for cloning an object and everything it references, including cycles. It is aimed at developers who need a copy of a Doctrine entity or a nested object graph rather than a shallow clone.
Who is it for?
Reach for myclabs/DeepCopy when a plain clone leaves you sharing nested objects or when the association graph has cycles, which is the case the README calls out for Doctrine entities. Skip it if you only ever copy flat value objects, since clone is enough there.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 49 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem DeepCopy solves for PHP object graphs

PHP's clone keyword copies an object's properties, but any property holding another object keeps pointing at the same instance. The README frames the escalation in three steps: first clone, then implementing __clone() by hand to copy referenced objects, then the point where cycles in the association graph turn that hand-written code into what the README calls a big mess. A cycle is the ordinary case in an ORM: an order references a customer and the customer references the order back. A naive recursive copy either loops forever or duplicates the shared node into two separate objects, and the second outcome is worse because it is silent. DeepCopy exists for that third step. Its audience is PHP developers working with Doctrine entities, nested DTOs, or any object model where references between objects are as meaningful as the values inside them.

How the copier traverses properties and preserves identity

The README states that DeepCopy recursively traverses all of the object's properties and clones them, and that to avoid cloning the same object twice it keeps a map of source objects to their copies, which is what preserves the object graph. That map is the whole design: traversal handles depth, the map handles cycles and shared references. Two entry points are documented. The function DeepCopy\deep_copy($var) is a ready-made call, and a DeepCopy instance gives you configuration. The constructor takes a boolean, shown as new DeepCopy(true) in the README, and the instance exposes copy($var). The README also shows the pattern of wrapping a static instance inside your own namespaced deep_copy function, which keeps the map alive across calls instead of rebuilding a copier each time. Customisation runs through addFilter($filter, $matcher): a filter transforms what it matches, a matcher decides what is matched. Matchers come in two families, DeepCopy\Matcher for object attributes and TypeMatcher for any element in the graph including array elements. PropertyNameMatcher('id') matches a property by name anywhere, PropertyMatcher('MyClass', 'id') narrows that to one class, and TypeMatcher matches by class or by anything gettype() would return. The ordering rule matters: by design, matching a filter stops the chain, so later filters never see that node unless the filter is wrapped in ChainableFilter.

Installing myclabs/DeepCopy and making a first copy

Installation is a single Composer command. The README gives it as composer require myclabs/deep-copy, and the package name on Packagist follows the myclabs/deep-copy form even though the repository is myclabs/DeepCopy.

bash
composer require myclabs/deep-copy

The smallest useful program uses the shipped function. Pass any variable and you get back a copy; for an object graph, nested objects come along and shared references stay shared inside the copy.

php
use function DeepCopy\deep_copy;

$copy = deep_copy($var);

When you need configuration, build an instance instead. The README shows the constructor receiving true and the copy being taken through the instance method.

php
use DeepCopy\DeepCopy;

$copier = new DeepCopy(true);
$copy = $copier->copy($var);

The first real use case in the README is a database record or Doctrine entity whose copy should not carry the original ID. A SetNullFilter paired with PropertyNameMatcher('id') does that: the README's example prints 123 before the copy and null after it.

php
use DeepCopy\DeepCopy;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\PropertyNameMatcher;

$copier = new DeepCopy();
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copy = $copier->copy($object);

Where the filter design bites back

The filter chain is the part most likely to surprise. Because a matching filter stops the chain, adding SetNullFilter for every property named id will also blank IDs on objects you meant to copy whole, and any filter registered after it for that same node will not run. ChainableFilter exists precisely to avoid that stop, and the README's example combines it with DoctrineProxyFilter so that a proxy is loaded first and a later SetNullFilter still applies to the id. That is a two-filter arrangement for one node, and it is easy to get wrong if you assume filters compose in registration order without knowing the stopping rule. KeepFilter is the other side of the same coin: it marks a property, for example an association to a category, as untouched, so the copy shares that object with the original. That is a deliberate aliasing decision, and it means the copy is not fully independent. There is also no documented rollback or dry-run: the README does not describe a way to preview which filters would fire before copy() runs. Finally, if your objects hold resources such as open file handles or PDO connections, nothing in the README suggests DeepCopy treats them specially.

DeepCopy compared with a hand-written __clone

The alternative the README itself sets up is overriding __clone() and implementing the copy behaviour yourself. The difference is not convenience, it is where the graph state lives. A __clone() method is per class and has no memory of the wider graph, so each class must know how to detach itself from its neighbours, and cycles have to be broken by hand with a visited set you maintain yourself. DeepCopy moves that state into the copier, which keeps the source-to-copy map across the whole traversal. The cost is that copying becomes a library concern rather than a class concern: your classes stay ignorant of copying, but behaviour such as blanking IDs or skipping a collection now lives in filter registrations outside the class, which is harder to discover when reading the class alone. For a small model with one level of nesting, __clone() is less machinery. For Doctrine entities with bidirectional associations, the filter classes the README documents (DoctrineCollectionFilter, DoctrineEmptyCollectionFilter, DoctrineProxyFilter, ReplaceFilter, ShallowCopyFilter) are doing work you would otherwise repeat in every entity.

Maintenance, upgrades and the MIT licence

The repository is not archived and the last push was on 2026-08-12, so work on the 1.x branch is recent. The release history is informative about upgrade risk: 1.13.3 in July 2025, 1.13.4 in August 2025, then 1.14.0 on 2026-08-11, roughly a year later. That cadence suggests the library is stable rather than fast-moving, and a minor version bump after a long quiet period is the kind of release worth reading before you pin it. There is no changelog in the top-level entries listed for the repository (the entries are .gitattributes, .github/, .gitignore, .scrutinizer.yml, LICENSE, README.md, composer.json, doc/, fixtures/, phpunit.xml.dist, src/ and tests/), so the release notes on the tag are the place to look. The licence is MIT, which permits commercial and closed-source use and requires the copyright notice and permission notice to be retained; that is a general description of the licence, not legal advice, and your own counsel should confirm how it applies to your distribution. Practically, the upgrade cost is low if you only call deep_copy(), and higher if you subclass filters or matchers, since those are the extension points most exposed to internal change.

Editorial conclusion

Reach for myclabs/DeepCopy when a plain clone leaves you sharing nested objects or when the association graph has cycles, which is the case the README calls out for Doctrine entities. Skip it if you only ever copy flat value objects, since clone is enough there. Before adopting it in a Doctrine project, check which filter classes your version ships (DoctrineCollectionFilter, DoctrineProxyFilter, DoctrineEmptyCollectionFilter) and read the edge cases section of the README, because the filter chain stops at the first match unless you wrap it in ChainableFilter.

Frequently asked questions

What is a deep copy in myclabs/DeepCopy?

It is a copy of an object in which the objects referenced by its properties are copied too, rather than shared with the original. The README contrasts this with plain clone, which copies the object but leaves referenced objects pointing at the same instances.

How do you make a deep copy with myclabs/DeepCopy?

Install it with composer require myclabs/deep-copy, then either call the DeepCopy\deep_copy($var) function or create a DeepCopy instance and call copy($var) on it. The README shows both forms.

What is the difference between shallow copy and deep copy in myclabs/DeepCopy?

A shallow copy, which is what clone gives you, leaves properties that hold other objects pointing at the originals. DeepCopy traverses those properties and clones them, keeping a map of source objects to copies so that cycles and shared references are preserved rather than duplicated.

What is the difference between copy and deepcopy in myclabs/DeepCopy?

The README shows clone as the plain copy, which duplicates the object but not the objects its properties reference. DeepCopy's copy() walks those references and clones them, using a source-to-copy map so the same object is not cloned twice.

How do you use myclabs/DeepCopy?

Call DeepCopy\deep_copy($var) for the default behaviour, or create a DeepCopy instance to configure it. Configuration goes through addFilter($filter, $matcher), with matchers such as PropertyNameMatcher, PropertyMatcher and TypeMatcher deciding which nodes a filter applies to.

How do you use copy.deepcopy in myclabs/DeepCopy?

The PHP equivalent of copy.deepcopy is the DeepCopy\deep_copy($var) function, or $copier->copy($var) on a DeepCopy instance. Both are shown in the README's usage section.

Official sources

  1. Issues
  2. License: MIT
  3. myclabs/DeepCopy on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/myclabs-deepcopy.svg)](https://hysenlabs.com/projects/myclabs-deepcopy)