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

Laravel Record Merge Laravel Package

bernskiold/laravel-record-merge

Bernskiold Laravel Record Merge helps you safely combine duplicate Eloquent models, moving related records and resolving conflicts so you can clean up your database without losing data or breaking relationships.

View on GitHub
Deep Wiki
Context7

Getting Started

  • composer require bernskiold/laravel-record-merge and pin ^0.2.1 — earlier 0.2.x releases never dispatched the failure event, so any error-handling you build on top of it was effectively dead code.
  • Publish any package config/assets if the service provider exposes them (php artisan vendor:publish --provider="Bernskiold\RecordMerge\..."), then read the config file first. It's the fastest way to see which relations are movable, what the default merge strategy is, and which model contract/trait you need to add.
  • Add the package's mergeable contract or trait to the models you intend to deduplicate (typically Customer, Contact, Lead, Company). Only models carrying it are eligible sources/targets.
  • First use case: a "Merge duplicate" action in an admin resource. Build one screen that takes a source and a target record, shows a preview of what will move, and only then commits. Once that works, the same call is trivially reusable from an artisan command or an import cleanup job.
  • Look at the package's own tests for the canonical invocation signature — the public API surface is small but the exact method names on the merge service are worth confirming against the version you install rather than guessing.

Implementation Patterns

Preview-then-commit. Resolve the merge service, run it in a "show me what would happen" mode (or diff relation counts yourself), present that to the user, then run the real merge inside a DB::transaction(). Deduplication is destructive and irreversible; never wire the commit straight to a single click.

Keep the controller thin, push the work to a job. For records with many relations (orders, notes, attachments, activity log), a synchronous merge will time out. Dispatch a queued job that owns the transaction and reports back via the package's events. The job also gives you a natural place to store a serialized "merge report" for auditing.

Listen to the merge lifecycle events. This is where the 0.2.1 fix matters. Register listeners for the success event (write an audit row, dispatch a "records merged" notification, clear caches, redirect the source URL) and the failure event (alert, log the exception, mark the job failed with context). Because the failure event previously never fired, treat 0.2.1 as the first version where your failed listener is actually exercised — write a test that forces a failure (e.g. a relation throwing, or a unique constraint violation) and assert the listener runs.

Centralise conflict resolution. Most merges need a policy for "both records have a value in this column." Wrap the package in your own small MergePolicy class or config array so that this rule lives in one place per model, rather than being reimplemented in every admin controller.

Dry-run in tests, assert on relations. A good regression test for your integration looks like: create target with 2 orders, source with 1 order and 3 notes, merge, assert target has 3 orders and 3 notes, assert source is gone (or soft-deleted), assert both events fired. Do this per model you enable merging for — relation mapping mistakes are the most common bug.

Idempotency guard. Before merging, verify the two records are different and that the target hasn't already absorbed the source (e.g. source still exists). Double-submitted forms are a realistic failure mode.

Gotchas and Tips

  • The fail() → failed() rename (0.2.1) is technically a method rename on the event class. If you subclassed the event, called fail() yourself, or asserted on the old method name, update it. If you only registered a listener, nothing changes — but prior to 0.2.1 that listener was never invoked, so audit any "cleanup on failure" logic you assumed was running.
  • Database constraints, not PHP, will bite you. Moving child rows can violate unique indexes (one primary email per contact, one default address, unique slugs). Decide up front whether the merge should null out, suffix, or drop conflicting values, and handle it before the package moves the rows.
  • Polymorphic relations need your morph map registered and stable, otherwise moved rows point at the wrong parent type after the source is deleted.
  • Soft deletes. Merging into or from soft-deleted models behaves differently depending on your withTrashed scoping — check how the package resolves relations and don't assume trashed children move.
  • Media/attachments with physical storage paths. Files are usually keyed by model ID or a directory per record; a merge that updates DB rows but not storage paths leaves orphaned files. Handle these as a custom relation strategy rather than relying on the default move.
  • Observers and model events fire per moved row. Anything with side effects (sending mail, syncing to a CRM, touching updated_at-driven caches) will run many times during one merge. Consider withoutEvents() around the merge if those side effects aren't wanted mid-operation.
  • Large relation sets cause memory blowups. If the package loads relations eagerly, chunk or merge in batches for records with thousands of children, or accept the queue timeout as the trade-off.
  • Debugging: log the merge report (what moved, what was skipped, what conflicted) at info level with both model IDs. When a user says "my notes disappeared," that log is the only way to reconstruct which merge ate them.
  • Extension points: the relation strategy/configuration layer is where you should invest. Anything custom (files, external IDs, third-party sync tables) should be a named handler in config, not a if ($model instanceof ...) branch scattered across your app.
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.
digitickets/lalit
acacha/names
scool/foundation
frankkessler/guzzle-oauth2-middleware
delatbabel/site-config
delatbabel/applog
eusonlito/laravel-gettext
henzeb/housepedia-intelligence-laravel
aliqsm/laravel-authkit
itsjustvita/laravel-wp-migrator
uslanozan/laravel-cache-contract
adilazhari/laravel-trace
ulsyairil/ai-local-guardrails
dzwallet/laravel-sdk
glc36/maxloop-kernel
ikhbarnuridev/filament-mega-menu
mydnic/volet-chatbot
enadstack/laravel-localization
mydnic/volet-feature-board-filament-plugin
wmde/email-address