Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — main (#5478)
by Cristian
26:33 queued 12:14
created

deleteRepeatableRelationFiles()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 5
eloc 15
c 2
b 1
f 0
nc 5
nop 1
dl 0
loc 27
rs 9.4555
1
<?php
2
3
namespace Backpack\CRUD\app\Library\Uploaders\Support\Traits;
4
5
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
6
use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\Relations\Pivot;
9
use Illuminate\Support\Collection;
10
use Illuminate\Support\Facades\Log;
11
use Illuminate\Support\Facades\Storage;
12
use Illuminate\Support\Str;
13
14
/**
15
 * @codeCoverageIgnore
16
 */
17
trait HandleRepeatableUploads
18
{
19
    public bool $handleRepeatableFiles = false;
20
21
    public null|string $repeatableContainerName = null;
22
23
    /*******************************
24
     * Setters - fluently configure the uploader
25
     *******************************/
26
    public function repeats(string $repeatableContainerName): self
27
    {
28
        $this->handleRepeatableFiles = true;
29
30
        $this->repeatableContainerName = $repeatableContainerName;
31
32
        return $this;
33
    }
34
35
    /*******************************
36
     * Getters
37
     *******************************/
38
    public function getRepeatableContainerName(): null|string
39
    {
40
        return $this->repeatableContainerName;
41
    }
42
43
    /*******************************
44
     * Default implementation methods
45
     *******************************/
46
    protected function uploadRepeatableFiles($values, $previousValues, $entry = null)
47
    {
48
    }
49
50
    protected function handleRepeatableFiles(Model $entry): Model
51
    {
52
        if ($this->isRelationship) {
53
            return $this->processRelationshipRepeatableUploaders($entry);
54
        }
55
56
        $values = collect(CRUD::getRequest()->get($this->getRepeatableContainerName()));
0 ignored issues
show
Bug introduced by
The method getRequest() does not exist on Backpack\CRUD\app\Librar...udPanel\CrudPanelFacade. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

56
        $values = collect(CRUD::/** @scrutinizer ignore-call */ getRequest()->get($this->getRepeatableContainerName()));
Loading history...
57
        $files = collect(CRUD::getRequest()->file($this->getRepeatableContainerName()));
58
59
        $value = $this->mergeValuesRecursive($values, $files);
60
61
        $processedEntryValues = $this->processRepeatableUploads($entry, $value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type array; however, parameter $values of Backpack\CRUD\app\Librar...cessRepeatableUploads() does only seem to accept Illuminate\Support\Collection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

61
        $processedEntryValues = $this->processRepeatableUploads($entry, /** @scrutinizer ignore-type */ $value);
Loading history...
62
63
        if ($this->isFake()) {
0 ignored issues
show
Bug introduced by
It seems like isFake() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

63
        if ($this->/** @scrutinizer ignore-call */ isFake()) {
Loading history...
64
            $fakeValues = $entry->{$this->getFakeAttribute()} ?? [];
0 ignored issues
show
Bug introduced by
It seems like getFakeAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

64
            $fakeValues = $entry->{$this->/** @scrutinizer ignore-call */ getFakeAttribute()} ?? [];
Loading history...
65
66
            if (is_string($fakeValues)) {
67
                $fakeValues = json_decode($fakeValues, true);
68
            }
69
70
            $fakeValues[$this->getRepeatableContainerName()] = empty($processedEntryValues)
71
                                                        ? null
72
                                                        : (isset($entry->getCasts()[$this->getFakeAttribute()])
73
                                                            ? $processedEntryValues
74
                                                            : json_encode($processedEntryValues));
75
76
            $entry->{$this->getFakeAttribute()} = isset($entry->getCasts()[$this->getFakeAttribute()])
77
                                                            ? $fakeValues
78
                                                            : json_encode($fakeValues);
79
80
            return $entry;
81
        }
82
83
        $entry->{$this->getRepeatableContainerName()} = empty($processedEntryValues)
84
                                                        ? null
85
                                                        : (isset($entry->getCasts()[$this->getRepeatableContainerName()])
86
                                                            ? $processedEntryValues
87
                                                            : json_encode($processedEntryValues));
88
89
        return $entry;
90
    }
91
92
    private function processRelationshipRepeatableUploaders(Model $entry)
93
    {
94
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
95
            $entry = $uploader->uploadRelationshipFiles($entry);
96
        }
97
98
        return $entry;
99
    }
100
101
    protected function uploadRelationshipFiles(Model $entry): Model
102
    {
103
        $entryValue = $this->getFilesFromEntry($entry);
104
105
        if ($this->handleMultipleFiles && is_string($entryValue)) {
106
            try {
107
                $entryValue = json_decode($entryValue, true);
108
            } catch (\Exception) {
109
                return $entry;
110
            }
111
        }
112
113
        if ($this->hasDeletedFiles($entryValue)) {
114
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false);
0 ignored issues
show
Bug introduced by
It seems like getAttributeName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

114
            $entry->{$this->/** @scrutinizer ignore-call */ getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
Bug introduced by
It seems like uploadFiles() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

114
            /** @scrutinizer ignore-call */ 
115
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
115
            $this->updatedPreviousFiles = $this->getEntryAttributeValue($entry);
0 ignored issues
show
Bug Best Practice introduced by
The property updatedPreviousFiles does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
116
        }
117
118
        if ($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) {
119
            $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry);
120
121
            return $entry;
122
        }
123
124
        if ($this->shouldUploadFiles($entryValue)) {
125
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue);
126
        }
127
128
        return $entry;
129
    }
130
131
    protected function getFilesFromEntry(Model $entry)
132
    {
133
        return $entry->getAttribute($this->getAttributeName());
134
    }
135
136
    protected function getEntryAttributeValue(Model $entry)
137
    {
138
        return $entry->{$this->getAttributeName()};
139
    }
140
141
    protected function getEntryOriginalValue(Model $entry)
142
    {
143
        return $entry->getOriginal($this->getAttributeName());
144
    }
145
146
    protected function shouldUploadFiles($entryValue): bool
147
    {
148
        return true;
149
    }
150
151
    protected function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool
152
    {
153
        return $entry->exists && ($entryValue === null || $entryValue === [null]);
154
    }
155
156
    protected function hasDeletedFiles($entryValue): bool
157
    {
158
        return $entryValue === false || $entryValue === null || $entryValue === [null];
159
    }
160
161
    protected function processRepeatableUploads(Model $entry, Collection $values): array
162
    {
163
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
164
            $uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
165
166
            $values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) {
167
                $item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null;
168
169
                return $item;
170
            });
