Skip to content

Commit 31927e8

Browse files
authored
Merge pull request #5243 from Laravel-Backpack/handle-uploads-inside-relationships
Handle uploads inside relationships
2 parents bc15b25 + b299410 commit 31927e8

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,32 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field)
296296
case 'MorphToMany':
297297
case 'BelongsToMany':
298298
$pivotSelectorField = static::getPivotFieldStructure($field);
299+
300+
$pivot = Arr::where($field['subfields'], function ($item) use ($pivotSelectorField) {
301+
return $item['name'] === $pivotSelectorField['name'];
302+
});
303+
304+
if (! empty($pivot)) {
305+
break;
306+
}
307+
299308
$this->setupFieldValidation($pivotSelectorField, $field['name']);
300309
$field['subfields'] = Arr::prepend($field['subfields'], $pivotSelectorField);
310+
301311
break;
302312
case 'MorphMany':
303313
case 'HasMany':
304314
$entity = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$field['entity'] : $field['entity'];
305315
$relationInstance = $this->getRelationInstance(['entity' => $entity]);
316+
317+
$localKeyField = Arr::where($field['subfields'], function ($item) use ($relationInstance) {
318+
return $item['name'] === $relationInstance->getRelated()->getKeyName();
319+
});
320+
321+
if (! empty($localKeyField)) {
322+
break;
323+
}
324+
306325
$field['subfields'] = Arr::prepend($field['subfields'], [
307326
'name' => $relationInstance->getRelated()->getKeyName(),
308327
'type' => 'hidden',

src/app/Library/Uploaders/SingleFile.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function uploadFiles(Model $entry, $value = null)
2323
return $this->getPath().$fileName;
2424
}
2525

26-
if (! $value && CrudPanelFacade::getRequest()->has($this->getName()) && $previousFile) {
26+
if (! $value && CrudPanelFacade::getRequest()->has($this->getRepeatableContainerName() ?? $this->getName()) && $previousFile) {
2727
Storage::disk($this->getDisk())->delete($previousFile);
2828

2929
return null;

src/app/Library/Uploaders/Support/Interfaces/UploaderInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public function relationship(bool $isRelation): self;
3535
*/
3636
public function getName(): string;
3737

38+
public function getAttributeName(): string;
39+
3840
public function getDisk(): string;
3941

4042
public function getPath(): string;
@@ -53,5 +55,7 @@ public function shouldDeleteFiles(): bool;
5355

5456
public function canHandleMultipleFiles(): bool;
5557

58+
public function isRelationship(): bool;
59+
5660
public function getPreviousFiles(Model $entry): mixed;
5761
}

src/app/Library/Uploaders/Support/RegisterUploadEvents.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
88
use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface;
99
use Exception;
10+
use Illuminate\Database\Eloquent\Relations\Pivot;
1011

1112
final class RegisterUploadEvents
1213
{
@@ -46,6 +47,10 @@ private function registerEvents(array|null $subfield = [], ?bool $registerModelE
4647
$model = $attributes['model'] ?? get_class($this->crudObject->crud()->getModel());
4748
$uploader = $this->getUploader($attributes, $this->uploaderConfiguration);
4849

50+
if (isset($attributes['relation_type']) && $attributes['entity'] !== false) {
51+
$uploader = $uploader->relationship(true);
52+
}
53+
4954
$this->setupModelEvents($model, $uploader);
5055
$this->setupUploadConfigsInCrudObject($uploader);
5156
}
@@ -63,12 +68,13 @@ private function registerSubfieldEvent(array $subfield, bool $registerModelEvent
6368
return;
6469
}
6570

66-
$model = $subfield['baseModel'] ?? get_class($this->crudObject->crud()->getModel());
67-
6871
if (isset($crudObject['relation_type']) && $crudObject['entity'] !== false) {
6972
$uploader = $uploader->relationship(true);
73+
$subfield['relation_type'] = $crudObject['relation_type'];
7074
}
7175

76+
$model = $this->getSubfieldModel($subfield, $uploader);
77+
7278
// only the last subfield uploader will setup the model events for the whole group
7379
if ($registerModelEvents) {
7480
$this->setupModelEvents($model, $uploader);
@@ -120,7 +126,11 @@ private function setupModelEvents(string $model, UploaderInterface $uploader): v
120126
if (app('crud')->entry) {
121127
app('crud')->entry = $uploader->retrieveUploadedFiles(app('crud')->entry);
122128
} else {
123-
$model::retrieved(function ($entry) use ($uploader) {
129+
// the retrieve model may differ from the deleting and saving models because retrieved event
130+
// is not called in pivot models when loading the relations.
131+
$retrieveModel = $this->getModelForRetrieveEvent($model, $uploader);
132+
133+
$retrieveModel::retrieved(function ($entry) use ($uploader) {
124134
if ($entry->translationEnabled()) {
125135
$locale = request('_locale', \App::getLocale());
126136
if (in_array($locale, array_keys($entry->getAvailableLocales()))) {
@@ -172,4 +182,26 @@ private function setupUploadConfigsInCrudObject(UploaderInterface $uploader): vo
172182
{
173183
$this->crudObject->upload(true)->disk($uploader->getDisk())->prefix($uploader->getPath());
174184
}
185+
186+
private function getSubfieldModel(array $subfield, UploaderInterface $uploader)
187+
{
188+
if (! $uploader->isRelationship()) {
189+
return $subfield['baseModel'] ?? get_class(app('crud')->getModel());
190+
}
191+
192+
if (in_array($subfield['relation_type'], ['BelongsToMany', 'MorphToMany'])) {
193+
return app('crud')->getModel()->{$subfield['baseEntity']}()->getPivotClass();
194+
}
195+
196+
return $subfield['baseModel'];
197+
}
198+
199+
private function getModelForRetrieveEvent(string $model, UploaderInterface $uploader)
200+
{
201+
if (! $uploader->isRelationship()) {
202+
return $model;
203+
}
204+
205+
return is_a($model, Pivot::class, true) ? $this->crudObject->getAttributes()['model'] : $model;
206+
}
175207
}

src/app/Library/Uploaders/Support/Traits/HandleRepeatableUploads.php

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model
6363
$value = $value->slice($modelCount, 1)->toArray();
6464

6565
foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
66-
if (array_key_exists($modelCount, $value) && isset($value[$modelCount][$uploader->getName()])) {
67-
$entry->{$uploader->getName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getName()]);
66+
if (array_key_exists($modelCount, $value) && array_key_exists($uploader->getAttributeName(), $value[$modelCount])) {
67+
$entry->{$uploader->getAttributeName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getAttributeName()]);
6868
}
6969
}
7070

@@ -74,10 +74,10 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model
7474
protected function processRepeatableUploads(Model $entry, Collection $values): Collection
7575
{
7676
foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
77-
$uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
77+
$uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
7878

7979
$values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) {
80-
$item[$uploader->getName()] = $uploadedValues[$key] ?? null;
80+
$item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null;
8181

8282
return $item;
8383
});
@@ -89,7 +89,7 @@ protected function processRepeatableUploads(Model $entry, Collection $values): C
8989
private function retrieveRepeatableFiles(Model $entry): Model
9090
{
9191
if ($this->isRelationship) {
92-
return $this->retrieveFiles($entry);
92+
return $this->retrieveRepeatableRelationFiles($entry);
9393
}
9494

9595
$repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName());
@@ -98,7 +98,7 @@ private function retrieveRepeatableFiles(Model $entry): Model
9898
$values = is_string($values) ? json_decode($values, true) : $values;
9999
$values = array_map(function ($item) use ($repeatableUploaders) {
100100
foreach ($repeatableUploaders as $upload) {
101-
$item[$upload->getName()] = $this->getValuesWithPathStripped($item, $upload);
101+
$item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload);
102102
}
103103

104104
return $item;
@@ -109,10 +109,48 @@ private function retrieveRepeatableFiles(Model $entry): Model
109109
return $entry;
110110
}
111111

112+
private function retrieveRepeatableRelationFiles(Model $entry)
113+
{
114+
switch($this->getRepeatableRelationType()) {
115+
case 'BelongsToMany':
116+
case 'MorphToMany':
117+
$pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass();
118+
$pivotFieldName = 'pivot_'.$this->getAttributeName();
119+
$connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]);
120+
$entry->{$pivotFieldName} = $this->retrieveFiles($connectedEntry)->{$this->getAttributeName()};
121+
122+
break;
123+
default:
124+
$entry = $this->retrieveFiles($entry);
125+
}
126+
127+
return $entry;
128+
}
129+
130+
private function getRepeatableRelationType()
131+
{
132+
return $this->getUploaderField()->getAttributes()['relation_type'];
133+
}
134+
135+
private function getUploaderField()
136+
{
137+
return app('crud')->field($this->getRepeatableContainerName());
138+
}
139+
140+
private function getUploaderSubfield()
141+
{
142+
return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->getName())->first();
143+
}
144+
145+
private function getUploaderFieldSubfields()
146+
{
147+
return $this->getUploaderField()->getAttributes()['subfields'];
148+
}
149+
112150
private function deleteRepeatableFiles(Model $entry): void
113151
{
114152
if ($this->isRelationship) {
115-
$this->deleteFiles($entry);
153+
$this->deleteRelationshipFiles($entry);
116154

117155
return;
118156
}
@@ -199,4 +237,42 @@ private function getValuesWithPathStripped(array|string|null $item, UploaderInte
199237

200238
return isset($uploadedValues) ? Str::after($uploadedValues, $upload->getPath()) : null;
201239
}
240+
241+
private function deleteRelationshipFiles(Model $entry): void
242+
{
243+
if (in_array($this->getRepeatableRelationType(), ['BelongsToMany', 'MorphToMany'])) {
244+
$pivotAttributes = $entry->getAttributes();
245+
$connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) {
246+
$itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes));
247+
248+
return $itemPivotAttributes === $pivotAttributes;
249+
})->first();
250+
251+
if (! $connectedPivot) {
252+
return;
253+
}
254+
255+
$files = $connectedPivot->getOriginal()['pivot_'.$this->getAttributeName()];
256+
257+
if (! $files) {
258+
return;
259+
}
260+
261+
if (is_array($files)) {
262+
foreach ($files as $value) {
263+
$value = Str::start($value, $this->getPath());
264+
Storage::disk($this->getDisk())->delete($value);
265+
}
266+
267+
return;
268+
}
269+
270+
$value = Str::start($files, $this->getPath());
271+
Storage::disk($this->getDisk())->delete($value);
272+
273+
return;
274+
}
275+
276+
$this->deleteFiles($entry);
277+
}
202278
}

