From 9b421e87887512a8f3683acc4afdd00f1bfa7679 Mon Sep 17 00:00:00 2001 From: Tofandel Date: Tue, 2 Jul 2024 05:23:58 +0200 Subject: [PATCH] Fix sync of medias with multiple fields --- src/Models/Behaviors/HasFiles.php | 2 +- src/Models/Behaviors/HasMedias.php | 1 + src/Repositories/Behaviors/HandleFiles.php | 21 +++++---- src/Repositories/Behaviors/HandleMedias.php | 16 ++++--- src/Repositories/BlockRepository.php | 6 ++- src/TwillUtil.php | 52 +++++++++++++++++++++ 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/Models/Behaviors/HasFiles.php b/src/Models/Behaviors/HasFiles.php index 160c34735e..d7e79aebed 100644 --- a/src/Models/Behaviors/HasFiles.php +++ b/src/Models/Behaviors/HasFiles.php @@ -18,7 +18,7 @@ public function files() File::class, 'fileable', config('twill.fileables_table', 'twill_fileables') - )->withPivot(['role', 'locale']) + )->withPivot(['id', 'role', 'locale']) ->withTimestamps()->orderBy(config('twill.fileables_table', 'twill_fileables') . '.id', 'asc'); } diff --git a/src/Models/Behaviors/HasMedias.php b/src/Models/Behaviors/HasMedias.php index 685d14305f..b3a54eaacb 100644 --- a/src/Models/Behaviors/HasMedias.php +++ b/src/Models/Behaviors/HasMedias.php @@ -45,6 +45,7 @@ public function medias() 'mediable', config('twill.mediables_table', 'twill_mediables') )->withPivot([ + 'id', 'crop', 'role', 'crop_w', diff --git a/src/Repositories/Behaviors/HandleFiles.php b/src/Repositories/Behaviors/HandleFiles.php index 876b7fa0cf..766482ab06 100644 --- a/src/Repositories/Behaviors/HandleFiles.php +++ b/src/Repositories/Behaviors/HandleFiles.php @@ -2,9 +2,10 @@ namespace A17\Twill\Repositories\Behaviors; +use A17\Twill\Facades\TwillUtil; +use A17\Twill\Models\Behaviors\HasFiles; use A17\Twill\Models\Contracts\TwillModelContract; use A17\Twill\Models\File; -use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Support\Collection; @@ -37,7 +38,7 @@ public function hydrateHandleFiles($object, $fields) } /** - * @param \A17\Twill\Models\Model $object + * @param \A17\Twill\Models\Model|HasFiles $object * @param array $fields * @return void */ @@ -47,7 +48,7 @@ public function afterSaveHandleFiles($object, $fields) return; } - $object->files()->sync($this->getFiles($fields)); + TwillUtil::syncUsingPrimaryKey($object->files(), $this->getFiles($fields)); } /** @@ -73,11 +74,11 @@ private function getFiles($fields) || in_array($role, config('twill.block_editor.files', [])) ) { Collection::make($filesForRole)->each(function ($file) use (&$files, $role, $locale) { - $files->push([ + $files[$file['pivot_id'] ?? uniqid('file')] = [ 'file_id' => $file['id'], 'role' => $role, 'locale' => $locale, - ]); + ]; }); } } @@ -97,8 +98,8 @@ public function getFormFieldsHandleFiles($object, $fields) if ($object->has('files')) { foreach ($object->files->groupBy('pivot.role') as $role => $filesByRole) { foreach ($filesByRole->groupBy('pivot.locale') as $locale => $filesByLocale) { - $fields['files'][$locale][$role] = $filesByLocale->map(function ($file) { - return $file->toCmsArray(); + $fields['files'][$locale][$role] = $filesByLocale->map(function (File $file) { + return $file->toCmsArray() + ['pivot_id' => $file->pivot->id]; }); } } @@ -107,9 +108,13 @@ public function getFormFieldsHandleFiles($object, $fields) return $fields; } + /** + * @param HasFiles|TwillModelContract $object + * @param HasFiles|TwillModelContract $newObject + */ public function afterDuplicateHandleFiles(TwillModelContract $object, TwillModelContract $newObject): void { - $newObject->files()->sync( + $newObject->files()->attach( $object->files->mapWithKeys(function ($file) use ($object) { return [ $file->id => Collection::make($object->files()->getPivotColumns())->mapWithKeys( diff --git a/src/Repositories/Behaviors/HandleMedias.php b/src/Repositories/Behaviors/HandleMedias.php index ed9017ebc2..729a72f42c 100644 --- a/src/Repositories/Behaviors/HandleMedias.php +++ b/src/Repositories/Behaviors/HandleMedias.php @@ -3,6 +3,8 @@ namespace A17\Twill\Repositories\Behaviors; use A17\Twill\Facades\TwillBlocks; +use A17\Twill\Facades\TwillUtil; +use A17\Twill\Models\Behaviors\HasMedias; use A17\Twill\Models\Contracts\TwillModelContract; use A17\Twill\Models\Media; use Illuminate\Support\Arr; @@ -43,7 +45,7 @@ public function hydrateHandleMedias($object, $fields) } /** - * @param \A17\Twill\Models\Model $object + * @param \A17\Twill\Models\Model|HasMedias $object * @param array $fields * @return void */ @@ -53,7 +55,7 @@ public function afterSaveHandleMedias($object, $fields) return; } - $object->medias()->sync($this->getMedias($fields)); + TwillUtil::syncUsingPrimaryKey($object->medias(), $this->getMedias($fields)); } /** @@ -80,7 +82,7 @@ private function getMedias($fields) $customMetadatas = $media['metadatas']['custom'] ?? []; if (isset($media['crops']) && !empty($media['crops'])) { foreach ($media['crops'] as $cropName => $cropData) { - $medias->push([ + $medias[$cropData['pivot_id'] ?? uniqid('media')] = [ 'media_id' => $media['id'], 'crop' => $cropName, 'role' => $role, @@ -91,11 +93,11 @@ private function getMedias($fields) 'crop_x' => $cropData['x'], 'crop_y' => $cropData['y'], 'metadatas' => json_encode($customMetadatas), - ]); + ]; } } else { foreach ($this->getCrops($role) as $cropName => $cropDefinitions) { - $medias->push([ + $medias[$media['pivot_id'] ?? uniqid('media')] = [ 'media_id' => $media['id'], 'crop' => $cropName, 'role' => $role, @@ -106,7 +108,7 @@ private function getMedias($fields) 'crop_x' => null, 'crop_y' => null, 'metadatas' => json_encode($customMetadatas), - ]); + ]; } } }); @@ -157,12 +159,14 @@ private function getMediaFormItems($medias) $item = $mediasById->first(); $itemForForm = $item->toCmsArray(); + $itemForForm['pivot_id'] = $item->pivot->id; $itemForForm['metadatas']['custom'] = json_decode($item->pivot->metadatas, true); foreach ($mediasById->groupBy('pivot.crop') as $crop => $mediaByCrop) { $media = $mediaByCrop->first(); $itemForForm['crops'][$crop] = [ + 'pivot_id' => $media->pivot->id, 'name' => $media->pivot->ratio, 'width' => $media->pivot->crop_w, 'height' => $media->pivot->crop_h, diff --git a/src/Repositories/BlockRepository.php b/src/Repositories/BlockRepository.php index 06e8e2b835..ed721d7f7c 100644 --- a/src/Repositories/BlockRepository.php +++ b/src/Repositories/BlockRepository.php @@ -3,6 +3,7 @@ namespace A17\Twill\Repositories; use A17\Twill\Facades\TwillBlocks; +use A17\Twill\Models\Block; use A17\Twill\Models\Contracts\TwillModelContract; use A17\Twill\Repositories\Behaviors\HandleFiles; use A17\Twill\Repositories\Behaviors\HandleMedias; @@ -75,10 +76,11 @@ public function afterSave(TwillModelContract $model, array $fields): void parent::afterSave($model, $fields); } + /** @param Block $object */ public function afterDelete(TwillModelContract $object): void { - $object->medias()->sync([]); - $object->files()->sync([]); + $object->medias()->detach(); + $object->files()->detach(); if (Schema::hasTable(config('twill.related_table', 'twill_related'))) { $object->clearAllRelated(); diff --git a/src/TwillUtil.php b/src/TwillUtil.php index cb93efe352..7cd1ee582e 100644 --- a/src/TwillUtil.php +++ b/src/TwillUtil.php @@ -3,6 +3,7 @@ namespace A17\Twill; use A17\Twill\Models\Contracts\TwillLinkableModel; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Session; @@ -103,4 +104,55 @@ private function pushToTempStore(string $key, int $frontendId, int $dbId): void Session::put(self::SESSION_FIELD, $sessionData); } + + public function syncUsingPrimaryKey(BelongsToMany $relation, $ids, $detaching = true): array + { + return (function ($ids, $detaching = true) { + $changes = [ + 'attached' => [], 'detached' => [], 'updated' => [], + ]; + + // First we need to attach any of the associated models that are not currently + // in this joining 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->getCurrentlyAttachedPivots() + ->pluck('id')->all(); + + $records = $this->formatRecordsList($this->parseIds($ids)); + + // Next, we will take the differences of the currents and given IDs and detach + // all of the entities that exist in the "current" array but are not in the + // array of the new IDs given to the method which will complete the sync. + if ($detaching) { + $detach = array_diff($current, array_keys($records)); + + if (count($detach) > 0) { + $this->newPivotQuery()->whereIn('id', $detach)->delete(); + + $changes['detached'] = $this->castKeys($detach); + } + } + + // Now we are finally ready to attach the new records. Note that we'll disable + // touching until after the entire operation is complete so we don't fire a + // ton of touch operations until we are totally done syncing the records. + $changes = array_merge( + $changes, + $this->attachNew($records, $current, false) + ); + + // Once we have finished attaching or detaching the records, we will see if we + // have done any attaching or detaching, and if we have we will touch these + // relationships if they are configured to touch on any database updates. + if ( + count($changes['attached']) || + count($changes['updated']) || + count($changes['detached']) + ) { + $this->touchIfTouching(); + } + + return $changes; + })->call($relation, $ids, $detaching); + } }