Duplicating an Eloquent model — without its primary key, without timestamps, without accidentally overwriting the original record — is a small problem with a few subtly wrong solutions before you arrive at the right one.
The clone keyword
PHP has a built-in clone keyword that produces a shallow copy of an object. Scalar properties are copied by value; object properties are copied by reference. For a database model, the defaults are almost never right: the copy shares the same id, its exists flag is still true, and any object-type properties point to the same instances as the original.
The __clone() magic method fires on the new copy immediately after the shallow copy is made — $this inside it refers to the clone, not the source. The correct pattern:
public function __clone()
{
unset($this->id);
$this->exists = false;
}
Used as:
$copy = clone $model;
One thing to be clear about: __clone() does not use its return value. PHP ignores it. The method modifies $this (the clone) in-place. A version that creates a $clone = new self(...) inside __clone() and returns it is doing work that goes nowhere.
Putting this on a base model class that your application models extend is the right place — consistent behaviour without touching individual models.
shallowClone — explicit attribute exclusion
A named method on your base model is more legible at the call site and lets you control which attributes are excluded:
public function shallowClone(array $except = []): static
{
$attributes = $this->getAttributes();
foreach (array_merge(['id', 'created_at', 'updated_at'], $except) as $key) {
unset($attributes[$key]);
}
$clone = new static();
$clone->setRawAttributes($attributes);
$clone->exists = false;
return $clone;
}
getAttributes() returns raw stored values — no appended accessors, no relations. setRawAttributes() bypasses mutators, which is what you want here: data coming out of the database is already in its stored form, and re-running a setNameAttribute on it would transform it a second time.
If your model has mutators you do want to run on the copy — say, you’re intentionally normalising — use fill() instead and accept that mutation.
If you’re on Laravel 5+, replicate() covers this case without the boilerplate:
$copy = $model->replicate(['id', 'created_at', 'updated_at']);
// $copy->exists === false, $copy->id === null
deepClone — carrying over loaded relations
Both replicate() and the shallow approach above copy attributes only. Loaded relationships on the model are dropped. If you want to carry them over:
public function deepClone(array $except = []): static
{
$clone = $this->shallowClone($except);
foreach ($this->relations as $name => $related) {
if ($related instanceof Model) {
$clone->setRelation($name, $related->shallowClone());
} elseif ($related instanceof Collection) {
$clone->setRelation($name, $related->map(fn($m) => $m->shallowClone()));
} else {
// null or a pivot — carry over as-is
$clone->setRelation($name, $related);
}
}
return $clone;
}
Three things worth being explicit about here:
This only clones relations that are already loaded. It will not lazy-load. If you need a relation included in the deep clone, call load() on the source model before cloning.
Nothing is persisted. The clone and its related models have no IDs and exists = false. Saving the parent alone will not cascade to the relations. You need to save each cloned related model separately, set foreign keys, and associate them with the parent after persist.
Pivot data is not handled. A BelongsToMany with extra pivot columns requires separate handling after the relations are saved. setRelation on the clone puts the models in memory but doesn’t reconstruct the pivot rows.
When not to use model cloning
Cloning is the right tool when you’re implementing an explicit “duplicate this record” feature — a user copying a template, order, or document.
It’s the wrong tool when:
- You want versioning or audit history. A separate versions table with a polymorphic relation is a more honest data model and doesn’t conflate a copy with a fork.
- You want a record initialised from another as a default. Just construct a new model and set what you need. Cloning a source and then overriding half the attributes is harder to follow than an explicit builder.
- The model has complex lifecycle observers. The
creating,saving, andcreatedevents fire on persist. If those observers assume a new record is genuinely new — generating a slug, firing a notification, initialising a workflow state — cloning can trigger them in states they weren’t written for. Know your observers before you clone into them.
The attribute copy is the trivial part. The state machine around persistence — exists, wasRecentlyCreated, dirty tracking, relation graphs — is where cloning quietly earns its complexity.