So I ended up with the following implementation:
PollQuestion model
<?php
namespace App\Models;
use App\Support\Arrays;
class PollQuestion extends AbstractModel
{
public function syncOptions(array $options, $column = 'option')
{
$new_options = array_filter($options);
$old_options = $this->options->lists($column, 'id');
// Delete removed options, if any
if ($deleted = Arrays::keysDeleted($new_options, $old_options)) {
$this->options()->whereIn('id', $deleted)->delete();
}
// Create new options, if any
if ($created = Arrays::keysCreated($new_options, $old_options)) {
foreach ($created as $id) {
$new[] = $this->options()->getModel()->newInstance([
$column => $new_options[$id],
]);
}
$this->options()->saveMany($new);
}
// Update changed options, if any
if ($updated = Arrays::keysUpdated($new_options, $old_options)) {
foreach ($updated as $id) {
$this->options()->find($id)->update([
$column => $new_options[$id],
]);
}
}
}
}
Arrays support class
<?php
namespace App\Support;
use Underscore\Types\Arrays as _Arrays;
class Arrays extends _Arrays
{
public static function keysCreated($new, $old)
{
return array_keys(array_diff_key($new, $old));
}
public static function keysDeleted($new, $old)
{
return array_keys(array_diff_key($old, $new));
}
public static function keysUpdated($new, $old)
{
return array_diff(
array_keys(array_diff_assoc($new, $old)),
static::keysCreated($new, $old)
);
}
}
It became much simpler than it was before.
The code still belongs to this model only, so to achieve that same functionality in another model, I still have to duplicate the code. So I further extended the implementation and moved it to AbstractModel, in a new sync($relationship, $column, $values) method:
AbstractModel
<?php
namespace App\Models;
use App\Support\Arrays;
use Illuminate\Database\Eloquent\Model;
abstract class AbstractModel extends Model implements \JsonSerializable
{
public function sync($relationship, $column, array $values)
{
$new_values = array_filter($values);
$old_values = $this->$relationship->lists($column, 'id');
// Delete removed values, if any
if ($deleted = Arrays::keysDeleted($new_values, $old_values)) {
$this->$relationship()->whereIn('id', $deleted)->delete();
}
// Create new values, if any
if ($created = Arrays::keysCreated($new_values, $old_values)) {
foreach ($created as $id) {
$new[] = $this->$relationship()->getModel()->newInstance([
$column => $new_values[$id],
]);
}
$this->$relationship()->saveMany($new);
}
// Update changed values, if any
if ($updated = Arrays::keysUpdated($new_values, $old_values)) {
foreach ($updated as $id) {
$this->$relationship()->find($id)->update([
$column => $new_values[$id],
]);
}
}
}
}
So I cleaned up my PollQuestion model and now I can use $poll->sync('options', 'option', $options) to sync new options with existing ones. Do you think it looks good? Can it be further improved? I'm welcome to suggestions. :)