jackabox/nova-duplicate-field
A Laravel Nova field that adds a one-click duplicate action to your resources, letting admins copy records and relationships from the index or detail view. Simple to install and customize for faster CRUD workflows.
This package adds a replication layer on top of Laravel models so you can duplicate a record (and optionally its relations) without hand-writing replicate() + fill() boilerplate. v0.3.0 adds per-field control — you can now ignore fields (never copied) or override them (copied but replaced with a computed value) during replication.
Minimal setup:
composer require <vendor>/<package>
php artisan vendor:publish --tag=replication-config
Then replicate your first record:
use Vendor\Package\Facades\Replicator;
$copy = Replicator::make($product)
->ignore(['sku', 'slug'])
->override(['status' => 'draft', 'created_by' => auth()->id()])
->save();
Where to look first, in order:
config/replication.php — global ignore/override defaults and the model resolver. This is where 90% of "why was this field copied?" questions get answered.Replicator facade / service contract — the fluent API (make, ignore, override, withRelations, save).Replicates trait or replicationConfig() method you can add to models — per-model defaults live here, not in config.First real use case: "duplicate this product/listing/page as a draft". You want a new row with a fresh ID, a handful of identity fields (sku) stripped, and ownership/status reset — exactly the ignore/override split.
Ignore vs. override — pick deliberately. ignore() removes a field entirely (the DB default or null applies). override() keeps the column in the payload but swaps the value. For anything with a NOT NULL + no-default column, use override, not ignore, or the insert fails.
Identity fields belong in ignore, ownership in override:
$copy = Replicator::make($page)
->ignore(['slug', 'uuid', 'published_at']) // regenerated downstream
->override(['status' => 'draft', 'author_id' => auth()->id()])
->save();
$copy->update(['slug' => Str::slug($copy->title).'-'.Str::random(6)]);
Per-model defaults via a trait/config method so controllers stay thin:
class Product extends Model
{
use Replicates;
public function replicationConfig(): array
{
return [
'ignore' => ['sku', 'slug'],
'override' => ['status' => 'draft'],
];
}
}
Call-site overrides should merge on top of (not replace) model defaults — verify this in your version, since it's the most common source of surprise.
Controller + form request pattern. Validate the user-supplied overrides only, then hand them to the replicator. Don't let request input reach the override map unfiltered — that's a mass-assignment hole with a different name.
Bulk replication. Wrap in a job and chunk; the fluent builder is per-record. For "duplicate this template into 50 tenants", loop the builder inside DB::transaction() per record rather than one giant transaction.
Relations. If the package supports withRelations(), replicate children after the parent saves so you can point foreign_id at the new ID via override. Do not assume children()->replicate() cascades — it doesn't in most implementations.
Testing. Assert the negative space: assertNull($copy->sku), assertNotEquals($original->uuid, $copy->uuid), assertSame('draft', $copy->status). Snapshot-style assertions on the whole attribute array break on every schema change.
ignore() on a column with no DB default throws on insert. Prefer override when in doubt; check SHOW CREATE TABLE before trusting an ignore list.created_at/updated_at are excluded by default. If you ignore deleted_at but copy deleted_at's value implicitly, a soft-deleted original can produce a soft-deleted copy — usually not what you want.creating/created/saved hooks (audit logs, search indexing, slug generation) run against the new record. If the package's own save bypasses Eloquent, your observers won't fire — test both directions and pick one behaviour intentionally.array/json casts round-trip cleanly, but Carbon instances are shared objects unless cloned — mutate the copy's dates carefully.$fillable/$guarded still apply if the package uses fill(). An override for a non-fillable column silently disappears. Use forceFill semantics or add the field to $fillable.php artisan config:cache in deploy means edits to config/replication.php won't take effect until re-cached. Classic "works locally, not in prod" for this class of package.save(), dump the resolved ignore/override arrays and the final attribute set. When a field "mysteriously" copies, it's almost always precedence: config default < model replicationConfig() < call-site override. Know which layer won.replicationConfig(), then call-site ignore/override, then a custom field resolver or macro if the package exposes one. Avoid forking the replicator for one quirky model.How can I help you explore Laravel packages today?