gabrielesbaiz/duplicate-toolkit
Laravel package for duplicating Eloquent models and their relationships. Clone records with customizable column overrides, relationship handling, and events via a simple API, making it easy to create reusable copies of templates, content, or configuration records.
Architecture fit. gabrielesbaiz/duplicate-toolkit addresses a genuinely recurring gap in Laravel applications: cloning an Eloquent model together with its relation graph without hand-writing per-model copy logic. Architecturally it is a thin, opt-in concern — a Concerns\HasDuplicates trait on models plus a fluent, immutable DuplicateOptions object — which is the right shape for a cross-cutting capability. It does not impose a repository pattern, a persistence abstraction, or a container-level service, so it composes with existing Laravel idioms (events, queues, transactions, Eloquent lifecycle). The 2.0 rewrite is a real architectural reset rather than a patch on a fork: a proper service provider and auto-discovery, reflection-based relation discovery that no longer invokes public methods (avoiding accessor side effects and opcache problems), and a design that separates relation traversal from specialized passes (e.g., media-library rows are owned by a dedicated pass rather than the generic walk). That separation matters to a TPM because it means extending coverage for a bespoke relation type is a bounded change, not a rewrite of the traversal engine.
Integration feasibility. The hard gate is PHP 8.3+ and Laravel 12 or 13. For a team already on that platform, integration is genuinely cheap: install, add a trait to the relevant models, optionally define a duplicateOptions() method, and call duplicate(). Three per-relation strategies (copy, reference, skip) are settable at the model level, at the call site, or via dot notation several levels down, which is exactly the ergonomic surface needed to encode product rules like "copy the invoice, reference the customer, skip audit notes." Attribute overrides (suffix, prefix, replace, withAttributes, mutate) are applied before insert, so renaming a copy is one write instead of two and does not re-fire events that quietly() suppressed. The relations, duplicate, make-options, and upgrade Artisan commands plus preview()/dry-run materially reduce the cost of evaluation — a spike against two or three real models is hours of work, not days. The morphToMany fatal, the cross-model relation cache leak, the media-row mis-clone, and the unsaved-replica unique-column loop were all real 1.x defects that are explicitly fixed, which is the class of bug that would otherwise have surfaced in production after a "cheap" adoption.
Technical risk. The dominant risk is not code quality but maturity and change velocity. Zero stars, zero dependents, single maintainer, and a 2.0 that shares no code with 1.x means there is effectively no production track record for the current line. The engineering hygiene signals are strong — PHPStan at max with no baseline and no ignores, Pint, Rector, 105 tests against a real Testbench application covering every relation type plus soft deletes, UUID keys, and cycles, and CI across PHP 8.3/8.4 × Laravel 12/13 × prefer-lowest/prefer-stable — but hygiene is a leading indicator, not a substitute for operational history. Secondary risks: the 2.0 default of max_depth = 1 is deliberately conservative and will surprise teams expecting recursion (mitigated by preview() and strictDepth()); DuplicateOptions is immutable and fully typed with __get() removed, so any 1.x code doing dynamic property access breaks; and the package's own upgrade notes flag that 1.x results are suspect wherever a single request duplicated two different model classes, which converts an "upgrade" into a data-verification exercise. Finally, duplication touches writes: correctness bugs here are silent data-shape defects, not crashes, which raises the cost of every gap relative to a read-path library.
Key questions. (1) Which of our models have relation graphs that a single spike can validate, and which depend on custom pivot or versioning behavior the package does not model? (2) Is our stack actually on PHP 8.3+ and Laravel 12/13 today, or does adoption silently couple this feature to a platform upgrade initiative? (3) Do we already ship 1.x duplication anywhere, and if so, which requests duplicated two different model classes — and who owns verifying those copies? (4) What does "correct" mean per model — ownership reassignment, status reset, pricing recalculation, audit suppression — and can each rule be expressed as an override or strategy, or does it need in-house code? (5) Are we willing to pin the version and hide the API behind an internal DuplicationService, given 2.0's renames prove the internals will move again? (6) For large graphs, do we need the queued/batchable job path and the four events from day one, or is request-blocking acceptable for the first release?
Stack fit. Best fit is a Laravel 12/13 application on PHP 8.3+ with Eloquent models that already follow conventional relation definitions and where duplication is a repeated but non-core need — admin "duplicate" actions, save-as-template, tenant or onboarding configuration cloning, demo/sandbox data generation, campaign or product variants, CMS page copies. Poor fit is any stack below those versions (a blocker, not a caveat), or a product where duplication carries regulated or high-volume semantics.
Migration path. Treat 2.0 as a new dependency, not a version bump. For greenfield adoption: install, pin the version, add the trait to one low-risk model, write a duplicateOptions() for it, and verify against preview() before enabling any UI. For existing 1.x users the sequence is stricter: read UPGRADE.md first; note that the app keeps booting after the update, which is a trap rather than a convenience; run php artisan duplicate-toolkit:upgrade to rewrite most call sites; then manually handle the two flagged cases — in particular, audit any request path that duplicated two different model classes under 1.x, because the unreset static relation cache leaked relations between classes and made those copies incorrect. Apply the renames explicitly rather than relying on deprecated aliases (Traits\HasDuplicates → Concerns\HasDuplicates, Options\DuplicateOptions → DuplicateOptions, saveAsDuplicate() → duplicate(), getDuplicateOptions() → duplicateOptions(), disableDeepDuplication() → shallow(), saveQuietly() → quietly()), because deprecated shims are the first thing to disappear in 3.x and are exactly how a "safe" upgrade becomes a future breaking change. Note also that DuplicateOptions is now immutable and typed with __get() gone; any code reading options dynamically needs rewriting, not aliasing.
Compatibility and sequencing. Sequence the work so the risky parts are validated before they are user-facing. First, spike: use the relations command to draw the relation tree for two or three representative models, run duplicate interactively, and use preview()/dry run to confirm row counts and rollback behavior. Second, wrap: put a thin internal DuplicationService or interface in front of the package so product code never calls the package API directly — this is the single highest-leverage decision available, and 2.0's full rewrite is the evidence for it. Third, encode semantics: define copy/reference/skip per relation for each in-scope model, and move domain rules (pricing, ownership, status transitions) into overrides or in-house post-processing rather than expecting the package to own them. Fourth, harden the write path: decide transaction boundaries, confirm behavior under soft deletes and UUID/ULID keys, and design the old-key→new-key map and provenance column into the feature spec now rather than retrofitting traceability later. Fifth, extend to async only where justified — the queued and batchable job plus four events turn duplication into a background operation with progress and notification, which is what unlocks larger graphs and bulk admin actions without blocking requests.
Cross-cutting integration points. Events give hooks for audit logging and notification without forking the package; the provenance column and key map feed analytics and support tooling; strictDepth() and cycle detection bound worst-case cost. Keep domain-specific copy rules out of the package and in the wrapper, so that when the package's API moves again — and it will — the blast radius is one internal class.
Maintenance. Steady-state maintenance is modest: MIT license, published documentation, an upgrade guide, and a coherent public API mean the day-to-day cost is version pinning and periodic review rather than patching. But the 2.0 rewrite is a structural signal: the package's internals are still moving, and a single-maintainer project with zero dependents has no community backpressure to keep APIs stable. Budget for a maintenance event at every major version, and keep the wrapper thin enough that the event is routine. The make-options command lowers the cost of adding newly duplicated models.
Support. There is no SLA, no commercial backing, and no visible support channel beyond the repository. Escalation path for a production defect is "read the source, patch or pin." That is acceptable for internal admin tooling on a team with Laravel depth; it is not acceptable where duplication sits on a customer-facing revenue path with an on-call rotation. The 105-test suite and max-level PHPStan mean the team can confidently read the source — a genuine support substitute, but a substitute nonetheless.
Scaling. Two axes. Row-count scaling is now governable: max_depth defaults to 1, deeper recursion is opt-in, cycle detection and strictDepth() bound the traversal, and preview()/dry run exposes cost before commit — which is precisely the control that was missing when a 43-row copy could silently become thousands. Throughput scaling is handled by the queued and batchable job, which moves duplication off the request path for large graphs and bulk operations. The remaining scaling unknown is relation-graph complexity, not volume: models with custom pivot behavior or versioning conventions need per-model validation, and that cost grows with the number of in-scope models rather than with traffic.
Failure modes. Ranked by how they actually present: (1) Silent incorrectness — a copy that looks right but references the wrong parent, points at the original's media directory, or carries a wrong unique suffix. This is the dominant failure mode for any duplication tool and the reason for the wrapper plus per-model verification. (2) Cost explosion — an unexpectedly deep graph producing far more rows than intended; mitigated by the conservative default, preview(), and strictDepth(), but only if the team actually uses preview before enabling the action. (3) Event-driven side effects — duplication fires events and can trigger observers, notifications, and integrations downstream; quietly() suppresses Eloquent events but the override ordering matters, and this is a per-model thing to test, not assume. (4) Upgrade breakage — an app that boots fine after a version change while behaving differently, which the 1.x→2.0 story demonstrates concretely. (5) Partial writes — mitigated by transactional duplication, but only if the caller opts in and understands nesting behavior. Note that (1) and (4) are the dangerous ones because they are invisible at deploy time.
Ramp-up. Low for engineers who already know Eloquent: a trait, a fluent options object, and an Artisan command to scaffold options is a half-day to first working duplication and a few days to per-model confidence. The relations and duplicate commands plus preview() make the learning loop fast and self-correcting, which is unusual for a package at this adoption level. The real ramp-up cost is not the API but the judgment: knowing which relations are copy vs. reference vs. skip, which domain rules must live outside the package, and which models need human verification after any version change. That judgment is product work, and it should be written down as per-model duplication specs rather than rediscovered per feature. Budget also for the upgrade-guide read and the 1.x verification pass if any duplication already shipped — that is a small migration project with a data-correctness component, not a dependency bump.
How can I help you explore Laravel packages today?