[Laravel 5.2] sync method for Has Many Relationship

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:

<?php
namespace 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!