Merging Eloquent Models in Laravel (Part 2)

Ariel Vallese
4 min readSep 12, 2018

--

Photo by Sam Carter on Unsplash

In a previous article we covered how to merge eloquent models in a simple fashion. This allowed us to get the following result:

$modelA = Patient::make(['firstname' => 'John', 'lastname' => 'Doe', 'age' => 33]);
$modelB = Patient::make(['firstname' => 'John', 'lastname' => 'Doe', 'phone' => '+1 123 456 789', 'eyes' => 'green']);
// ... black magic here (ok, just a simple merging, see Part 1)dd($mergedModel->toArray());array:5 [
"firstname" => "John"
"lastname" => "Doe"
"age" => 33
"phone" => "+1 123 456 789"
"eyes" => "green"
]

But since not all scenarios are that simple, we‘ll keep moving on trying to cover several constraints and requirements to get as much control as possible over a merge operation.

Naming convention: Base and Dupe models

In order to start using a more appropiate wording to refer things, we may often call modelA as baseModel (the one we want to keep) and modelB as dupeModel(the one we want to get rid of).

Validating compound key attributes

Exact duplicates are easy to spot. We just see that model A has pretty much the exact same attributes of model B. Thus, we could mark any of them for deletion (there may, although, exist decision strategies for picking the best option).

But the scenario turns a bit harsher when models A and B have similar attributes but not equal. Still, after we determine that they correspond to the same real world single entity, we may want to merge while taking some cautions.

Before moving to merge, we could add a validation constraint to make sure that we only move forward if at least the compound key (defined by us) is the exact same. This allows us to STOP if we are about to merge models that look the same but actually are not.

$keys = ['firstname', 'lastname', 'age'];$dataA = $modelA->only($keys);
$dataB = $modelB->only($keys);
if ($dataA != $dataB) {
// Models are not considered duplicates
// ... abort merge
}
// ... merge to put together other fields (like eyes, phone, address, email, etc)

If we wrapped this validation up into a package like model merge, we could do something like this:

try {    $mergedModel = 
ModelMerge::withKey(['firstname', 'lastname', 'age'])
->setModelA($modelA)
->setModelB($modelB)
->merge();
} catch (ModelsNotDupeException $exception) { // ... models were not duplicate. Abort mission. phew!!}

Automatically saving changes

So at this point, we kinda got the best of both worlds into a new model we called $mergedModel. But to be fair, you might be saying — we now have 3 model instances instead of just one, you are not helping me. You are right.

We still need to discard the duplicate and save the original model with updated attributes like so:

// Update the base model with merged values
$modelA->fill($mergedModel->toArray());
// Save base model to preserve changes
$modelA->save();
// Say goodbye to our impostor dupe
$modelB->delete();

Of course, we can wrap this up into a nice package call:

$baseModel = ModelMerge::setBase($baseModel)
->setDupe($dupeModel)
->unifyOnBase();
$baseModel->firstname // John
$baseModel->lastname // Doe
$baseModel->age // 33
$baseModel->phone // +1 123 456 789
$baseModel->exists // true (saved to database)
$dupeModel->exists // false (deleted from database)

Preferring newest or oldest model

Sometimes we will want to explicitly keep either the newest or oldest record but make sure we update it first with any extra value that might have arrived on its accidental rebirth.

$oldest = DummyContact::create([
'firstname' => 'John',
'lastname' => 'Doe',
'age' => 33,
'eyes' => 'green',
'created_at' => Carbon::now()]);
$newest = DummyContact::create([
'firstname' => 'John',
'lastname' => 'Doe',
'age' => 34,
'phone' => '+1 123 456 789',
'created_at' => Carbon::now()->addDay()]);
// before merging, we would first check which is the oldest recordif($oldest->created_at < $newest->created_at)
{
// ... safe merge
}
else
{
// ... swap models and then merge
}

We can also achieve this easily in a packaged feature like so:

$baseModel = ModelMerge::setBase($oldestModel)
->setDupe($newestModel)
->preferNewest()
->unifyOnBase();
$baseModel->firstname; // John
$baseModel->lastname; // Doe
$baseModel->age; // 34
$baseModel->eyes; // green
$baseModel->phone; // +1 123 456 789
$oldest->exists; // false
$newest->exists; // true

That’s all for now. We are now able to merge plain models in a safe and controllable fashion.

The idea of this post is both explaining how to achieve a happy path to models merging and provide a package to get this working out-of-the box. Feel free to comment below any other scenario you would like to get covered.

Relationships

Wondering about relationships? I’m covering a bit of this in PART 3, keep reading.

--

--

Ariel Vallese

Hi, I’m Ariel Vallese, I enjoy coding on Laravel. Created Fimedi NET for dietitians and patients and Timegrid in the past. Also love diving and traveling.