Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Push — fix-uploaders ( 8aa075...c768c9 )
by Pedro
12:43
created

HandleRepeatableUploads::handleRepeatableFiles()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 9
eloc 25
c 2
b 1
f 0
nc 21
nop 1
dl 0
loc 39
rs 8.0555
1
<?php
2
3
namespace Backpack\CRUD\app\Library\Uploaders\Support\Traits;
4
5
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
6
use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Support\Collection;
9
use Illuminate\Support\Facades\Log;
10
use Illuminate\Support\Facades\Storage;
11
use Illuminate\Support\Str;
12
13
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
            return $entry;
76
        }
77
78
        $entry->{$this->getRepeatableContainerName()} = empty($processedEntryValues)
79
                                                        ? null
80
                                                        : (isset($entry->getCasts()[$this->getRepeatableContainerName()])
81
                                                            ? $processedEntryValues
82
                                                            : json_encode($processedEntryValues));
83
84
        return $entry;
85
    }
86
87
    private function processRelationshipRepeatableUploaders(Model $entry)
88
    {
89
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
90
            $entry = $uploader->uploadRelationshipFiles($entry);
91
        }
92
93
        return $entry;
94
    }
95
96
    protected function uploadRelationshipFiles(Model $entry): Model
97
    {
98
        $entryValue = $this->getFilesFromEntry($entry);
99
100
        if ($this->handleMultipleFiles && is_string($entryValue)) {
101
            try {
102
                $entryValue = json_decode($entryValue, true);
103
            } catch (\Exception) {
104
                return $entry;
105
            }
106
        }
107
108
        if ($this->hasDeletedFiles($entryValue)) {
109
            $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

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

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

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

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

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

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

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

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