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

Uploader   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Importance

Changes 16
Bugs 10 Features 1
Metric Value
eloc 120
dl 0
loc 335
rs 2.96
c 16
b 10
f 1
wmc 68

30 Methods

Rating   Name   Duplication   Size   Complexity  
A retrieveUploadedFiles() 0 7 2
A for() 0 3 1
A __construct() 0 10 4
A isFake() 0 3 1
A getFakeAttribute() 0 3 1
A getExpirationTimeInMinutes() 0 3 1
A getValueWithoutPath() 0 3 2
A shouldDeleteFiles() 0 3 1
A getName() 0 3 1
A getDisk() 0 3 1
A getPath() 0 3 1
A useTemporaryUrl() 0 3 1
A canHandleMultipleFiles() 0 3 1
A getIdentifier() 0 7 2
A deleteUploadedFiles() 0 10 3
A getNameForRequest() 0 3 1
A fake() 0 5 1
A shouldKeepPreviousValueUnchanged() 0 3 3
A getPathFromConfiguration() 0 5 2
A multiple() 0 5 1
A relationship() 0 5 1
A storeUploadedFiles() 0 19 5
A uploadFiles() 0 2 1
A performFileDeletion() 0 9 3
A getAttributeName() 0 3 1
A getPreviousFiles() 0 9 3
B deleteFiles() 0 33 8
A isRelationship() 0 3 1
A getOriginalValue() 0 23 6
B retrieveFiles() 0 27 8

How to fix   Complexity   

Complex Class

