Skip to content

Instantly share code, notes, and snippets.

@patoui
Last active February 7, 2020 13:25
Show Gist options
  • Save patoui/cbd8a6370d8786565f2bb0efdae55867 to your computer and use it in GitHub Desktop.
Save patoui/cbd8a6370d8786565f2bb0efdae55867 to your computer and use it in GitHub Desktop.
Utility class to perform multiple updates on a has many relationship (thanks to Glen UK on Larachat for finding bugs)
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class HasManyUpdater
{
/** @var Model */
private $owner;
/** @var string */
private $related;
/** @var string|null */
private $relation;
/**
* @param Model $owner Instance of the owning model
* @param string $related Fully qualified path of the related model
* @param string|null $relation Name of the relation method on the owner model
*/
public function __construct(
Model $owner,
string $related,
?string $relation = null
) {
$this->owner = $owner;
if (! $this->owner->exists) {
throw (new ModelNotFoundException())
->setModel(get_class($this->owner));
}
$relatedInstance = App::make($related);
if (! $relatedInstance instanceof Model) {
throw new InvalidArgumentException(
"Property 'related' should be an instance of ".Model::class
);
}
$this->related = $relatedInstance;
$this->relation = $relation ?? $this->related->getTable();
if (! method_exists($this->owner, $this->relation)) {
throw RelationNotFoundException::make($this->owner, $this->relation);
}
}
public function update(array $others) : array
{
$changes = DB::transaction(function () use ($others) {
$createdIds = [];
$updatedIds = [];
$existingIds = $this->owner->{$this->relation}()->pluck('id')->toArray();
foreach ($others as $other) {
$id = data_get($other, 'id');
if ($id) {
$this->related->findOrFail($id)->update($other);
$updatedIds[] = $id;
} else {
$createdIds[] = $this->owner->{$this->relation}()->create($other)->id;
}
}
$deletedIds = array_diff($existingIds, $updatedIds);
if (! empty($deletedIds)) {
$this->owner
->{$this->relation}()
->whereIn($this->related->getTable().'.id', $deletedIds)
->delete();
}
return [
'created' => $createdIds,
'deleted' => $deletedIds,
'updated' => $updatedIds
];
});
return $changes;
}
}
@clarg18
Copy link

clarg18 commented Sep 26, 2019

on line 63 the foreignKey name should be derived from the owner table, rather than the related table:

$this->foreignKey = $foreignKey ?? Str::singular($this->owner->getTable()).'_id';

Additionally, the code creates a new record if the id of the related record is not numeric; however, it does not change the ID. This causes an SQL failure if the ID is a string, for example. I corrected this by adding 'id' => null in the merge_array function on line 81:

                   $createdIds[] = $this->related->create(array_merge(
                        $other,
                        [
                            $this->foreignKey => $this->owner->id,
                            'id' => null,
                        ]
                    ))->id;

@patoui
Copy link
Author

patoui commented Sep 26, 2019

@glendog good catches!

I'll update the first mentioned right away!

For the second I think I made an assumption which I shouldn't have. An ID could potentially be a string (i.e. uuid) so I'm going to change the if statement to check for its existence.

@patoui
Copy link
Author

patoui commented Sep 26, 2019

Removed foreignKey in favour of using the existing relationship

$createdIds[] = $this->owner->{$this->relation}()->create($other)->id;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment