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
27:37 queued 12:39
created

HandleRepeatableUploads::handleRepeatableFiles()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 9
eloc 25
c 2
b 1
f 0
nc 21
nop 1
dl 0
loc 40
rs 8.0555
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\Support\Collection;
9
use Illuminate\Support\Facades\Log;
10
use Illuminate\Support\Facades\Storage;
11
use Illuminate\Support\Str;
12
13
/**
14
 * @codeCoverageIgnore
15
 */
16
trait HandleRepeatableUploads
17
{
18
    public bool $handleRepeatableFiles = false;
19
20
    public null|string $repeatableContainerName = null;
21
22
    /*******************************
23
     * Setters - fluently configure the uploader
24
     *******************************/
25
    public function repeats(string $repeatableContainerName): self
26
    {
27
        $this->handleRepeatableFiles = true;
28
29
        $this->repeatableContainerName = $repeatableContainerName;
30
31
        return $this;
32
    }
33
34
    /*******************************
35
     * Getters
36
     *******************************/
37
    public function getRepeatableContainerName(): null|string
38
    {
39
        return $this->repeatableContainerName;
40
    }
41
42
    /*******************************
43
     * Default implementation methods
44
     *******************************/
45
    protected function uploadRepeatableFiles($values, $previousValues, $entry = null)
46
    {
47
    }
48
49
    protected function handleRepeatableFiles(Model $entry): Model
50
    {
51
        if ($this->isRelationship) {
52
            return $this->processRelationshipRepeatableUploaders($entry);
53
        }
54
55
        $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

55
        $values = collect(CRUD::/** @scrutinizer ignore-call */ getRequest()->get($this->getRepeatableContainerName()));
Loading history...
56
        $files = collect(CRUD::getRequest()->file($this->getRepeatableContainerName()));
57
58
        $value = $this->mergeValuesRecursive($values, $files);
59
60
        $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

60
        $processedEntryValues = $this->processRepeatableUploads($entry, /** @scrutinizer ignore-type */ $value);
Loading history...
61
62
        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

62
        if ($this->/** @scrutinizer ignore-call */ isFake()) {
Loading history...
63
            $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

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

113
            /** @scrutinizer ignore-call */ 
114
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
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

113
            $entry->{$this->/** @scrutinizer ignore-call */ getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
114
            $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...
115
        }
116
117
        if ($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) {
118
            $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry);
119
120
            return $entry;
121
        }
122
123
        if ($this->shouldUploadFiles($entryValue)) {
124
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue);
125
        }
126
127
        return $entry;
128
    }
129
130
    protected function getFilesFromEntry(Model $entry)
131
    {
132
        return $entry->getAttribute($this->getAttributeName());
133
    }
134
135
    protected function getEntryAttributeValue(Model $entry)
136
    {
137
        return $entry->{$this->getAttributeName()};
138
    }
139
140
    protected function getEntryOriginalValue(Model $entry)
141
    {
142
        return $entry->getOriginal($this->getAttributeName());
143
    }
144
145
    protected function shouldUploadFiles($entryValue): bool
146
    {
147
        return true;
148
    }
149
150
    protected function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool
151
    {
152
        return $entry->exists && ($entryValue === null || $entryValue === [null]);
153
    }
154
155
    protected function hasDeletedFiles($entryValue): bool
156
    {
157
        return $entryValue === false || $entryValue === null || $entryValue === [null];
158
    }
159
160
    protected function processRepeatableUploads(Model $entry, Collection $values): array
161
    {
162
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
163
            $uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
164
165
            $values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) {
166
                $item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null;
167
168
                return $item;
169
            });
170
        }
171
172
        return $values->toArray();
173
    }
174
175
    private function retrieveRepeatableFiles(Model $entry): Model
176
    {
177
        if ($this->isRelationship) {
178
            return $this->retrieveRepeatableRelationFiles($entry);
179
        }
180
181
        $repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName());
182
183
        if ($this->attachedToFakeField) {
184
            $values = $entry->{$this->attachedToFakeField};
185
186
            $values = is_string($values) ? json_decode($values, true) : $values;
187
188
            $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

188
            $values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? $this->/** @scrutinizer ignore-call */ getValueWithoutPath($values[$this->getAttributeName()]) : null;
Loading history...
189
            $entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $values : json_encode($values);
190
191
            return $entry;
192
        }
193
194
        $values = $entry->{$this->getRepeatableContainerName()};
195
        $values = is_string($values) ? json_decode($values, true) : $values;
196
        $values = array_map(function ($item) use ($repeatableUploaders) {
197
            foreach ($repeatableUploaders as $upload) {
198
                $item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload);
199
            }
200
201
            return $item;
202
        }, $values ?? []);
203
204
        $entry->{$this->getRepeatableContainerName()} = $values;
205
206
        return $entry;
207
    }
208
209
    private function retrieveRepeatableRelationFiles(Model $entry)
210
    {
211
        switch($this->getRepeatableRelationType()) {
212
            case 'BelongsToMany':
213
            case 'MorphToMany':
214
                $pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass();
215
                $pivotFieldName = 'pivot_'.$this->getAttributeName();
216
                $connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]);
217
                $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

217
                $entry->{$pivotFieldName} = $this->/** @scrutinizer ignore-call */ retrieveFiles($connectedEntry)->{$this->getAttributeName()};
Loading history...
218
219
                break;
220
            default:
221
                $entry = $this->retrieveFiles($entry);
222
        }
223
224
        return $entry;
225
    }
226
227
    private function getRepeatableRelationType()
228
    {
229
        return $this->getUploaderField()->getAttributes()['relation_type'];
230
    }
231
232
    private function getUploaderField()
233
    {
234
        return app('crud')->field($this->getRepeatableContainerName());
235
    }
236
237
    private function getUploaderSubfield()
238
    {
239
        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

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

388
                    $value = Str::start($value, $this->/** @scrutinizer ignore-call */ getPath());
Loading history...
389
                    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

389
                    Storage::disk($this->/** @scrutinizer ignore-call */ getDisk())->delete($value);
Loading history...
390
                }
391
392
                return;
393
            }
394
395
            $value = Str::start($files, $this->getPath());
396
            Storage::disk($this->getDisk())->delete($value);
397
398
            return;
399
        }
400
401
        $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

401
        $this->/** @scrutinizer ignore-call */ 
402
               deleteFiles($entry);
Loading history...
402
    }
403
}
404