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-uploads-in-relationship-re... ( 29b535 )
by Pedro
14:55
created

HandleRepeatableUploads   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Importance

Changes 13
Bugs 11 Features 0
Metric Value
eloc 137
dl 0
loc 336
rs 2.88
c 13
b 11
f 0
wmc 69

26 Methods

Rating   Name   Duplication   Size   Complexity  
B uploadRelationshipFiles() 0 27 7
A hasDeletedFiles() 0 3 3
A getUploaderSubfield() 0 3 1
A getFileOrderFromRequest() 0 10 3
A handleRepeatableFiles() 0 13 2
A uploadRepeatableFiles() 0 2 1
B deleteRepeatableRelationFiles() 0 46 9
A getValuesWithPathStripped() 0 10 3
A retrieveRepeatableRelationFiles() 0 16 3
A getRepeatableRelationType() 0 3 1
A getUploaderFieldSubfields() 0 3 1
A getUploaderField() 0 3 1
A getPreviousRepeatableValues() 0 9 2
A deleteRelationshipFiles() 0 4 2
A mergeValuesRecursive() 0 12 5
A processRelationshipRepeatableUploaders() 0 7 2
B deleteRepeatableFiles() 0 28 8
A shouldKeepPreviousValueUnchanged() 0 3 3
A getEntryAttributeValue() 0 3 1
A getEntryOriginalValue() 0 3 1
A shouldUploadFiles() 0 3 1
A retrieveRepeatableFiles() 0 21 4
A repeats() 0 7 1
A processRepeatableUploads() 0 13 2
A getRepeatableContainerName() 0 3 1
A getFilesFromEntry() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like HandleRepeatableUploads often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HandleRepeatableUploads, and based on these observations, apply Extract Interface, too.

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
        $value = $this->mergeValuesRecursive($values, $files);
55
56
        $entry->{$this->getRepeatableContainerName()} = json_encode($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

56
        $entry->{$this->getRepeatableContainerName()} = json_encode($this->processRepeatableUploads($entry, /** @scrutinizer ignore-type */ $value));
Loading history...
57
58
        return $entry;
59
    }
60
61
    private function processRelationshipRepeatableUploaders(Model $entry)
62
    {
63
        foreach(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
64
            $entry = $uploader->uploadRelationshipFiles($entry);
65
        }
66
67
        return $entry;
68
    }
69
70
    protected function uploadRelationshipFiles(Model $entry): Model
71
    {
72
        $entryValue = $this->getFilesFromEntry($entry);
73
        
74
        if($this->handleMultipleFiles && is_string($entryValue)) {
75
            try {
76
                $entryValue = json_decode($entryValue, true);
77
            } catch (\Exception) {
78
               return $entry;
79
            }
80
        }
81
82
        if ($this->hasDeletedFiles($entryValue)) {
83
            $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

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

83
            /** @scrutinizer ignore-call */ 
84
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false);
Loading history...
84
            $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...
85
        }
86
87
        if($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) {
88
            $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry);
89
            return $entry;
90
        }
91
92
        if($this->shouldUploadFiles($entryValue)) {
93
            $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue);
94
        }
95
96
        return $entry;
97
    }
98
99
    protected function getFilesFromEntry(Model $entry)
100
    {
101
        return $entry->getAttribute($this->getAttributeName());
102
    }
103
104
    protected function getEntryAttributeValue(Model $entry)
105
    {
106
        return $entry->{$this->getAttributeName()};
107
    }
108
109
    protected function getEntryOriginalValue(Model $entry)
110
    {
111
        return $entry->getOriginal($this->getAttributeName());
112
    }
113
114
    protected function shouldUploadFiles($entryValue): bool
115
    {
116
        return true;
117
    }
118
119
    protected function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool
120
    {
121
        return $entry->exists && ($entryValue === null || $entryValue === [null]);
122
    }
123
124
    protected function hasDeletedFiles($entryValue): bool
125
    {
126
        return $entryValue === false || $entryValue === null || $entryValue === [null];
127
    }
128
    
129
    protected function processRepeatableUploads(Model $entry, Collection $values): Collection
130
    {
131
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
132
            $uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
133
134
            $values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) {
135
                $item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null;
136
137
                return $item;
138
            });
139
        }
140
141
        return $values;
142
    }
143
144
    private function retrieveRepeatableFiles(Model $entry): Model
145
    {
146
        if ($this->isRelationship) {
147
            return $this->retrieveRepeatableRelationFiles($entry);
148
        }
149
150
        $repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName());
151
152
        $values = $entry->{$this->getRepeatableContainerName()};
153
        $values = is_string($values) ? json_decode($values, true) : $values;
154
        $values = array_map(function ($item) use ($repeatableUploaders) {
155
            foreach ($repeatableUploaders as $upload) {
156
                $item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload);
157
            }
158
159
            return $item;
160
        }, $values);
161
162
        $entry->{$this->getRepeatableContainerName()} = $values;
163
164
        return $entry;
165
    }
166
167
    private function retrieveRepeatableRelationFiles(Model $entry)
168
    {
169
        switch($this->getRepeatableRelationType()) {
170
            case 'BelongsToMany':
171
            case 'MorphToMany':
172
                $pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass();
173
                $pivotFieldName = 'pivot_'.$this->getAttributeName();
174
                $connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]);
175
                $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

175
                $entry->{$pivotFieldName} = $this->/** @scrutinizer ignore-call */ retrieveFiles($connectedEntry)->{$this->getAttributeName()};
Loading history...
176
177
                break;
178
            default:
179
                $entry = $this->retrieveFiles($entry);
180
        }
181
182
        return $entry;
183
    }
