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
Pull Request — main (#5427)
by Pedro
14:44
created

HandleRepeatableUploads::deleteRepeatableFiles()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 3 Features 0
Metric Value
cc 8
eloc 16
c 5
b 3
f 0
nc 8
nop 1
dl 0
loc 28
rs 8.4444
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