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-upload-validators-in-repea... ( 0d6f3d )
by Pedro
14:44
created

HandleRepeatableUploads::mergeValuesRecursive()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 12
rs 9.6111
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
        return self::mergeFilesAndValuesRecursive($values, $files);
64
    }
65
66
    private function processRelationshipRepeatableUploaders(Model $entry)
67
    {
68
        foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
69
            $entry = $uploader->uploadRelationshipFiles($entry);
70
        }
71
72
        return $entry;
73
    }
74
75
    protected function uploadRelationshipFiles(Model $entry): Model
76
    {
77
        $entryValue = $this->getFilesFromEntry($entry);
78
79
        if ($this->handleMultipleFiles && is_string($entryValue)) {
80
            try {
81
                $entryValue = json_decode($entryValue, true);
82
            } catch (\Exception) {
83
                return $entry;
84
            }
85
        }
86
87
        if ($this->hasDeletedFiles($entryValue)) {
88
            $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

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

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

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

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

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

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

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

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