src/app/Library/Uploaders/Uploader.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ public function storeUploadedFiles(Model $entry): Model
7474
if ($this->attachedToFakeField) {
7575
$fakeFieldValue = $entry->{$this->attachedToFakeField};
7676
$fakeFieldValue = is_string($fakeFieldValue) ? json_decode($fakeFieldValue, true) : (array) $fakeFieldValue;
77-
$fakeFieldValue[$this->getName()] = $this->uploadFiles($entry);
77+
$fakeFieldValue[$this->getAttributeName()] = $this->uploadFiles($entry);
7878

7979
$entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $fakeFieldValue : json_encode($fakeFieldValue);
8080

8181
return $entry;
8282
}
8383

84-
$entry->{$this->getName()} = $this->uploadFiles($entry);
84+
$entry->{$this->getAttributeName()} = $this->uploadFiles($entry);
8585

8686
return $entry;
8787
}
@@ -118,6 +118,11 @@ public function getName(): string
118118
return $this->name;
119119
}
120120

121+
public function getAttributeName(): string
122+
{
123+
return Str::afterLast($this->name, '.');
124+
}
125+
121126
public function getDisk(): string
122127
{
123128
return $this->disk;
@@ -157,6 +162,11 @@ public function canHandleMultipleFiles(): bool
157162
return $this->handleMultipleFiles;
158163
}
159164

