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 (#5427)
by Cristian
29:45 queued 14:40
created

HandleRepeatableUploads::getUploaderSubfield()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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\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
        $value = self::collecFilesAndValuesFromRequest($this->getRepeatableContainerName());
0 ignored issues
show
Bug introduced by
It seems like $this->getRepeatableContainerName() can also be of type null; however, parameter $attribute of Backpack\CRUD\app\Librar...sAndValuesFromRequest() 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

52
        $value = self::collecFilesAndValuesFromRequest(/** @scrutinizer ignore-type */ $this->getRepeatableContainerName());
Loading history...
53
54
        $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

54
        $entry->{$this->getRepeatableContainerName()} = json_encode($this->processRepeatableUploads($entry, /** @scrutinizer ignore-type */ $value));
Loading history...
55
56
        return $entry;
57
    }
58
59
    public static function collecFilesAndValuesFromRequest(string $attribute): array|Collection
60
    {
61
        $values = collect(CRUD::getRequest()->get($attribute));
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

61
        $values = collect(CRUD::/** @scrutinizer ignore-call */ getRequest()->get($attribute));
Loading history...
62
        $files = collect(CRUD::getRequest()->file($attribute));
63
64
        return self::mergeFilesAndValuesRecursive($values, $files);
65
    }
66
67
    private function processRelationshipRepeatableUploaders(Model $entry)
68
    {
69
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
70
            $entry = $uploader->uploadRelationshipFiles($entry);
71
        }
72
73
        return $entry;
74
    }
75
76
    protected function uploadRelationshipFiles(Model $entry): Model
77
    {
78
        $entryValue = $this->getFilesFromEntry($entry);
79
80
        if ($this->handleMultipleFiles && is_string($entryValue)) {
81
            try {
82
                $entryValue = json_decode($entryValue, true);
83
            } catch (\Exception) {
84
                return $entry;
85
            }
86
        }
87
88
        if ($this->hasDeletedFiles($entryValue)) {
89
            $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

89
            /** @scrutinizer ignore-call */ 
90
            $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

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

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

204
        return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->/** @scrutinizer ignore-call */ getName())->first();
Loading history...
205
    }
206
207
    private function getUploaderFieldSubfields()
208
    {
209
        return $this->getUploaderField()->getAttributes()['subfields'];
210
    }
211
212
    private function deleteRepeatableFiles(Model $entry): void
213
    {
214
        if ($this->isRelationship) {
215
            $this->deleteRelationshipFiles($entry);
216
217
            return;
218
        }
219
220
        $repeatableValues = collect($entry->{$this->getName()});
221
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $upload) {
222
            if (! $upload->shouldDeleteFiles()) {
223
                continue;
224
            }
225
            $values = $repeatableValues->pluck($upload->getName())->toArray();
226
            foreach ($values as $value) {
227
                if (! $value) {
228
                    continue;
229
                }
230
231
                if (is_array($value)) {
232
                    foreach ($value as $subvalue) {
233
                        Storage::disk($upload->getDisk())->delete($upload->getPath().$subvalue);
234
                    }
235
236
                    continue;
237
                }
238
239
                Storage::disk($upload->getDisk())->delete($upload->getPath().$value);
240
            }
241
        }
242
    }
243
244
    /*******************************
245
     * Helper methods
246
     *******************************/
247
    /**
248
     * Repeatable items send `_order_` parameter in the request.
249
     * This holds the order of the items in the repeatable container.
250
     */
251
    protected function getFileOrderFromRequest(): array
252
    {
253
        $items = CRUD::getRequest()->input('_order_'.$this->getRepeatableContainerName()) ?? [];
254
255
        array_walk($items, function (&$key, $value) {
256
            $requestValue = $key[$this->getName()] ?? null;
257
            $key = $this->handleMultipleFiles ? (is_string($requestValue) ? explode(',', $requestValue) : $requestValue) : $requestValue;
258
        });
259
260
        return $items;
261
    }
262
263
    private function getPreviousRepeatableValues(Model $entry, UploaderInterface $uploader): array
264
    {
265
        $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

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

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

326
                    Storage::disk($this->/** @scrutinizer ignore-call */ getDisk())->delete($value);
Loading history...
327
                }
328
329
                return;
330
            }
331
332
            $value = Str::start($files, $this->getPath());
333
            Storage::disk($this->getDisk())->delete($value);
334
335
            return;
336
        }
337
338
        $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

338
        $this->/** @scrutinizer ignore-call */ 
339
               deleteFiles($entry);
Loading history...
339
    }
340
}
341