184
185
    private function getRepeatableRelationType()
186
    {
187
        return $this->getUploaderField()->getAttributes()['relation_type'];
188
    }
189
190
    private function getUploaderField()
191
    {
192
        return app('crud')->field($this->getRepeatableContainerName());
193
    }
194
195
    private function getUploaderSubfield()
196
    {
197
        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

197
        return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->/** @scrutinizer ignore-call */ getName())->first();
Loading history...
198
    }
199
200
    private function getUploaderFieldSubfields()
201
    {
202
        return $this->getUploaderField()->getAttributes()['subfields'];
203
    }
204
205
    private function deleteRepeatableFiles(Model $entry): void
206
    {
207
        if ($this->isRelationship) {
208
            $this->deleteRelationshipFiles($entry);
209
210
            return;
211
        }
212
213
        $repeatableValues = collect($entry->{$this->getName()});
214
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $upload) {
215
            if (! $upload->shouldDeleteFiles()) {
216
                continue;
217
            }
218
            $values = $repeatableValues->pluck($upload->getName())->toArray();
219
            foreach ($values as $value) {
220
                if (! $value) {
221
                    continue;
222
                }
223
224
                if (is_array($value)) {
225
                    foreach ($value as $subvalue) {
226
                        Storage::disk($upload->getDisk())->delete($upload->getPath().$subvalue);
227
                    }
228
229
                    continue;
230
                }
231
232
                Storage::disk($upload->getDisk())->delete($upload->getPath().$value);
233
            }
234
        }
235
    }
236
    /*******************************
237
     * Helper methods
238
     *******************************/
239
240
    /**
241
     * Given two multidimensional arrays/collections, merge them recursively.
242
     */
243
    protected function mergeValuesRecursive(array|Collection $array1, array|Collection $array2): array|Collection
244
    {
245
        $merged = $array1;
246
        foreach ($array2 as $key => &$value) {
247
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
248
                $merged[$key] = $this->mergeValuesRecursive($merged[$key], $value);
249
            } else {
250
                $merged[$key] = $value;
251
            }
252
        }
253
254
        return $merged;
255
    }
256
257
    /**
258
     * Repeatable items send `_order_` parameter in the request.
259
     * This holds the order of the items in the repeatable container.
260
     */
261
    protected function getFileOrderFromRequest(): array
262
    {
263
        $items = CRUD::getRequest()->input('_order_'.$this->getRepeatableContainerName()) ?? [];
264
265
        array_walk($items, function (&$key, $value) {
266
            $requestValue = $key[$this->getName()] ?? null;
267
            $key = $this->handleMultipleFiles ? (is_string($requestValue) ? explode(',', $requestValue) : $requestValue) : $requestValue;
268
        });
269
270
        return $items;
271
    }
272
273
    private function getPreviousRepeatableValues(Model $entry, UploaderInterface $uploader): array
274
    {
275
        $previousValues = json_decode($entry->getOriginal($uploader->getRepeatableContainerName()), true);
0 ignored issues
show
Bug introduced by
It seems like $entry->getOriginal($upl...eatableContainerName()) can also be of type array; however, parameter $json of json_decode() 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

275
        $previousValues = json_decode(/** @scrutinizer ignore-type */ $entry->getOriginal($uploader->getRepeatableContainerName()), true);
Loading history...
276
277
        if (! empty($previousValues)) {
278
            $previousValues = array_column($previousValues, $uploader->getName());
279
        }
280
281
        return $previousValues ?? [];
282
    }
283
284
    private function getValuesWithPathStripped(array|string|null $item, UploaderInterface $upload)
285
    {
286
        $uploadedValues = $item[$upload->getName()] ?? null;
287
        if (is_array($uploadedValues)) {
288
            return array_map(function ($value) use ($upload) {
289
                return Str::after($value, $upload->getPath());
290
            }, $uploadedValues);
291
        }
292
293
        return isset($uploadedValues) ? Str::after($uploadedValues, $upload->getPath()) : null;
294
    }
295
296
    private function deleteRelationshipFiles(Model $entry): void
297
    {
298
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
299
            $uploader->deleteRepeatableRelationFiles($entry);
300
        }
301
    }
302
303
    private function deleteRepeatableRelationFiles(Model $entry)
304
    {
305
        if (in_array($this->getRepeatableRelationType(), ['BelongsToMany', 'MorphToMany'])) {
306
            $pivotAttributes = $entry->getAttributes();
307
            $connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) {
308
                $itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes));
309
310
                return $itemPivotAttributes === $pivotAttributes;
311
            })->first();
312
313
            if (! $connectedPivot) {
314
                return;
315
            }
316
317
            $files = $connectedPivot->getOriginal()['pivot_'.$this->getAttributeName()];
318
319
            if (! $files) {
320
                return;
321
            }
322
323
            if($this->handleMultipleFiles && is_string($files)) {
324
                try {
325
                    $files = json_decode($files, true);
326
                } catch (\Exception) {
327
                   Log::error('Could not parse files for deletion pivot entry with key: '. $entry->getKey() . ' and uploader: ' . $this->getName());
328
                   return;
329
                }
330
            }
331
           
332
333
            if (is_array($files)) {
334
                foreach ($files as $value) {
335
                    $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

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

336
                    Storage::disk($this->/** @scrutinizer ignore-call */ getDisk())->delete($value);
Loading history...
337
                }
338
339
                return;
340
            }
341
342
            $value = Str::start($files, $this->getPath());
343
            Storage::disk($this->getDisk())->delete($value);
344
345
            return;
346
        }
347
348
        $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

348
        $this->/** @scrutinizer ignore-call */ 
349
               deleteFiles($entry);
Loading history...
349
    }
350
}
351