171
        }
172
173
        return $values->toArray();
174
    }
175
176
    private function retrieveRepeatableFiles(Model $entry): Model
177
    {
178
        if ($this->isRelationship) {
179
            return $this->retrieveRepeatableRelationFiles($entry);
180
        }
181
182
        $repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName());
183
184
        if ($this->attachedToFakeField) {
185
            $values = $entry->{$this->attachedToFakeField};
186
187
            $values = is_string($values) ? json_decode($values, true) : $values;
188
189
            $values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? $this->getValueWithoutPath($values[$this->getAttributeName()]) : null;
0 ignored issues
show
Bug introduced by
It seems like getValueWithoutPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

189
            $values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? $this->/** @scrutinizer ignore-call */ getValueWithoutPath($values[$this->getAttributeName()]) : null;
Loading history...
190
            $entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $values : json_encode($values);
191
192
            return $entry;
193
        }
194
195
        $values = $entry->{$this->getRepeatableContainerName()};
196
        $values = is_string($values) ? json_decode($values, true) : $values;
197
        $values = array_map(function ($item) use ($repeatableUploaders) {
198
            foreach ($repeatableUploaders as $upload) {
199
                $item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload);
200
            }
201
202
            return $item;
203
        }, $values ?? []);
204
205
        $entry->{$this->getRepeatableContainerName()} = $values;
