myclabs/deep-copy
DeepCopy creates safe deep clones of PHP objects, recursively copying referenced objects while preserving the original object graph. It handles cyclic references to avoid infinite loops, and supports customization via matchers and filters for specific properties and types.
Install the package via Composer:
composer require myclabs/deep-copy
The simplest use case is copying an object with full recursion and cycle handling:
use DeepCopy\DeepCopy;
$copier = new DeepCopy();
$copy = $copier->copy($originalObject);
Or use the convenient helper function:
use function DeepCopy\deep_copy;
$copy = deep_copy($originalObject);
Start with this if you're copying entities or complex data structures where clone falls short due to reference cycles or shallow copies of nested objects.
Idempotent utility function: Wrap DeepCopy in a project-level helper to enforce consistent behavior:
function dcopy($value) {
static $copier;
$copier = $copier ?? new DeepCopy(true);
return $copier->copy($value);
}
Using true enables safe clone (avoids cloning objects that already exist in the graph).
Custom copy behavior per property: Combine matchers and filters to control cloning granularity:
$copier = new DeepCopy();
$copier->addFilter(
new SetNullFilter(),
new PropertyNameMatcher('id')
);
$copier->addFilter(
new KeepFilter(),
new PropertyMatcher(Order::class, 'customer')
);
Doctrine integration: For entities with Collection properties, always add DoctrineCollectionFilter:
$copier->addFilter(
new DoctrineCollectionFilter(),
new TypeMatcher(\Doctrine\Common\Collections\Collection::class)
);
Proxy-aware copying: For lazy-loaded Doctrine entities, apply DoctrineProxyFilter early in the chain (ideally first), decorated with ChainableFilter:
$copier->addFilter(
new ChainableFilter(new DoctrineProxyFilter()),
new DoctrineProxyMatcher()
);
Testing with mocks: Use ShallowCopyFilter for mocks to prevent deep cloning overhead and incompatibility:
$copier->addTypeFilter(
new ShallowCopyFilter(),
new TypeMatcher(\Mockery\MockInterface::class)
);
Value transformation: Use ReplaceFilter to derive new values instead of copying:
$copier->addFilter(
new ReplaceFilter(fn($val) => 'COPY_' . $val),
new PropertyMatcher(Document::class, 'title')
);
Type-level replacement: Replace entire objects by type (e.g., convert domain models to DTOs):
$copier->addTypeFilter(
new ReplaceFilter(fn(MyEntity $e) => new MyDto($e)),
new TypeMatcher(MyEntity::class)
);
Readonly properties in PHP 8.1+: Starting from v1.12, readonly properties are skipped by default (not cloned). Override with a ReplaceFilter if needed.
Reflection limitations: Some internal structures (e.g., Closure, Generator, SimpleXMLReader, DateTime, DatePeriod) cannot be cloned via reflection. DatePeriod is handled in v1.12.1+ with DatePeriodFilter, but others require custom __clone() or a TypeFilter.
Matcher/filter precedence: Filters are applied in chain order and stop after the first match—unless using ChainableFilter. Ensure filters like DoctrineProxyFilter run before others by registering them early.
Don’t filter by value directly: Type matchers work on type, not specific values. For conditional logic (e.g., only if status === 'draft'), use ReplaceFilter’s callback.
Performance: Deep copying large graphs is expensive. Profile before deploying to production, especially when copying large trees or Doctrine-managed entities.
Type safety: Since v1.13.2, DeepCopy::copy() is generic (copy(mixed $value): mixed), enabling static analysis support—leverage IDE hints and Psalm/PHPStan.
Don’t assume cycle-breaking behavior: DeepCopy preserves graph structure but does not reset references to avoid infinite loops—it ensures each object is cloned once and reused.
Version tip: Recent releases (v1.13+) improve PHP 8.4+ and Doctrine compatibility. Check CHANGELOG when upgrading, especially around proxy handling and readonly support.
How can I help you explore Laravel packages today?