165+
public function isRelationship(): bool
166+
{
167+
return $this->isRelationship;
168+
}
169+
160170
public function getPreviousFiles(Model $entry): mixed
161171
{
162172
if (! $this->attachedToFakeField) {
@@ -166,7 +176,7 @@ public function getPreviousFiles(Model $entry): mixed
166176
$value = $this->getOriginalValue($entry, $this->attachedToFakeField);
167177
$value = is_string($value) ? json_decode($value, true) : (array) $value;
168178

169-
return $value[$this->getName()] ?? null;
179+
return $value[$this->getAttributeName()] ?? null;
170180
}
171181

172182
/*******************************
@@ -195,11 +205,11 @@ public function uploadFiles(Model $entry, $values = null)
195205

196206
private function retrieveFiles(Model $entry): Model
197207
{
198-
$value = $entry->{$this->name};
208+
$value = $entry->{$this->getAttributeName()};
199209

200210
if ($this->handleMultipleFiles) {
201-
if (! isset($entry->getCasts()[$this->name]) && is_string($value)) {
202-
$entry->{$this->name} = json_decode($value, true);
211+
if (! isset($entry->getCasts()[$this->getName()]) && is_string($value)) {
212+
$entry->{$this->getAttributeName()} = json_decode($value, true);
203213
}
204214

205215
return $entry;
@@ -209,13 +219,13 @@ private function retrieveFiles(Model $entry): Model
209219
$values = $entry->{$this->attachedToFakeField};
210220
$values = is_string($values) ? json_decode($values, true) : (array) $values;
211221

212-
$values[$this->name] = isset($values[$this->name]) ? Str::after($values[$this->name], $this->path) : null;
222+
$values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? Str::after($values[$this->getAttributeName()], $this->path) : null;
213223
$entry->{$this->attachedToFakeField} = json_encode($values);
214224

215225
return $entry;
216226
}
217227

218-
$entry->{$this->name} = Str::after($value, $this->path);
228+
$entry->{$this->getAttributeName()} = Str::after($value, $this->path);
219229

220230
return $entry;
221231
}
@@ -242,7 +252,7 @@ private function deleteFiles(Model $entry)
242252

243253
private function performFileDeletion(Model $entry)
244254
{
245-
if ($this->isRelationship || ! $this->handleRepeatableFiles) {
255+
if (! $this->handleRepeatableFiles) {
246256
$this->deleteFiles($entry);
247257

248258
return;
@@ -263,13 +273,13 @@ private function getPathFromConfiguration(array $crudObject, array $configuratio
263273

264274
private function getOriginalValue(Model $entry, $field = null)
265275
{
266-
$previousValue = $entry->getOriginal($field ?? $this->getName());
276+
$previousValue = $entry->getOriginal($field ?? $this->getAttributeName());
267277

268278
if (! $previousValue) {
269279
return $previousValue;
270280
}
271281

272-
if ($entry->translationEnabled()) {
282+
if (method_exists($entry, 'translationEnabled') && $entry->translationEnabled()) {
273283
return $previousValue[$entry->getLocale()] ?? null;
274284
}
275285

0 commit comments

Comments
 (0)