diff --git a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php index 49eec6e304..2b0d7b7b5d 100644 --- a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php +++ b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php @@ -296,13 +296,32 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field) case 'MorphToMany': case 'BelongsToMany': $pivotSelectorField = static::getPivotFieldStructure($field); + + $pivot = Arr::where($field['subfields'], function ($item) use ($pivotSelectorField) { + return $item['name'] === $pivotSelectorField['name']; + }); + + if (! empty($pivot)) { + break; + } + $this->setupFieldValidation($pivotSelectorField, $field['name']); $field['subfields'] = Arr::prepend($field['subfields'], $pivotSelectorField); + break; case 'MorphMany': case 'HasMany': $entity = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$field['entity'] : $field['entity']; $relationInstance = $this->getRelationInstance(['entity' => $entity]); + + $localKeyField = Arr::where($field['subfields'], function ($item) use ($relationInstance) { + return $item['name'] === $relationInstance->getRelated()->getKeyName(); + }); + + if (! empty($localKeyField)) { + break; + } + $field['subfields'] = Arr::prepend($field['subfields'], [ 'name' => $relationInstance->getRelated()->getKeyName(), 'type' => 'hidden', diff --git a/src/app/Library/Uploaders/SingleFile.php b/src/app/Library/Uploaders/SingleFile.php index 5ed8500450..02c0c9e81f 100644 --- a/src/app/Library/Uploaders/SingleFile.php +++ b/src/app/Library/Uploaders/SingleFile.php @@ -23,7 +23,7 @@ public function uploadFiles(Model $entry, $value = null) return $this->getPath().$fileName; } - if (! $value && CrudPanelFacade::getRequest()->has($this->getName()) && $previousFile) { + if (! $value && CrudPanelFacade::getRequest()->has($this->getRepeatableContainerName() ?? $this->getName()) && $previousFile) { Storage::disk($this->getDisk())->delete($previousFile); return null; diff --git a/src/app/Library/Uploaders/Support/Interfaces/UploaderInterface.php b/src/app/Library/Uploaders/Support/Interfaces/UploaderInterface.php index 337765e1d0..fe3db9fccf 100644 --- a/src/app/Library/Uploaders/Support/Interfaces/UploaderInterface.php +++ b/src/app/Library/Uploaders/Support/Interfaces/UploaderInterface.php @@ -35,6 +35,8 @@ public function relationship(bool $isRelation): self; */ public function getName(): string; + public function getAttributeName(): string; + public function getDisk(): string; public function getPath(): string; @@ -53,5 +55,7 @@ public function shouldDeleteFiles(): bool; public function canHandleMultipleFiles(): bool; + public function isRelationship(): bool; + public function getPreviousFiles(Model $entry): mixed; } diff --git a/src/app/Library/Uploaders/Support/RegisterUploadEvents.php b/src/app/Library/Uploaders/Support/RegisterUploadEvents.php index f47486f667..53458b819c 100644 --- a/src/app/Library/Uploaders/Support/RegisterUploadEvents.php +++ b/src/app/Library/Uploaders/Support/RegisterUploadEvents.php @@ -7,6 +7,7 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface; use Exception; +use Illuminate\Database\Eloquent\Relations\Pivot; final class RegisterUploadEvents { @@ -46,6 +47,10 @@ private function registerEvents(array|null $subfield = [], ?bool $registerModelE $model = $attributes['model'] ?? get_class($this->crudObject->crud()->getModel()); $uploader = $this->getUploader($attributes, $this->uploaderConfiguration); + if (isset($attributes['relation_type']) && $attributes['entity'] !== false) { + $uploader = $uploader->relationship(true); + } + $this->setupModelEvents($model, $uploader); $this->setupUploadConfigsInCrudObject($uploader); } @@ -63,12 +68,13 @@ private function registerSubfieldEvent(array $subfield, bool $registerModelEvent return; } - $model = $subfield['baseModel'] ?? get_class($this->crudObject->crud()->getModel()); - if (isset($crudObject['relation_type']) && $crudObject['entity'] !== false) { $uploader = $uploader->relationship(true); + $subfield['relation_type'] = $crudObject['relation_type']; } + $model = $this->getSubfieldModel($subfield, $uploader); + // only the last subfield uploader will setup the model events for the whole group if ($registerModelEvents) { $this->setupModelEvents($model, $uploader); @@ -120,7 +126,11 @@ private function setupModelEvents(string $model, UploaderInterface $uploader): v if (app('crud')->entry) { app('crud')->entry = $uploader->retrieveUploadedFiles(app('crud')->entry); } else { - $model::retrieved(function ($entry) use ($uploader) { + // the retrieve model may differ from the deleting and saving models because retrieved event + // is not called in pivot models when loading the relations. + $retrieveModel = $this->getModelForRetrieveEvent($model, $uploader); + + $retrieveModel::retrieved(function ($entry) use ($uploader) { if ($entry->translationEnabled()) { $locale = request('_locale', \App::getLocale()); if (in_array($locale, array_keys($entry->getAvailableLocales()))) { @@ -172,4 +182,26 @@ private function setupUploadConfigsInCrudObject(UploaderInterface $uploader): vo { $this->crudObject->upload(true)->disk($uploader->getDisk())->prefix($uploader->getPath()); } + + private function getSubfieldModel(array $subfield, UploaderInterface $uploader) + { + if (! $uploader->isRelationship()) { + return $subfield['baseModel'] ?? get_class(app('crud')->getModel()); + } + + if (in_array($subfield['relation_type'], ['BelongsToMany', 'MorphToMany'])) { + return app('crud')->getModel()->{$subfield['baseEntity']}()->getPivotClass(); + } + + return $subfield['baseModel']; + } + + private function getModelForRetrieveEvent(string $model, UploaderInterface $uploader) + { + if (! $uploader->isRelationship()) { + return $model; + } + + return is_a($model, Pivot::class, true) ? $this->crudObject->getAttributes()['model'] : $model; + } } diff --git a/src/app/Library/Uploaders/Support/Traits/HandleRepeatableUploads.php b/src/app/Library/Uploaders/Support/Traits/HandleRepeatableUploads.php index 7caf3ef9b2..643349f530 100644 --- a/src/app/Library/Uploaders/Support/Traits/HandleRepeatableUploads.php +++ b/src/app/Library/Uploaders/Support/Traits/HandleRepeatableUploads.php @@ -63,8 +63,8 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model $value = $value->slice($modelCount, 1)->toArray(); foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { - if (array_key_exists($modelCount, $value) && isset($value[$modelCount][$uploader->getName()])) { - $entry->{$uploader->getName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getName()]); + if (array_key_exists($modelCount, $value) && array_key_exists($uploader->getAttributeName(), $value[$modelCount])) { + $entry->{$uploader->getAttributeName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getAttributeName()]); } } @@ -74,10 +74,10 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model protected function processRepeatableUploads(Model $entry, Collection $values): Collection { foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { - $uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader)); + $uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader)); $values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) { - $item[$uploader->getName()] = $uploadedValues[$key] ?? null; + $item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null; return $item; }); @@ -89,7 +89,7 @@ protected function processRepeatableUploads(Model $entry, Collection $values): C private function retrieveRepeatableFiles(Model $entry): Model { if ($this->isRelationship) { - return $this->retrieveFiles($entry); + return $this->retrieveRepeatableRelationFiles($entry); } $repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()); @@ -98,7 +98,7 @@ private function retrieveRepeatableFiles(Model $entry): Model $values = is_string($values) ? json_decode($values, true) : $values; $values = array_map(function ($item) use ($repeatableUploaders) { foreach ($repeatableUploaders as $upload) { - $item[$upload->getName()] = $this->getValuesWithPathStripped($item, $upload); + $item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload); } return $item; @@ -109,10 +109,48 @@ private function retrieveRepeatableFiles(Model $entry): Model return $entry; } + private function retrieveRepeatableRelationFiles(Model $entry) + { + switch($this->getRepeatableRelationType()) { + case 'BelongsToMany': + case 'MorphToMany': + $pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass(); + $pivotFieldName = 'pivot_'.$this->getAttributeName(); + $connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]); + $entry->{$pivotFieldName} = $this->retrieveFiles($connectedEntry)->{$this->getAttributeName()}; + + break; + default: + $entry = $this->retrieveFiles($entry); + } + + return $entry; + } + + private function getRepeatableRelationType() + { + return $this->getUploaderField()->getAttributes()['relation_type']; + } + + private function getUploaderField() + { + return app('crud')->field($this->getRepeatableContainerName()); + } + + private function getUploaderSubfield() + { + return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->getName())->first(); + } + + private function getUploaderFieldSubfields() + { + return $this->getUploaderField()->getAttributes()['subfields']; + } + private function deleteRepeatableFiles(Model $entry): void { if ($this->isRelationship) { - $this->deleteFiles($entry); + $this->deleteRelationshipFiles($entry); return; } @@ -199,4 +237,42 @@ private function getValuesWithPathStripped(array|string|null $item, UploaderInte return isset($uploadedValues) ? Str::after($uploadedValues, $upload->getPath()) : null; } + + private function deleteRelationshipFiles(Model $entry): void + { + if (in_array($this->getRepeatableRelationType(), ['BelongsToMany', 'MorphToMany'])) { + $pivotAttributes = $entry->getAttributes(); + $connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) { + $itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes)); + + return $itemPivotAttributes === $pivotAttributes; + })->first(); + + if (! $connectedPivot) { + return; + } + + $files = $connectedPivot->getOriginal()['pivot_'.$this->getAttributeName()]; + + if (! $files) { + return; + } + + if (is_array($files)) { + foreach ($files as $value) { + $value = Str::start($value, $this->getPath()); + Storage::disk($this->getDisk())->delete($value); + } + + return; + } + + $value = Str::start($files, $this->getPath()); + Storage::disk($this->getDisk())->delete($value); + + return; + } + + $this->deleteFiles($entry); + } } diff --git a/src/app/Library/Uploaders/Uploader.php b/src/app/Library/Uploaders/Uploader.php index c591a87a74..e636aa6fa9 100644 --- a/src/app/Library/Uploaders/Uploader.php +++ b/src/app/Library/Uploaders/Uploader.php @@ -74,14 +74,14 @@ public function storeUploadedFiles(Model $entry): Model if ($this->attachedToFakeField) { $fakeFieldValue = $entry->{$this->attachedToFakeField}; $fakeFieldValue = is_string($fakeFieldValue) ? json_decode($fakeFieldValue, true) : (array) $fakeFieldValue; - $fakeFieldValue[$this->getName()] = $this->uploadFiles($entry); + $fakeFieldValue[$this->getAttributeName()] = $this->uploadFiles($entry); $entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $fakeFieldValue : json_encode($fakeFieldValue); return $entry; } - $entry->{$this->getName()} = $this->uploadFiles($entry); + $entry->{$this->getAttributeName()} = $this->uploadFiles($entry); return $entry; } @@ -118,6 +118,11 @@ public function getName(): string return $this->name; } + public function getAttributeName(): string + { + return Str::afterLast($this->name, '.'); + } + public function getDisk(): string { return $this->disk; @@ -157,6 +162,11 @@ public function canHandleMultipleFiles(): bool return $this->handleMultipleFiles; } + public function isRelationship(): bool + { + return $this->isRelationship; + } + public function getPreviousFiles(Model $entry): mixed { if (! $this->attachedToFakeField) { @@ -166,7 +176,7 @@ public function getPreviousFiles(Model $entry): mixed $value = $this->getOriginalValue($entry, $this->attachedToFakeField); $value = is_string($value) ? json_decode($value, true) : (array) $value; - return $value[$this->getName()] ?? null; + return $value[$this->getAttributeName()] ?? null; } /******************************* @@ -195,11 +205,11 @@ public function uploadFiles(Model $entry, $values = null) private function retrieveFiles(Model $entry): Model { - $value = $entry->{$this->name}; + $value = $entry->{$this->getAttributeName()}; if ($this->handleMultipleFiles) { - if (! isset($entry->getCasts()[$this->name]) && is_string($value)) { - $entry->{$this->name} = json_decode($value, true); + if (! isset($entry->getCasts()[$this->getName()]) && is_string($value)) { + $entry->{$this->getAttributeName()} = json_decode($value, true); } return $entry; @@ -209,13 +219,13 @@ private function retrieveFiles(Model $entry): Model $values = $entry->{$this->attachedToFakeField}; $values = is_string($values) ? json_decode($values, true) : (array) $values; - $values[$this->name] = isset($values[$this->name]) ? Str::after($values[$this->name], $this->path) : null; + $values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? Str::after($values[$this->getAttributeName()], $this->path) : null; $entry->{$this->attachedToFakeField} = json_encode($values); return $entry; } - $entry->{$this->name} = Str::after($value, $this->path); + $entry->{$this->getAttributeName()} = Str::after($value, $this->path); return $entry; } @@ -242,7 +252,7 @@ private function deleteFiles(Model $entry) private function performFileDeletion(Model $entry) { - if ($this->isRelationship || ! $this->handleRepeatableFiles) { + if (! $this->handleRepeatableFiles) { $this->deleteFiles($entry); return; @@ -263,13 +273,13 @@ private function getPathFromConfiguration(array $crudObject, array $configuratio private function getOriginalValue(Model $entry, $field = null) { - $previousValue = $entry->getOriginal($field ?? $this->getName()); + $previousValue = $entry->getOriginal($field ?? $this->getAttributeName()); if (! $previousValue) { return $previousValue; } - if ($entry->translationEnabled()) { + if (method_exists($entry, 'translationEnabled') && $entry->translationEnabled()) { return $previousValue[$entry->getLocale()] ?? null; }