chriskonnertz/deeply
Deeply is a PHP library for deep cloning objects and arrays. It recursively copies nested objects so duplicates share no references with the original, with a simple API and support for magic methods like __clone and __wakeup. Useful for snapshots, caching, and safe duplication without side effects.
Install the package with Composer:
composer require chriskonnertz/deeply
First place to look is the README’s copy entry point and the package source for how it treats arrays, objects, private/protected properties, and circular references. In Laravel, treat Deeply as an in-memory value-object utility. It is not a replacement for Eloquent’s replicate(), and it should usually be applied to DTOs, plain objects, and arrays rather than framework internals.
The first practical use case is a draft/preview service: copy an object graph before applying user changes so the persisted/original graph is not mutated.
Task 1.1 — Minimal deep copy
Given a Quote object with a nested Customer object and an array of QuoteLine objects, implement a preview method that deep-copies the quote, applies a change to the copy, and leaves the original untouched.
use ChrisKonnertz\Deeply\Deeply;
final class QuoteDraftService
{
public function preview(Quote $quote, array $changes): Quote
{
$draft = Deeply::copy($quote);
// Apply changes to $draft, e.g. $draft->customer->name = $changes['customer_name'];
return $draft;
}
}
Expected evidence:
$draft !== $quote$draft->customer !== $quote->customerIf the installed version exposes an instance API instead of a static one, accept the equivalent documented entry point, e.g. (new Deeply())->copy($quote), provided the candidate is consistent.
Pattern 1 — Copy at the service boundary before mutation
Use Deeply at the point where an in-memory graph is about to be modified but must not affect the caller’s graph.
public function applyDiscount(Quote $quote, float $percent): Quote
{
$updated = Deeply::copy($quote);
foreach ($updated->lines as $line) {
$line->discount = $line->subtotal * $percent;
}
return $updated;
}
This is useful for previews, draft states, simulations, and “what-if” calculations. Keep the copy boundary explicit in a service/action class rather than scattering deep copies through controllers.
Pattern 2 — Snapshot and undo/redo stacks
Deep copies make simple in-memory undo stacks possible when working with plain DTOs or value objects.
final class DraftHistory
{
/** @var Draft[] */
private array $snapshots = [];
public function remember(Draft $draft): void
{
$this->snapshots[] = Deeply::copy($draft);
}
public function restore(int $index): Draft
{
return Deeply::copy($this->snapshots[$index]);
}
}
This pattern is appropriate for Livewire components, admin preview screens, and editorial tools where the state graph is small and plain. Do not use it as a substitute for database transactions or persisted versioning.
Pattern 3 — Test isolation explicitly
When testing deep-copy behaviour, assert both top-level and nested object identity.
it('does not mutate the original quote when previewing', function () {
$quote = Quote::factory()->make([
'customer' => Customer::factory()->make(['name' => 'Original']),
]);
$draft = (new QuoteDraftService())->preview($quote, [
'customer_name' => 'Changed',
]);
expect($draft)->not->toBe($quote)
->and($draft->customer)->not->toBe($quote->customer)
->and($quote->customer->name)->toBe('Original');
});
Use not->toBe() for object identity and value assertions for the original graph. This catches shallow-copy mistakes that ordinary value assertions miss.
Pattern 4 — Laravel integration and Eloquent boundaries
For plain DTOs, bind a copy service or use Deeply directly in an action/service class. If the package is instance-based in your version, register it as a singleton:
$this->app->singleton(Deeply::class);
For Eloquent models, prefer Laravel’s own primitives:
replicate() to duplicate a model’s attributes.Task 2.1 — Duplicate action
Implement a DuplicateOrderAction that takes an OrderData DTO containing nested OrderLineData objects and returns a new DTO with copied lines. Explain why clone $orderData is insufficient and why deep-copying a full Eloquent Order model is risky.
Expected discussion:
clone only clones the top-level object; nested objects remain shared.replicate() or DTO mapping for model duplication.replicate() plus explicit relation handling, or copy a plain DTO.Request, Container, UploadedFile, jobs, or objects holding closures/resources/PDO handles. Copy only the validated/plain data you actually need.__clone() still runs. If a class defines __clone(), that method executes during the deep copy. Check for side effects and readonly-property constraints.=== and spl_object_id(). Value assertions can pass even when references are shared. Use object identity checks to verify nested isolation.dump($draft === $original); // false
dump($draft->customer === $original->customer); // should be false
dump(spl_object_id($draft->customer), spl_object_id($original->customer));
__clone() on that class to replace or reset the collaborator, or keep non-cloneable services outside the deep-copied graph.Task 3.1 — Diagnose the bug
A developer writes this in a controller:
$draft = Deeply::copy($request);
$draft->merge(['status' => 'draft']);
What is wrong with this use of Deeply, and what should they do instead?
Expected answer:
Request contains framework internals, input bags, files, and potentially closures/resources; it is not a safe deep-copy target.$draft = Deeply::copy($validatedData) or map validated input into an application DTO before copying/mutating it.How can I help you explore Laravel packages today?