206
207
        return $entry;
208
    }
209
210
    private function retrieveRepeatableRelationFiles(Model $entry)
211
    {
212
        switch($this->getRepeatableRelationType()) {
213
            case 'BelongsToMany':
214
            case 'MorphToMany':
215
                $pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass();
216
                $pivotFieldName = 'pivot_'.$this->getAttributeName();
217
                $connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]);
218
                $entry->{$pivotFieldName} = $this->retrieveFiles($connectedEntry)->{$this->getAttributeName()};
0 ignored issues
show
Bug introduced by
It seems like retrieveFiles() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

218
                $entry->{$pivotFieldName} = $this->/** @scrutinizer ignore-call */ retrieveFiles($connectedEntry)->{$this->getAttributeName()};
Loading history...
219
220
                break;
221
            default:
222
                $entry = $this->retrieveFiles($entry);
223
        }
224
225
        return $entry;
226
    }
227
228
    private function getRepeatableRelationType()
229
    {
230
        return $this->getUploaderField()->getAttributes()['relation_type'];
231
    }
232
233
    private function getUploaderField()
234
    {
235
        return app('crud')->field($this->getRepeatableContainerName());
236
    }
237
238
    private function getUploaderSubfield()
239
    {
240
        return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->getName())->first();
0 ignored issues
show
Bug introduced by
It seems like getName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

240
        return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->/** @scrutinizer ignore-call */ getName())->first();
Loading history...
241
    }
242
243
    private function getUploaderFieldSubfields()
244
    {
245
        return $this->getUploaderField()->getAttributes()['subfields'];
246
    }
247
248
    private function deleteRepeatableFiles(Model $entry): void
249
    {
250
        if ($this->isRelationship) {
251
            $this->deleteRelationshipFiles($entry);
252
253
            return;
254
        }
255
256
        if ($this->attachedToFakeField) {
257
            $repeatableValues = $entry->{$this->attachedToFakeField}[$this->getRepeatableContainerName()] ?? null;
258
            $repeatableValues = is_string($repeatableValues) ? json_decode($repeatableValues, true) : $repeatableValues;
259
            $repeatableValues = collect($repeatableValues);
260
        }
261
262
        $repeatableValues ??= collect($entry->{$this->getRepeatableContainerName()});
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $repeatableValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
263
264
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $upload) {
265
            if (! $upload->shouldDeleteFiles()) {
266
                continue;
267
            }
268
            $values = $repeatableValues->pluck($upload->getName())->toArray();
269
            foreach ($values as $value) {
270
                if (! $value) {
271
                    continue;
272
                }
273
274
                if (is_array($value)) {
275
                    foreach ($value as $subvalue) {
276
                        Storage::disk($upload->getDisk())->delete($upload->getPath().$subvalue);
277
                    }
278
279
                    continue;
280
                }
281
282
                Storage::disk($upload->getDisk())->delete($upload->getPath().$value);
283
            }
284
        }
285
    }
286
    /*******************************
287
     * Helper methods
288
     *******************************/
289
290
    /**
291
     * Given two multidimensional arrays/collections, merge them recursively.
292
     */
293
    protected function mergeValuesRecursive(array|Collection $array1, array|Collection $array2): array|Collection
294
    {
295
        $merged = $array1;
296
        foreach ($array2 as $key => &$value) {
297
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
298
                $merged[$key] = $this->mergeValuesRecursive($merged[$key], $value);
299
            } else {
300
                $merged[$key] = $value;
301
            }
302
        }
303
304
        return $merged;
305
    }
306
307
    /**
308
     * Repeatable items send `_order_` parameter in the request.
309
     * This holds the order of the items in the repeatable container.
310
     */
311
    protected function getFileOrderFromRequest(): array
312
    {
313
        $items = CRUD::getRequest()->input('_order_'.$this->getRepeatableContainerName()) ?? [];
314
315
        array_walk($items, function (&$key, $value) {
316
            $requestValue = $key[$this->getName()] ?? null;
317
            $key = $this->handleMultipleFiles ? (is_string($requestValue) ? explode(',', $requestValue) : $requestValue) : $requestValue;
318
        });
319
320
        return $items;
321
    }
