The problem with deleting and reading related objects is that it will violate any foreign key restrictions that you may have on these child objects.
The best solution is to change the relationship of Laravel HasMany
to the sync
method:
<?php namespace App\Model\Relations; use Illuminate\Database\Eloquent\Relations\HasMany; class HasManySyncable extends HasMany { public function sync($data, $deleting = true) { $changes = [ 'created' => [], 'deleted' => [], 'updated' => [], ]; $relatedKeyName = $this->related->getKeyName(); // First we need to attach any of the associated models that are not currently // in the child entity table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. $current = $this->newQuery()->pluck( $relatedKeyName )->all(); // Separate the submitted data into "update" and "new" $updateRows = []; $newRows = []; foreach ($data as $row) { // We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and // match a related row in the database. if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) { $id = $row[$relatedKeyName]; $updateRows[$id] = $row; } else { $newRows[] = $row; } } // Next, we'll determine the rows in the database that aren't in the "update" list. // These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id'). $updateIds = array_keys($updateRows); $deleteIds = []; foreach ($current as $currentId) { if (!in_array($currentId, $updateIds)) { $deleteIds[] = $currentId; } } // Delete any non-matching rows if ($deleting && count($deleteIds) > 0) { $this->getRelated()->destroy($deleteIds); $changes['deleted'] = $this->castKeys($deleteIds); } // Update the updatable rows foreach ($updateRows as $id => $row) { $this->getRelated()->where($relatedKeyName, $id) ->update($row); } $changes['updated'] = $this->castKeys($updateIds); // Insert the new rows $newIds = []; foreach ($newRows as $row) { $newModel = $this->create($row); $newIds[] = $newModel->$relatedKeyName; } $changes['created'][] = $this->castKeys($newIds); return $changes; } /** * Cast the given keys to integers if they are numeric and string otherwise. * * @param array $keys * @return array */ protected function castKeys(array $keys) { return (array) array_map(function ($v) { return $this->castKey($v); }, $keys); } /** * Cast the given key to an integer if it is numeric. * * @param mixed $key * @return mixed */ protected function castKey($key) { return is_numeric($key) ? (int) $key : (string) $key; } }
You can override the Eloquent Model
class to use HasManySyncable
instead of the standard HasMany
relation:
<?php namespace App\Model; use App\Model\Relations\HasManySyncable; use Illuminate\Database\Eloquent\Model; abstract class MyBaseModel extends Model { public function hasMany($related, $foreignKey = null, $localKey = null) { $instance = $this->newRelatedInstance($related); $foreignKey = $foreignKey ?: $this->getForeignKey(); $localKey = $localKey ?: $this->getKeyName(); return new HasManySyncable( $instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey ); }
Suppose your Post
model extends MyBaseModel
and has links()
HasMany
, you can do something like:
$post->links()->sync([ [ 'id' => 21, 'name' => "LinkedIn profile" ], [ 'id' => null, 'label' => "Personal website" ] ]);
All records in this multidimensional array that have id
that correspond to the table of child entities ( links
) will be updated. Entries in the table that are not in this array will be deleted. Entries in the array that are not present in the table (have an id
or id
of null mismatch) will be considered βnewβ entries and will be inserted into the database.