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 (#5518)
by Pedro
34:25 queued 23:25
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
trait HandleRepeatableUploads
14
{
15
    public bool $handleRepeatableFiles = false;
16
17
    public null|string $repeatableContainerName = null;
18
19
    /*******************************
20
     * Setters - fluently configure the uploader
21
     *******************************/
22
    public function repeats(string $repeatableContainerName): self
23
    {
24
        $this->handleRepeatableFiles = true;
25
26
        $this->repeatableContainerName = $repeatableContainerName;
27
28
        return $this;
29
    }
30
31
    /*******************************
32
     * Getters
33
     *******************************/
34
    public function getRepeatableContainerName(): null|string
35
    {
36
        return $this->repeatableContainerName;
37
    }
38
39
    /*******************************
40
     * Default implementation methods
41
     *******************************/
42
    protected function uploadRepeatableFiles($values, $previousValues, $entry = null)
43
    {
44
    }
45
46
    protected function handleRepeatableFiles(Model $entry): Model
47
    {
48
        if ($this->isRelationship) {
49
            return $this->processRelationshipRepeatableUploaders($entry);
50
        }
51
52
        $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

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

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

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

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

110
            $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

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

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

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

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

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

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

398
        $this->/** @scrutinizer ignore-call */ 
399
               deleteFiles($entry);
Loading history...
399
    }
400
}
401