322
323
    private function getPreviousRepeatableValues(Model $entry, UploaderInterface $uploader): array
324
    {
325
        $previousValues = $entry->getOriginal($uploader->getRepeatableContainerName());
326
327
        if (! is_array($previousValues)) {
328
            $previousValues = json_decode($previousValues, true);
329
        }
330
331
        if (! empty($previousValues)) {
332
            $previousValues = array_column($previousValues, $uploader->getName());
333
        }
334
335
        return $previousValues ?? [];
336
    }
337
338
    private function getValuesWithPathStripped(array|string|null $item, UploaderInterface $uploader)
339
    {
340
        $uploadedValues = $item[$uploader->getName()] ?? null;
341
        if (is_array($uploadedValues)) {
342
            return array_map(function ($value) use ($uploader) {
343
                return $uploader->getValueWithoutPath($value);
344
            }, $uploadedValues);
345
        }
346
347
        return isset($uploadedValues) ? $uploader->getValueWithoutPath($uploadedValues) : null;
348
    }
349
350
    private function deleteRelationshipFiles(Model $entry): void
351
    {
352
        if (! is_a($entry, Pivot::class, true)) {
353
            $entry->loadMissing($this->getRepeatableContainerName());
354
        }
355
356
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
357
            if ($uploader->shouldDeleteFiles()) {
358
                $uploader->deleteRepeatableRelationFiles($entry);
359
            }
360
        }
361
    }
362
363
    protected function deleteRepeatableRelationFiles(Model $entry)
364
    {
365
        if (in_array($this->getRepeatableRelationType(), ['BelongsToMany', 'MorphToMany'])) {
366
            if (! is_a($entry, Pivot::class, true)) {
367
                $pivots = $entry->{$this->getRepeatableContainerName()};
368
                foreach ($pivots as $pivot) {
369
                    $this->deletePivotModelFiles($pivot);
370
                }
371
372
                return;
373
            }
374
375
            $pivotAttributes = $entry->getAttributes();
376
            $connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) {
377
                $itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes));
378
379
                return $itemPivotAttributes === $pivotAttributes;
380
            })->first();
381
382
            if (! $connectedPivot) {
383
                return;
384
            }
385
386
            $this->deletePivotModelFiles($connectedPivot);
387
        }
388
389
        $this->deleteFiles($entry);
0 ignored issues
show
Bug introduced by
It seems like deleteFiles() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
        $this->/** @scrutinizer ignore-call */ 
390
               deleteFiles($entry);
Loading history...
390
    }
391
392
    private function deletePivotModelFiles(Pivot|Model $entry)
393
    {
394
        $files = $entry->getOriginal()['pivot_'.$this->getAttributeName()];
395
396
        if (! $files) {
397
            return;
398
        }
399
400
        if ($this->handleMultipleFiles && is_string($files)) {
401
            try {
402
                $files = json_decode($files, true);
403
            } catch (\Exception) {
404
                Log::error('Could not parse files for deletion pivot entry with key: '.$entry->getKey().' and uploader: '.$this->getName());
405
406
                return;
407
            }
408
        }
409
410
        if (is_array($files)) {
411
            foreach ($files as $value) {
412
                $value = Str::start($value, $this->getPath());
0 ignored issues
show
Bug introduced by
It seems like getPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

412
                $value = Str::start($value, $this->/** @scrutinizer ignore-call */ getPath());
Loading history...
413
                Storage::disk($this->getDisk())->delete($value);
0 ignored issues
show
Bug introduced by
It seems like getDisk() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

413
                Storage::disk($this->/** @scrutinizer ignore-call */ getDisk())->delete($value);
Loading history...
414
            }
415
416
            return;
417
        }
418
419
        $value = Str::start($files, $this->getPath());
420
        Storage::disk($this->getDisk())->delete($value);
421
    }
422
}
423