Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Deep Copy Laravel Package

myclabs/deep-copy

DeepCopy creates deep clones of PHP objects, recursively copying referenced objects while preserving the object graph and safely handling cycles. It avoids duplicating objects and supports filters and matchers to customize copy behavior.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (and check what you actually have):

composer require myclabs/deep-copy
composer why myclabs/deep-copy   # PHPUnit pulls it in as a dev dep — don't rely on that in production code

Since 1.14 the package requires PHP 8.0+ (7.x support was dropped). If you still run 7.4 anywhere, pin ^1.13.

Where to look first:

  1. DeepCopy\DeepCopy — the only class you normally touch: copy() plus addFilter() / addTypeFilter() / prependTypeFilter().
  2. DeepCopy\Filter\* — SetNullFilter, KeepFilter, ReplaceFilter, ShallowCopyFilter, ChainableFilter.
  3. DeepCopy\Matcher\* — PropertyNameMatcher, PropertyMatcher, PropertyTypeMatcher.
  4. Everything else (Doctrine\*, TypeFilter\*) is only relevant once you hit a specific problem.

First use case: duplicating a record with its graph.

$model->replicate() copies attributes only — relations are lost. That's the gap DeepCopy fills:

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

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

$order = Order::with('items.options')->findOrFail($id);
$copy  = $copier->copy($order);

$copy->exists = false;      // still true after copying — handle explicitly
$copy->wasRecentlyCreated = false;
$copy->push();              // persists the whole copied graph with new IDs

Register it once so you're not rebuilding filter chains on every call:

// app/Providers/AppServiceProvider.php (boot)
$this->app->singleton(DeepCopy::class, function () {
    $copier = new DeepCopy();
    $copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
    return $copier;
});

The global deep_copy($var) helper is fine for one-off scripts, but it uses its own statically cached DeepCopy instance — filters you register elsewhere will not apply to it.

Implementation Patterns

1. Configured singleton + per-purpose copiers

Keep one container-bound copier for the common case (as above), and construct short-lived DeepCopy instances for specialized jobs where a broad filter would be wrong. Filters are additive and ordered, so "one giant shared instance" gets fragile fast.

2. "Duplicate this record" admin action

The classic Laravel workflow: copy an aggregate, blank the identity, tweak the labels.

$copier = app(DeepCopy::class);

$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('uuid'));
$copier->addFilter(new KeepFilter(), new PropertyMatcher(Order::class, 'tenant_id'));
$copier->addFilter(
    new ReplaceFilter(fn ($v) => $v . ' (copy)'),
    new PropertyMatcher(Order::class, 'reference')
);

$copy = $copier->copy($original);
$copy->exists = false;
$copy->save();

SetNullFilter on id cascades down the whole graph, so child rows come out clean too.

3. Snapshot / draft workflows

Copy a "template" or "published version" aggregate into an editable draft, mutate, compare or persist later:

$draft = $copier->copy($published);
$draft->status = Draft::class;

Because the object graph (not just the top object) is preserved, references between draft children still point at draft siblings — something manual clone + __clone() almost always gets wrong.

4. Keep the "infrastructure" out of the copy

Models and entities often hold references to things you never want to duplicate: connections, loggers, mocks, closures, big cached collections.

use DeepCopy\TypeFilter\ShallowCopyFilter;
use DeepCopy\TypeMatcher\TypeMatcher;

$copier->addTypeFilter(new ShallowCopyFilter(), new TypeMatcher(\Psr\Log\LoggerInterface::class));

ShallowCopyFilter falls back to plain clone for the matched type and stops recursion there — the cheap escape hatch for anything you don't control.

5. Test suites

Copying fixtures between tests is where DeepCopy quietly shines, and it plays well with Mockery:

$copier->addTypeFilter(new ShallowCopyFilter(), new TypeMatcher(Mockery\MockInterface::class));

Mocks get cloned rather than reflected into, which keeps expectations intact.

6. Compose filters instead of stacking them

Matching a filter stops the chain. When you genuinely need two transformations on the same node (e.g. "load the proxy" then "null the id"), decorate with ChainableFilter:

$copier->addFilter(
    new ChainableFilter(new DoctrineProxyFilter()),
    new DoctrineProxyMatcher()
);

And when ordering matters, use prependTypeFilter() (added in 1.13) rather than relying on registration order.

7. Custom filters / matchers

When the built-ins don't cover a domain rule (e.g. re-key a slug only on a specific VO), implement the tiny interfaces:

final class SlugSuffixFilter implements \DeepCopy\Filter\Filter
{
    public function apply($object, $property, $objectCopier)
    {
        $object->{$property} = $object->{$property} . '-' . Str::random(6);
    }
}

$copier->addFilter(new SlugSuffixFilter(), new PropertyMatcher(Post::class, 'slug'));

The $objectCopier callable lets you deep-copy a sub-value yourself if the filter needs to recurse. A custom Matcher that logs (class, property) pairs is also the best debugging tool you have — see below.

Gotchas and Tips

Filter chains short-circuit. The first matching filter wins; later ones never see the node. A broad PropertyNameMatcher('id') will shadow a narrower PropertyMatcher(SomeClass::class, 'id') registered after it. Register specific before generic, or prependTypeFilter().

Matchers and type matchers live in different namespaces and do different things. DeepCopy\Matcher\PropertyTypeMatcher (used with addFilter) matches a property's declared type; DeepCopy\TypeMatcher\TypeMatcher (used with addTypeFilter) matches any element in the graph, including scalars and array entries. Mixing them up produces silent no-ops rather than errors.

new DeepCopy(true) means "honour __clone()". With that flag, objects defining their own __clone() are cloned through it — nested objects stay shared and your filters don't run on those properties. Default is false (pure reflection). Don't flip it on casually for entity graphs.

Readonly properties are skipped (change landed in 1.12.0). If your value objects use readonly, verify the copy: an uninitialised readonly property throws the moment you read it. Test one DTO before rolling this out across a codebase.

Internal PHP classes can't be reflected into. DateTime, DatePeriod, SplObjectStorage, ArrayObject, PDO, Closure etc. are shallow-cloned and filters are not applied to them. Consequences:

  • Deep-copying an object graph containing a Closure blows up — PHP refuses to clone closures. Filter them out or keep them on a KeepFilter-ed property.
  • DatePeriod had a dedicated fix (DatePeriodFilter) in 1.12.1; other internals may need your own TypeFilter.

Eloquent specifics. replicate() is still the right tool for the model's own attributes; DeepCopy is for the graph. After copying, reset exists, wasRecentlyCreated, and anything in $original/$changes you care about. Avoid deep-copying models with huge eager-loaded relations you don't need — unsetRelation() first. Custom casts or attributes holding service objects are the usual source of surprise recursion.

Long-running processes. Before 1.14 the internal source→copy map was a strong reference (spl_object_hash + SplObjectStorage), so a reused DeepCopy instance accumulated every object it had ever seen — a leak in queue workers and Octane. 1.14 switched to WeakMap + spl_object_id. Upgrade to 1.14+ if you keep a singleton copier, and prefer a per-job instance if you're on 1.13.

There is no Laravel integration. No service provider, no facade, no config/deep_copy.php, no auto-discovery. Everything above is hand-wired — which also means zero surprises from a package trying to be clever.

Performance. It's reflection-driven and fully recursive: cost scales with total nodes, not top-level objects. Copying a 5,000-item collection will be noticeably slower than a SQL INSERT ... SELECT. Measure, and use ShallowCopyFilter to prune large subtrees you don't actually need to duplicate.

Doctrine users (if you have it in the stack). Lazy proxies produce __CG__\Proxy errors and missing-field exceptions. Register new ChainableFilter(new DoctrineProxyFilter()) with DoctrineProxyMatcher first, and add DoctrineCollectionFilter for Collection properties. Emptying a collection is DoctrineEmptyCollectionFilter + PropertyMatcher. None of this helps with Eloquent — different ORM, different problems.

Debugging playbook:

// 1. Which nodes does my filter actually hit?
$copier->addTypeFilter(new class implements \DeepCopy\TypeFilter\TypeFilter {
    public function apply($element) {
        logger()->debug('deep-copy visiting', ['type' => get_debug_type($element)]);
        return $element; // no-op
    }
}, new TypeMatcher('object')); // or write a logging Matcher

Then: catch ReflectionException and log get_class() from the stack trace to find the unreflectable class; and diff get_object_vars() (or json_encode) between source and copy for a small fixture to confirm what was and wasn't cloned.

Extend via filters, not subclassing. The internals were reworked in 1.14 (WeakMap, spl_object_id, generic copy() signature) — overriding protected methods is a maintenance trap. The Filter / TypeFilter / Matcher interfaces are the stable extension points.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo