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

Test Failed
Pull Request — main (#5478)
by Pedro
26:53
created

HandleRepeatableUploads::retrieveRepeatableFiles()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 4 Features 0
Metric Value
cc 8
eloc 18
c 4
b 4
f 0
nc 11
nop 1
dl 0
loc 32
rs 8.4444
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