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 (#4267)
by Cristian
28:44 queued 14:24
created

Update::getSubfieldsValues()   B

Complexity

Conditions 9
Paths 15

Size

Total Lines 38
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 15
nop 2
dl 0
loc 38
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Facades\DB;
8
use Illuminate\Support\Str;
9
10
trait Update
11
{
12
    /*
13
    |--------------------------------------------------------------------------
14
    |                                   UPDATE
15
    |--------------------------------------------------------------------------
16
    */
17
18
    /**
19
     * Update a row in the database.
20
     *
21
     * @param  int  $id  The entity's id
22
     * @param  array  $input  All inputs to be updated.
23
     * @return object
24
     */
25
    public function update($id, $input)
26
    {
27
        $item = $this->model->findOrFail($id);
28
29
        [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($input);
0 ignored issues
show
Bug introduced by
It seems like splitInputIntoDirectAndRelations() 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

29
        /** @scrutinizer ignore-call */ 
30
        [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($input);
Loading history...
30
        if ($this->get('update.useDatabaseTransactions') ?? config('backpack.base.useDatabaseTransactions', false)) {
0 ignored issues
show
Bug introduced by
It seems like get() 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

30
        if ($this->/** @scrutinizer ignore-call */ get('update.useDatabaseTransactions') ?? config('backpack.base.useDatabaseTransactions', false)) {
Loading history...
31
            return DB::transaction(fn () => $this->updateModelAndRelations($item, $directInputs, $relationInputs));
32
        }
33
34
        return $this->updateModelAndRelations($item, $directInputs, $relationInputs);
35
    }
36
37
    private function updateModelAndRelations(Model $item, array $directInputs, array $relationInputs): Model
38
    {
39
        $item->update($directInputs);
40
        $this->createRelationsForItem($item, $relationInputs);
0 ignored issues
show
Bug introduced by
It seems like createRelationsForItem() 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

40
        $this->/** @scrutinizer ignore-call */ 
41
               createRelationsForItem($item, $relationInputs);
Loading history...
41
42
        return $item;
43
    }
44
45
    /**
46
     * Get all fields needed for the EDIT ENTRY form.
47
     *
48
     * @param  int  $id  The id of the entry that is being edited.
49
     * @return array The fields with attributes, fake attributes and values.
50
     */
51
    public function getUpdateFields($id = false)
52
    {
53
        $fields = $this->fields();
0 ignored issues
show
Bug introduced by
The method fields() does not exist on Backpack\CRUD\app\Library\CrudPanel\Traits\Update. Did you maybe mean getSubfieldsValues()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

53
        /** @scrutinizer ignore-call */ 
54
        $fields = $this->fields();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
54
        $entry = ($id != false) ? $this->getEntryWithLocale($id) : $this->getCurrentEntryWithLocale();
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $id of type false|integer against false; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
Bug introduced by
It seems like getEntryWithLocale() 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

54
        $entry = ($id != false) ? $this->/** @scrutinizer ignore-call */ getEntryWithLocale($id) : $this->getCurrentEntryWithLocale();
Loading history...
Bug introduced by
It seems like getCurrentEntryWithLocale() 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

54
        $entry = ($id != false) ? $this->getEntryWithLocale($id) : $this->/** @scrutinizer ignore-call */ getCurrentEntryWithLocale();
Loading history...
55
56
        foreach ($fields as &$field) {
57
            $field['value'] = $field['value'] ?? $this->getModelAttributeValue($entry, $field);
58
        }
59
60
        // always have a hidden input for the entry id
61
        if (! array_key_exists('id', $fields)) {
62
            $fields['id'] = [
63
                'name' => $entry->getKeyName(),
64
                'value' => $entry->getKey(),
65
                'type' => 'hidden',
66
            ];
67
        }
68
69
        return $fields;
70
    }
71
72
    /**
73
     * Get the value of the 'name' attribute from the declared relation model in the given field.
74
     *
75
     * @param  \Illuminate\Database\Eloquent\Model  $model  The current CRUD model.
76
     * @param  array  $field  The CRUD field array.
77
     * @return mixed The value of the 'name' attribute from the relation model.
78
     */
79
    private function getModelAttributeValue($model, $field)
80
    {
81
        $model = $model->withFakes();
82
83
        $fieldEntity = $field['entity'] ?? false;
84
        $fakeField = $field['fake'] ?? false;
85
86
        if ($fieldEntity && ! $fakeField) {
87
            return $this->getModelAttributeValueFromRelationship($model, $field);
0 ignored issues
show
Bug introduced by
It seems like $model can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Eloq...gHasThroughRelationship; however, parameter $model of Backpack\CRUD\app\Librar...ValueFromRelationship() does only seem to accept Illuminate\Database\Eloquent\Model, 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

87
            return $this->getModelAttributeValueFromRelationship(/** @scrutinizer ignore-type */ $model, $field);
Loading history...
88
        }
89
90
        if ($this->holdsMultipleInputs($field['name'])) {
0 ignored issues
show
Bug introduced by
It seems like holdsMultipleInputs() 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

90
        if ($this->/** @scrutinizer ignore-call */ holdsMultipleInputs($field['name'])) {
Loading history...
91
            $result = array_map(function ($item) use ($model) {
92
                return $model->{$item};
93
            }, explode(',', $field['name']));
94
95
            return $result;
96
        }
97
98
        return $model->{$field['name']};
99
    }
100
101
    /**
102
     * Returns the value of the given attribute in the relationship.
103
     * It takes into account nested relationships.
104
     *
105
     * @param  \Illuminate\Database\Eloquent\Model  $model
106
     * @param  array  $field
107
     * @return mixed
108
     */
109
    private function getModelAttributeValueFromRelationship($model, $field)
110
    {
111
        [$relatedModel, $relationMethod] = $this->getModelAndMethodFromEntity($model, $field);
112
113
        if (! method_exists($relatedModel, $relationMethod)) {
114
            return $relatedModel->{$relationMethod};
115
        }
116
117
        $relation = $relatedModel->{$relationMethod}();
118
        $relationType = Str::afterLast(get_class($relation), '\\');
119
120
        switch ($relationType) {
121
            case 'MorphMany':
122
            case 'HasMany':
123
            case 'BelongsToMany':
124
            case 'MorphToMany':
125
                $relationModels = $relatedModel->{$relationMethod};
126
                $result = collect();
127
128
                foreach ($relationModels as $model) {
0 ignored issues
show
introduced by
$model is overwriting one of the parameters of this function.
Loading history...
129
                    $model = $this->setLocaleOnModel($model);
0 ignored issues
show
Bug introduced by
It seems like setLocaleOnModel() 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

129
                    /** @scrutinizer ignore-call */ 
130
                    $model = $this->setLocaleOnModel($model);
Loading history...
130
                    // when subfields are NOT set we don't need to get any more values
131
                    // we just return the plain models as we only need the ids
132
                    if (! isset($field['subfields'])) {
133
                        $result->push($model);
134
135
                        continue;
136
                    }
137
                    // when subfields are set we need to parse their values so they can be displayed
138
                    switch ($relationType) {
139
                        case 'HasMany':
140
                        case 'MorphMany':
141
                            // we will get model direct attributes and merge with subfields values.
142
                            $directAttributes = $this->getModelWithFakes($model)->getAttributes();
143
                            $result->push(array_merge($directAttributes, $this->getSubfieldsValues($field['subfields'], $model)));
0 ignored issues
show
Bug introduced by
array_merge($directAttri...['subfields'], $model)) of type array is incompatible with the type Illuminate\Support\TValue expected by parameter $values of Illuminate\Support\Collection::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

143
                            $result->push(/** @scrutinizer ignore-type */ array_merge($directAttributes, $this->getSubfieldsValues($field['subfields'], $model)));
Loading history...
144
                            break;
145
146
                        case 'BelongsToMany':
147
                        case 'MorphToMany':
148
                            // for any given model, we grab the attributes that belong to our pivot table.
149
                            $item = $model->{$relation->getPivotAccessor()}->getAttributes();
150
                            $item[$relationMethod] = $model->getKey();
151
                            $result->push($item);
152
                            break;
153
                    }
154
                }
155
156
                return $result;
157
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
158
            case 'HasOne':
159
            case 'MorphOne':
160
                if (! method_exists($relatedModel, $relationMethod)) {
161
                    return;
162
                }
163
164
                $model = $relatedModel->{$relationMethod};
165
166
                if (! $model) {
167
                    return;
168
                }
169
170
                $model = $this->setLocaleOnModel($model);
171
                $model = $this->getModelWithFakes($model);
172
173
                // if `entity` contains a dot here it means developer added a main HasOne/MorphOne relation with dot notation
174
                if (Str::contains($field['entity'], '.')) {
175
                    return $model->{Str::afterLast($field['entity'], '.')};
176
                }
177
178
                // when subfields exists developer used the repeatable interface to manage this relation
179
                if (isset($field['subfields'])) {
180
                    return [$this->getSubfieldsValues($field['subfields'], $model)];
181
                }
182
183
                return $this->getModelWithFakes($model);
184
185
                break;
186
            case 'BelongsTo':
187
                if ($relatedModel->{$relationMethod}) {
188
                    return $relatedModel->{$relationMethod}->getKey();
189
                }
190
191
                return $relatedModel->{$relationMethod};
192
                break;
193
            default:
194
                return $relatedModel->{$relationMethod};
195
        }
196
    }
197
198
    /**
199
     * This function checks if the provided model uses the CrudTrait.
200
     * If IT DOES it adds the fakes to the model attributes.
201
     * Otherwise just return the model back.
202
     *
203
     * @param  \Illuminate\Database\Eloquent\Model  $model
204
     * @return \Illuminate\Database\Eloquent\Model
205
     */
206
    private function getModelWithFakes($model)
207
    {
208
        if (in_array(\Backpack\CRUD\app\Models\Traits\CrudTrait::class, class_uses_recursive($model))) {
209
            return $model->withFakes();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $model->withFakes() also could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the documented return type Illuminate\Database\Eloquent\Model.
Loading history...
210
        }
211
212
        return $model;
213
    }
214
215
    /**
216
     * Returns the model and the method from the relation string starting from the provided model.
217
     *
218
     * @param  Illuminate\Database\Eloquent\Model  $model
0 ignored issues
show
Bug introduced by
The type Backpack\CRUD\app\Librar...Database\Eloquent\Model was not found. Did you mean Illuminate\Database\Eloquent\Model? If so, make sure to prefix the type with \.
Loading history...
219
     * @param  array  $field
220
     * @return array
221
     */
222
    private function getModelAndMethodFromEntity($model, $field)
223
    {
224
        // HasOne and MorphOne relations contains the field in the relation string. We want only the relation part.
225
        $relationEntity = $this->getOnlyRelationEntity($field);
0 ignored issues
show
Bug introduced by
It seems like getOnlyRelationEntity() 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

225
        /** @scrutinizer ignore-call */ 
226
        $relationEntity = $this->getOnlyRelationEntity($field);
Loading history...
226
227
        $relationArray = explode('.', $relationEntity);
228
229
        $relatedModel = array_reduce(array_splice($relationArray, 0, -1), function ($obj, $method) {
230
            // if the string ends with `_id` we strip it out
231
            $method = Str::endsWith($method, '_id') ? Str::replaceLast('_id', '', $method) : $method;
232
233
            return $obj->{$method} ? $obj->{$method} : $obj;
234
        }, $model);
235
236
        $relationMethod = Str::afterLast($relationEntity, '.');
237
238
        return [$relatedModel, $relationMethod];
239
    }
240
241
    /**
242
     * Return the subfields values from the related model.
243
     *
244
     * @param  array  $subfields
245
     * @param  \Illuminate\Database\Eloquent\Model  $relatedModel
246
     * @return array
247
     */
248
    private function getSubfieldsValues($subfields, $relatedModel)
249
    {
250
        $result = [];
251
        foreach ($subfields as $subfield) {
252
            $name = is_string($subfield) ? $subfield : $subfield['name'];
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
253
            // if the subfield name does not contain a dot we just need to check
254
            // if it has subfields and return the result accordingly.
255
            foreach ((array) $subfield['name'] as $name) {
256
                if (! Str::contains($name, '.')) {
257
                    // when subfields are present, $relatedModel->{$name} returns a model instance
258
                    // otherwise returns the model attribute.
259
                    if ($relatedModel->{$name} && isset($subfield['subfields'])) {
260
                        $result[$name] = [$relatedModel->{$name}->only(array_column($subfield['subfields'], 'name'))];
261
                    } else {
262
                        $result[$name] = $relatedModel->{$name};
263
                    }
264
                } else {
265
                    // if the subfield name contains a dot, we are going to iterate through
266
                    // those parts to get the last connected part and parse it for returning.
267
                    // $iterator would be either a string (the attribute in model, eg: street)
268
                    // or a model instance (eg: AddressModel)
269
                    $iterator = $relatedModel;
270
271
                    foreach (explode('.', $name) as $part) {
272
                        $iterator = $iterator->$part;
273
                    }
274
275
                    if (is_a($iterator, 'Illuminate\Database\Eloquent\Model', true)) {
276
                        $iterator = $this->setLocaleOnModel($iterator);
277
                        $iterator = $this->getModelWithFakes($iterator)->getAttributes();
278
                    }
279
280
                    Arr::set($result, $name, $iterator);
281
                }
282
            }
283
        }
284
285
        return $result;
286
    }
287
}
288