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
27:58
created

HandleRepeatableUploads::deletePivotModelFiles()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 16
c 2
b 1
f 0
nc 8
nop 1
dl 0
loc 29
rs 8.8333
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
        $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
        if ($this->isRelationship()) {
0 ignored issues
show
Bug introduced by
It seems like isRelationship() 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

57
        if ($this->/** @scrutinizer ignore-call */ isRelationship()) {
Loading history...
58
            if ($value->isEmpty()) {
59
                return $entry;
60
            }
61
62
            return $this->processRelationshipRepeatableUploaders($entry);
63
        }
64
65
        $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

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

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

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

118
            /** @scrutinizer ignore-call */ 
119
            $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

118
            $entry->{$this->/** @scrutinizer ignore-call */ getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
119
            $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...
120
        }
121
122
        if ($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) {
0 ignored issues
show
Bug introduced by
It seems like shouldKeepPreviousValueUnchanged() 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

122
        if ($this->/** @scrutinizer ignore-call */ shouldKeepPreviousValueUnchanged($entry, $entryValue)) {
Loading history...
123
            $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry);
124
125
            return $entry;
126
        }
127
128
        if ($this->shouldUploadFiles($entryValue)) {
129
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue);
130
        }
131
132
        return $entry;
133
    }
134
135
    protected function getFilesFromEntry(Model $entry)
136
    {
137
        return $entry->getAttribute($this->getAttributeName());
138
    }
139
140
    protected function getEntryAttributeValue(Model $entry)
141
    {
142
        return $entry->{$this->getAttributeName()};
143
    }
144
145
    protected function getEntryOriginalValue(Model $entry)
146
    {
147
        return $entry->getOriginal($this->getAttributeName());
148
    }
149
150
    protected function shouldUploadFiles($entryValue): bool
151
    {
152
        return true;
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
        if (! is_a($entry, Pivot::class, true) &&
352
            ! $entry->relationLoaded($this->getRepeatableContainerName()) &&
353
            method_exists($entry, $this->getRepeatableContainerName())
0 ignored issues
show
Bug introduced by
It seems like $this->getRepeatableContainerName() can also be of type null; however, parameter $method of method_exists() does only seem to accept string, 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

353
            method_exists($entry, /** @scrutinizer ignore-type */ $this->getRepeatableContainerName())
Loading history...
354
        ) {
355
            $entry->loadMissing($this->getRepeatableContainerName());
356
        }
357
358
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
359
            if ($uploader->shouldDeleteFiles()) {
360
                $uploader->deleteRepeatableRelationFiles($entry);
361
            }
362
        }
363
    }
364
365
    protected function deleteRepeatableRelationFiles(Model $entry)
366
    {
367
        match ($this->getRepeatableRelationType()) {
368
            'BelongsToMany', 'MorphToMany' => $this->deletePivotFiles($entry),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->deletePivotFiles($entry) targeting Backpack\CRUD\app\Librar...ads::deletePivotFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
369
            default => $this->deleteRelatedFiles($entry),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->deleteRelatedFiles($entry) targeting Backpack\CRUD\app\Librar...s::deleteRelatedFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
370
        };
371
    }
372
373
    private function deleteRelatedFiles(Model $entry)
374
    {
375
        if (get_class($entry) === get_class(app('crud')->model)) {
0 ignored issues
show
Bug introduced by
app('crud')->model of type string is incompatible with the type object expected by parameter $object of get_class(). ( Ignorable by Annotation )

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

375
        if (get_class($entry) === get_class(/** @scrutinizer ignore-type */ app('crud')->model)) {
Loading history...
376
            $relatedEntries = $entry->{$this->getRepeatableContainerName()} ?? [];
377
        }
378
379
        if (! is_a($relatedEntries ?? '', Collection::class, true)) {
380
            $relatedEntries = ! empty($relatedEntries) ? [$relatedEntries] : [$entry];
381
        }
382
383
        foreach ($relatedEntries as $relatedEntry) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $relatedEntries does not seem to be defined for all execution paths leading up to this point.
Loading history...
384
            $this->deleteFiles($relatedEntry);
0 ignored issues
show
Bug introduced by
The method deleteFiles() does not exist on Backpack\CRUD\app\Librar...HandleRepeatableUploads. Did you maybe mean deletePivotFiles()? ( Ignorable by Annotation )

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

384
            $this->/** @scrutinizer ignore-call */ 
385
                   deleteFiles($relatedEntry);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
385
        }
386
    }
387
388
    protected function deletePivotFiles(Pivot|Model $entry)
389
    {
390
        if (! is_a($entry, Pivot::class, true)) {
391
            $pivots = $entry->{$this->getRepeatableContainerName()};
392
            foreach ($pivots as $pivot) {
393
                $this->deletePivotModelFiles($pivot);
394
            }
395
396
            return;
397
        }
398
399
        $pivotAttributes = $entry->getAttributes();
400
        $connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) {
401
            $itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes));
402
403
            return $itemPivotAttributes === $pivotAttributes;
404
        })->first();
405
406
        if (! $connectedPivot) {
407
            return;
408
        }
409
410
        $this->deletePivotModelFiles($connectedPivot);
411
    }
412
413
    private function deletePivotModelFiles(Pivot|Model $entry)
414
    {
415
        $files = $entry->getOriginal()['pivot_'.$this->getAttributeName()];
416
417
        if (! $files) {
418
            return;
419
        }
420
421
        if ($this->handleMultipleFiles && is_string($files)) {
422
            try {
423
                $files = json_decode($files, true);
424
            } catch (\Exception) {
425
                Log::error('Could not parse files for deletion pivot entry with key: '.$entry->getKey().' and uploader: '.$this->getName());
426
427
                return;
428
            }
429
        }
430
431
        if (is_array($files)) {
432
            foreach ($files as $value) {
433
                $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

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

434
                Storage::disk($this->/** @scrutinizer ignore-call */ getDisk())->delete($value);
Loading history...
435
            }
436
437
            return;
438
        }
439
440
        $value = Str::start($files, $this->getPath());
441
        Storage::disk($this->getDisk())->delete($value);
442
    }
443
}
444