Complex classes like Uploader 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 Uploader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Backpack\CRUD\app\Library\Uploaders;
4
5
use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface;
6
use Backpack\CRUD\app\Library\Uploaders\Support\Traits\HandleFileNaming;
7
use Backpack\CRUD\app\Library\Uploaders\Support\Traits\HandleRepeatableUploads;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\SoftDeletes;
10
use Illuminate\Support\Facades\Storage;
11
use Illuminate\Support\Str;
12
13
abstract class Uploader implements UploaderInterface
14
{
15
    use HandleFileNaming;
16
    use HandleRepeatableUploads;
0 ignored issues
show
introduced by
The trait Backpack\CRUD\app\Librar...HandleRepeatableUploads requires some properties which are not provided by Backpack\CRUD\app\Library\Uploaders\Uploader: $model, $pivot, $pivotParent
Loading history...
17
18
    private string $name;
19
20
    private string $disk = 'public';
21
22
    private string $path = '';
23
24
    public bool $handleMultipleFiles = false;
25
26
    private bool $deleteWhenEntryIsDeleted = true;
27
28
    private bool|string $attachedToFakeField = false;
29
30
    /**
31
     * Cloud disks have the ability to generate temporary URLs to files, should we do it?
32
     */
33
    private bool $useTemporaryUrl = false;
34
35
    /**
36
     * When using temporary urls, define the time that the url will be valid.
37
     */
38
    private int $temporaryUrlExpirationTimeInMinutes = 1;
39
40
    /**
41
     * Indicates if the upload is relative to a relationship field/column.
42
     */
43
    private bool $isRelationship = false;
44
45
    /**
46
     * When previous files are updated, we need to keep track of them so that we don't add deleted files to the new list.
47
     */
48
    private $updatedPreviousFiles = null;
49
50
    public function __construct(array $crudObject, array $configuration)
51
    {
52
        $this->name = $crudObject['name'];
53
        $this->disk = $configuration['disk'] ?? $crudObject['disk'] ?? $this->disk;
54
        $this->path = $this->getPathFromConfiguration($crudObject, $configuration);
55
        $this->attachedToFakeField = isset($crudObject['fake']) && $crudObject['fake'] ? ($crudObject['store_in'] ?? 'extras') : ($crudObject['store_in'] ?? false);
56
        $this->useTemporaryUrl = $configuration['temporaryUrl'] ?? $this->useTemporaryUrl;
57
        $this->temporaryUrlExpirationTimeInMinutes = $configuration['temporaryUrlExpirationTime'] ?? $this->temporaryUrlExpirationTimeInMinutes;
58
        $this->deleteWhenEntryIsDeleted = $configuration['deleteWhenEntryIsDeleted'] ?? $this->deleteWhenEntryIsDeleted;
59
        $this->fileNamer = is_callable($configuration['fileNamer'] ?? null) ? $configuration['fileNamer'] : $this->getFileNameGeneratorInstance($configuration['fileNamer'] ?? null);
60
    }
61
62
    /*******************************
63
     * Static methods
64
     *******************************/
65
    public static function for(array $crudObject, array $definition): UploaderInterface
66
    {
67
        return new static($crudObject, $definition);
68
    }
69
70
    /*******************************
71
     * public methods - event handler methods
72
     *******************************/
73
    public function storeUploadedFiles(Model $entry): Model
74
    {
75
        if ($this->handleRepeatableFiles) {
76
            return $this->handleRepeatableFiles($entry);
77
        }
78
79
        if ($this->attachedToFakeField) {
80
            $fakeFieldValue = $entry->{$this->attachedToFakeField};
81
            $fakeFieldValue = is_string($fakeFieldValue) ? json_decode($fakeFieldValue, true) : (array) $fakeFieldValue;
82
            $fakeFieldValue[$this->getAttributeName()] = $this->uploadFiles($entry);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $fakeFieldValue[$this->getAttributeName()] is correct as $this->uploadFiles($entry) targeting Backpack\CRUD\app\Librar...Uploader::uploadFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
83
84
            $entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $fakeFieldValue : json_encode($fakeFieldValue);
85
86
            return $entry;
87
        }
88
89
        $entry->{$this->getAttributeName()} = $this->uploadFiles($entry);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $entry->$this->getAttributeName() is correct as $this->uploadFiles($entry) targeting Backpack\CRUD\app\Librar...Uploader::uploadFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
90
91
        return $entry;
92
    }
93
94
    public function retrieveUploadedFiles(Model $entry): Model
95
    {
96
        if ($this->handleRepeatableFiles) {
97
            return $this->retrieveRepeatableFiles($entry);
98
        }
99
100
        return $this->retrieveFiles($entry);
101
    }
102
103
    public function deleteUploadedFiles(Model $entry): void
104
    {
105
        if (! in_array(SoftDeletes::class, class_uses_recursive($entry), true)) {
106
            $this->performFileDeletion($entry);
107
108
            return;
109
        }
110
111
        if ($entry->isForceDeleting() === true) {
112
            $this->performFileDeletion($entry);
113
        }
114
    }
115
116
    /*******************************
117
     * Getters
118
     *******************************/
119
    public function getName(): string
120
    {
121
        return $this->name;
122
    }
123
124
    public function getAttributeName(): string
125
    {
126
        return Str::afterLast($this->name, '.');
127
    }
128
129
    public function getDisk(): string
130
    {
131
        return $this->disk;
132
    }
133
134
    public function getPath(): string
135
    {
136
        return $this->path;
137
    }
138
139
    public function useTemporaryUrl(): bool
140
    {
141
        return $this->useTemporaryUrl;
142
    }
143
144
    public function getExpirationTimeInMinutes(): int
145
    {
146
        return $this->temporaryUrlExpirationTimeInMinutes;
147
    }
148
149
    public function shouldDeleteFiles(): bool
150
    {
151
        return $this->deleteWhenEntryIsDeleted;
152
    }
153
154
    public function getIdentifier(): string
155
    {
156
        if ($this->handleRepeatableFiles) {
157
            return $this->repeatableContainerName.'_'.$this->name;
158
        }
159
160
        return $this->name;
161
    }
162
163
    public function getNameForRequest(): string
164
    {
165
        return $this->repeatableContainerName ?? $this->name;
166
    }
167
168
    public function canHandleMultipleFiles(): bool
169
    {
170
        return $this->handleMultipleFiles;
171
    }
172
173
    public function isRelationship(): bool
174
    {
175
        return $this->isRelationship;
176
    }
177
178
    public function getPreviousFiles(Model $entry): mixed
179
    {
180
        if (! $this->attachedToFakeField) {
181
            return $this->getOriginalValue($entry);
182
        }
183
        $value = $this->getOriginalValue($entry, $this->attachedToFakeField);
184
        $value = is_string($value) ? json_decode($value, true) : (array) $value;
185
186
        return $value[$this->getAttributeName()] ?? null;
187
    }
188
189
    public function getValueWithoutPath(?string $value = null): ?string
190
    {
191
        return $value ? Str::after($value, $this->path) : null;
192
    }
193
194
    public function isFake(): bool
195
    {
196
        return $this->attachedToFakeField !== false;
197
    }
198
199
    public function getFakeAttribute(): bool|string
200
    {
201
        return $this->attachedToFakeField;
202
    }
203
204
    public function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool
205
    {
206
        return $entry->exists && ($entryValue === null || $entryValue === [null]);
207
    }
208
209
    /*******************************
210
     * Setters - fluently configure the uploader
211
     *******************************/
212
    public function multiple(): self
213
    {
214
        $this->handleMultipleFiles = true;
215
216
        return $this;
217
    }
218
219
    public function relationship(bool $isRelationship): self
220
    {
221
        $this->isRelationship = $isRelationship;
222
223
        return $this;
224
    }
225
226
    public function fake(bool|string $isFake): self
227
    {
228
        $this->attachedToFakeField = $isFake;
229
230
        return $this;
231
    }
232
233
    /*******************************
234
     * Default implementation functions
235
     *******************************/
236
    public function uploadFiles(Model $entry, $values = null)
237
    {
238
    }
239
240
    private function retrieveFiles(Model $entry): Model
241
    {
242
        $value = $entry->{$this->getAttributeName()};
243
244
        if ($this->attachedToFakeField) {
245
            $values = $entry->{$this->attachedToFakeField};
246
247
            $values = is_string($values) ? json_decode($values, true) : $values;
248
            $attributeValue = $values[$this->getAttributeName()] ?? null;
249
            $attributeValue = is_array($attributeValue) ? array_map(fn ($value) => $this->getValueWithoutPath($value), $attributeValue) : $this->getValueWithoutPath($attributeValue);
250
            $values[$this->getAttributeName()] = $attributeValue;
251
            $entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $values : json_encode($values);
252
253
            return $entry;
254
        }
255
256
        if ($this->handleMultipleFiles) {
257
            if (! isset($entry->getCasts()[$this->getName()]) && is_string($value)) {
258
                $entry->{$this->getAttributeName()} = json_decode($value, true);
259
            }
260
261
            return $entry;
262
        }
263
264
        $entry->{$this->getAttributeName()} = $this->getValueWithoutPath($value);
265
266
        return $entry;
267
    }
268
269
    protected function deleteFiles(Model $entry)
270
    {
271
        if (! $this->shouldDeleteFiles()) {
272
            return;
273
        }
274
275
        if ($this->attachedToFakeField) {
276
            $values = $entry->{$this->attachedToFakeField};
277
            $values = is_string($values) ? json_decode($values, true) : $values;
278
            $values = $values[$this->getAttributeName()] ?? null;
279
        }
280
281
        $values ??= $entry->{$this->getAttributeName()};
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $values does not seem to be defined for all execution paths leading up to this point.
Loading history...
282
283
        if ($values === null) {
284
            return;
285
        }
286
287
        if ($this->handleMultipleFiles) {
288
            // ensure we have an array of values when field is not casted in model.
289
            if (is_string($values)) {
290
                $values = json_decode($values, true);
291
            }
292
            foreach ($values ?? [] as $value) {
293
                $value = Str::start($value, $this->path);
294
                Storage::disk($this->disk)->delete($value);
295
            }
296
297
            return;
298
        }
299
300
        $values = Str::start($values, $this->path);
301
        Storage::disk($this->disk)->delete($values);
302
    }
303
304
    private function performFileDeletion(Model $entry)
305
    {
306
        if (! $this->handleRepeatableFiles && $this->deleteWhenEntryIsDeleted) {
307
            $this->deleteFiles($entry);
308
309
            return;
310
        }
311
312
        $this->deleteRepeatableFiles($entry);
313
    }
314
315
    /*******************************
316
     * helper methods
317
     *******************************/
318
    private function getPathFromConfiguration(array $crudObject, array $configuration): string
319
    {
320
        $this->path = $configuration['path'] ?? $crudObject['prefix'] ?? $this->path;
321
322
        return empty($this->path) ? $this->path : Str::of($this->path)->finish('/')->value();
323
    }
324
325
    private function getOriginalValue(Model $entry, $field = null)
326
    {
327
        $field ??= $this->getAttributeName();
328
329
        if ($this->updatedPreviousFiles !== null) {
330
            return $this->updatedPreviousFiles;
331
        }
332
333
        $previousValue = $entry->getOriginal($field);
334
335
        if (! $previousValue) {
336
            return $previousValue;
337
        }
338
339
        if (
340
            method_exists($entry, 'translationEnabled') &&
341
            $entry->translationEnabled() &&
342
            $entry->isTranslatableAttribute($field)
343
        ) {
344
            return $previousValue[$entry->getLocale()] ?? null;
345
        }
346
347
        return $previousValue;
348
    }
349
}
350