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 Cristian
34:13 queued 19:11
created

processRelationshipRepeatableUploaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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