tanvirofficials/copy-clone
A Laravel package for copying or cloning Eloquent models and their relationships. It helps duplicate records quickly with configurable options, making it useful for templates, drafts, repeated data entry, and admin workflows.
Install and register
composer require tanvirofficials/copy-clone
This is a Nova package, so the first place to look is app/Nova/NovaServiceProvider.php — check whether the package ships an auto-discovered service provider, and whether it registers a global action or expects you to add an action class per-resource. Read the repo README before writing any code; at 7 stars and a single 2021-era release, the docs are the only real spec you have.
Where to look first
php artisan vendor:publish --tag=...).vendor/tanvirofficials/copy-clone/src/ — confirm the actual action class name, whether it clones relations, and whether it uses a trait or a handle()-based Nova Action.First use case
The natural fit is admin-side "duplicate this record" on a detail or index page — e.g. cloning a Product so an ops user can spin up a variant without re-entering 30 fields, or cloning a CMS Page with its content blocks. Wire it up on one low-risk resource first, clone a record in staging, and inspect the resulting rows before rolling it out.
Start with a resource that has no unique indexes beyond the primary key and no side-effecting observers. That gives you a clean read on what the package actually copies.
Attach the action to a resource
Nova actions are registered per-resource in actions(Request $request). Add the package's action alongside your own:
public function actions(Request $request)
{
return [
(new CloneRecord)->onlyOnDetail(),
];
}
Use onlyOnDetail() for cloning (index/bulk cloning is a footgun — see Gotchas). If you want it everywhere, register it as a global action in NovaServiceProvider::actions() instead, but prefer per-resource so you can tune it.
Normalize the clone in a subclass
Don't call the package action directly from ten resources. Wrap it once so behavior is consistent:
class CloneProduct extends CloneRecord
{
public function handle(ActionFields $fields, Collection $models)
{
foreach ($models as $model) {
DB::transaction(function () use ($model) {
$copy = parent::handle(...); // or replicate manually
$copy->forceFill([
'slug' => $model->slug . '-copy-' . Str::random(4),
'status' => 'draft',
])->save();
});
}
}
}
If the package doesn't expose a clean extension seam, fall back to $model->replicate() plus explicit relation copying — you keep full control and lose nothing but the button.
Reset fields that must not be inherited
The day-to-day pattern is: clone everything, then scrub. Typical candidates are slugs, SKUs, external IDs, published timestamps, is_default flags, and any "current version" pointer. Centralize this in a cloneExcludedAttributes() method on the model or a small config map so it isn't re-derived per resource.
Handle relations explicitly
If the package recurses into relations, confirm which ones (hasMany? belongsToMany with pivot data? morphMany?). If it doesn't, do it yourself and always inside DB::transaction:
$copy = $original->replicate();
$copy->push();
foreach ($original->variants as $variant) {
$copy->variants()->create($variant->only($variant->getFillable()));
}
Gate it
Cloning bypasses the normal create flow, so audit trails and "who created this" logic can be wrong. Restrict with ->canSee(fn () => $request->user()->isAdmin()) and make sure the clone records the acting user, not the original creator.
Queue the heavy ones
For resources with large child graphs (a template with 200 blocks), make the action ->queued() or dispatch your own job. Cloning in-request is fine for a handful of rows and painful beyond that.
composer.json, read the source before trusting it, and budget for a fork or a hand-rolled replicate() if it breaks. Do not adopt this in a codebase where you can't vendor the logic yourself.sku, slug, email, or composite unique index throws a QueryException mid-transaction. Decide on a suffixing/blanking convention up front and apply it in one place.replicate() and create() both trigger creating/created/saved, so observers that send mail, push to search indexes, or write audit rows will run for a clone. Use withoutEvents() if the clone shouldn't look like a real user action, or knowingly accept the side effects.belongsToMany relations often get the relation rows but lose extra pivot columns (ordering, flags, timestamps on the pivot). Check ->withPivot() columns survive.deleted_at = null and get fresh created_at. If the package copies raw attribute arrays, it may carry these over — override them.onlyOnDetail() until you've proven the action is safe.DB::listen() or enable query logging and inspect what SQL the action emits; run it against a RefreshDatabase test that asserts row counts on parent and each child relation. That test is your regression guard when a Nova upgrade lands.handle() you can override, or an event you can hook. If none exist, subclassing the action and calling parent::handle() is usually still viable — but confirm the method signature matches the signature of the method that's actually shipped.How can I help you explore Laravel packages today?