[Laravel 5.2] sync method for Has Many Relationship

Amith Gotamey
Jul 5, 2016 · 2 min read

I will assume that those of you who are reading this article are well versed in the Eloquent Model Relationships. If not, then I urge you to have a look at their documentation. I simply love the convenient sync method for Many To Many Relationship (using a pivot table). It takes an array of ids and syncs those with the calling model (deletes the missing records and adds new records from the ids array into the pivot/related table).

While this is excellent, you would expect to have similar functionality in the Has Many Relationships too. As far as I know and could research, I could not find any sync method in Illuminate\Database\Eloquent\Relations\HasMany or the Illuminate\Database\Eloquent\Relations\HasOnOrMany code base.

I have devised a simple method to do so using simple collection pipelines. To illustrate this, lets assume that we have an Invoice model which HasMany InvoiceItems. Now lets see what a sync method would look like. We will add this method to the Invoice model itself for the sake of this article:

<?phpnamespace App;use Illuminate\Database\Eloquent\Model;class Invoice extends Model
{
/**
* ...
*/
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function invoice_items()
{
return $this->hasMany(InvoiceItem::class);
}
/**
* @param array $invoice_items
*/
public function syncInvoiceItems(array $invoice_items)
{
$children = $this->invoice_items;
$invoice_items = collect($invoice_items);
$deleted_ids = $children->filter(
function ($child) use ($invoice_items) {
return empty(
$invoice_items->where('id', $child->id)->first()
);
}
)->map(function ($child) {
$id = $child->id;
$child->delete();
return $id;
}
);
$attachments = $invoice_items->filter(
function ($invoice_item) {
return empty($invoice_item['id']);
}
)->map(function ($invoice_item) use ($deleted_ids) {
$invoice_item['id'] = $deleted_ids->pop();
return new InvoiceItem($invoice_item);
});
$this->invoice_items()->saveMany($attachments);
}
/**
* ...
*/
}

Ok, let me explain all that mess. We are working with the Invoice model so $this refers to the model. The only hard rule this method should follow are follows:

/**
* If you are attaching InvoiceItem(s) for the first time, then pass
* in just the array of attributes:
* [
* [
* // invoice item attributes...
* ],
* [
* // invoice item attributes...
* ],
* ]
*/
/**
* If you are attaching new InvoiceItem(s) along with existing
* items, then you can pass just the `id` attribute
* [
* [
* 'id' => 123
* ],
* [
* // new invoice item attributes...
* ],
* ]
*/

Please keep in mind that if there were existing InvoiceItem(s) and you did not pass along those ids then it will be deleted. And also, if you want to modify existing details, then pass in the attributes without the id. Please note that I am capturing those deleted_ids to reuse it so that multiple edits do not drastically increase the auto increment (Infinity-phobia ¯\_(ツ)_/¯).

I hope this helps. I would love corrections and contributions. Cheers!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store