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

Cloner Laravel Package

bkwld/cloner

A Laravel Eloquent trait that clones models with their relationships, including files, and can duplicate records to another database connection. Configure cloneable relations and call duplicate() or duplicateTo().

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes):

composer require bkwld/cloner

Laravel 5.5+ auto-discovers Bkwld\Cloner\ServiceProvider; on older versions add it to config/app.php. Then opt a model in:

use Bkwld\Cloner\Cloneable;

class Article extends Model
{
    use Cloneable;
}

First use case — "Duplicate this record" in an admin panel:

$article = Article::findOrFail($id);
$copy = $article->duplicate();   // already persisted, new PK, new timestamps

return redirect()->route('articles.edit', $copy);

That single line is the whole value proposition: title, body, and any relations you declare get copied, and you land on an edit screen with a pre-filled draft instead of a blank form.

Where to look first, in this order:

  1. vendor/bkwld/cloner/src/Cloneable.php — read duplicate(), duplicateTo(), and the relation walker before you build anything on top. It's a small file and answers every "does it copy X?" question faster than the README.
  2. Your model's $cloneable property. If you don't declare it, you get only the parent row — the most common "it doesn't work" report is a missing $cloneable array.
  3. Your config/database.php connections, if the task mentions "copy this to staging" or multi-tenant duplication.

Implementation Patterns

Declare the relation graph on the model. Only relations listed here are cloned; they must also use the trait:

class Article extends Model
{
    use Cloneable;

    protected $cloneable = ['blocks', 'tags', 'seoMeta'];

    public function blocks()   { return $this->hasMany(Block::class)->orderBy('position'); }
    public function tags()     { return $this->belongsToMany(Tag::class); }
    public function seoMeta()  { return $this->hasOne(SeoMeta::class); }
}

Cloning is recursive: Block declaring its own $cloneable produces deep copies. Pivot rows for belongsToMany are re-created against the new parent.

Override at call time when a UI lets the user pick depth:

$copy = $article->duplicate(['blocks']);   // shallow-ish clone for a "quick template"

Content templating / seeding pattern. Keep a hidden "template" record per content type and stamp it out per tenant, locale, or campaign:

$template = Page::where('is_template', true)->firstOrFail();
foreach ($locales as $locale) {
    $page = $template->duplicate();
    $page->update(['locale' => $locale, 'slug' => "{$template->slug}-{$locale}", 'is_template' => false]);
}

This is a much better fit for Cloner than hand-written replicate() loops — the relation and file handling is what you'd otherwise be reimplementing badly.

Cross-database cloning — for staging refreshes, tenant provisioning, or moving a demo record into production:

$copy = $article->duplicateTo('tenant_42');

The clone and all cloned relations get setConnection('tenant_42'). Requirements: the connection is defined in config/database.php, the schema matches, and the DB user has write access. Files land on the same filesystem/disk, so if storage is also per-tenant you need to move them yourself after the clone.

Wrap multi-record work in a transaction. Because writes are eager and recursive, a failure halfway leaves orphans:

$copy = DB::transaction(fn () => $article->duplicate());

For anything with dozens of children, push the clone into a queued job and return immediately — admin POSTs shouldn't block on a recursive copy.

Controller/authorization pattern. Clone is a write to a new record, so check create permission, not just view:

$this->authorize('create', Article::class);
$this->authorize('view', $article);

$copy = DB::transaction(fn () => $article->duplicate());
$copy->update(['title' => $copy->title . ' (copy)']);

return redirect()->route('articles.edit', $copy)
    ->with('status', 'Duplicated. Review before publishing.');

Never trust $cloneable from a user-supplied depth — pass a fixed whitelist.

Files. Where the model's file columns are recognised, Cloner writes new files rather than sharing paths, so deleting the copy doesn't nuke the original. Verify in a dev environment with your actual disk config before assuming it holds for your custom uploader — anything exotic (S3 prefixes, image conversions, generated derivatives) usually needs a manual pass in an onCloned-style hook or a post-clone job.


Gotchas and Tips

duplicate() saves immediately. Consequence: every creating/created/saving observer fires for the parent and every child. Email notifications, Scout indexing, webhook dispatchers, audit logs, and Str::slug() boot hooks will all run. Usually you want Model::withoutEvents() around bulk duplication — but then you also lose legitimately useful hooks like slug generation, so decide per case.

Unique constraints are the number-one failure mode. slug, sku, email, and compound uniques will throw a QueryException before you get a chance to fix them, because there's no "return me an unsaved model" mode. Use the trait's pre-save lifecycle hook (read Cloneable.php for the exact method name in your version — it's the callback invoked before the insert) to mutate the offending attributes. If you can't find one, the pragmatic options are a nullable/defaulted unique column or a two-step clone into a scratch record you immediately re-slug.

Attributes not in the row aren't copied. Accessors, $appends, computed properties, and anything derived at read time are not in getAttributes() and therefore don't come along. This also means $fillable/$guarded are irrelevant here — raw attributes are transferred directly, including columns you'd normally guard against mass assignment.

deleted_at and position columns. Soft-deleted children generally should not be resurrected; if your $cloneable relation queries include trashed rows, the copy inherits them. Likewise position/sort/order columns copy verbatim and collide with the original's ordering — re-sequence child rows after cloning.

Cycles and fan-out. A self-referential parent_id in $cloneable will recurse until it blows up. Keep the trait off such relations, or guard with a depth limit in an overridden duplicate().

Eager-loaded state is thrown away. Cloner re-queries relations; $article->load('blocks') before duplicating buys you nothing.

Media libraries are not covered. If you use Spatie Media Library, Laravel Mediable, or similar, files attached through those packages are not part of the model's raw attributes and will not be copied. Plan an explicit media-copy step; this is the gap people hit right after the demo works.

Debugging checklist when a child is missing:

  1. Is the relation name in $cloneable (spelled exactly as the method)?
  2. Does the child model use Cloneable?
  3. Is it a relation type the package walks (hasMany, hasOne, belongsToMany, morphMany)? belongsTo deliberately doesn't clone — duplicating a parent is rarely what you mean.
  4. Add DB::listen() or Model::withoutEvents() to see whether the insert is even attempted.

Extension point. Rather than sprinkling logic in controllers, put a HasClones trait on your base model that defines duplicate() as: sanitize unique fields → parent::duplicate() → normalize (strip published_at, reset flags, suffix the title) → dispatch your own ModelCloned event. That gives tests and jobs a single seam.

Testing. Use RefreshDatabase and assert both directions: counts +1 for each expected table, and unchanged counts for tables you deliberately excluded. duplicateTo() is easiest to assert against a second configured connection in phpunit.xml.

Version pinning. The package is stable and still releasing, but the README on the default branch sometimes lags the trait source. When in doubt, the source wins — pin a minor version in composer.json and re-read Cloneable.php after any upgrade, since lifecycle hook names are the thing most likely to shift.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums