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 — master (#3410)
by
unknown
11:02
created

Create::createRelationsForItem()   C

Complexity

Conditions 13
Paths 29

Size

Total Lines 57
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 32
c 0
b 0
f 0
nc 29
nop 2
dl 0
loc 57
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Database\Eloquent\Relations\HasMany;
7
use Illuminate\Database\Eloquent\Relations\HasOne;
8
use Illuminate\Database\Eloquent\Relations\MorphMany;
9
use Illuminate\Database\Eloquent\Relations\MorphOne;
10
use Illuminate\Support\Arr;
11
12
trait Create
13
{
14
    /*
15
    |--------------------------------------------------------------------------
16
    |                                   CREATE
17
    |--------------------------------------------------------------------------
18
    */
19
20
    /**
21
     * Insert a row in the database.
22
     *
23
     * @param array $data All input values to be inserted.
24
     *
25
     * @return \Illuminate\Database\Eloquent\Model
26
     */
27
    public function create($data)
28
    {
29
        $data = $this->decodeJsonCastedAttributes($data);
0 ignored issues
show
Bug introduced by
It seems like decodeJsonCastedAttributes() 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
        $data = $this->decodeJsonCastedAttributes($data);
Loading history...
30
        $data = $this->compactFakeFields($data);
0 ignored issues
show
Bug introduced by
It seems like compactFakeFields() 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
        /** @scrutinizer ignore-call */ 
31
        $data = $this->compactFakeFields($data);
Loading history...
31
32
        // omit the n-n relationships when updating the eloquent item
33
        $relationships = Arr::pluck($this->getRelationFields(), 'name');
34
35
        // init and fill model
36
        $item = $this->model->make(Arr::except($data, $relationships));
37
38
        // handle BelongsTo 1:1 relations
39
        $item = $this->associateOrDissociateBelongsToRelations($item, $data);
40
        $item->save();
41
42
        // if there are any other relations create them.
43
        $this->createRelations($item, $data);
44
45
        return $item;
46
    }
47
48
    /**
49
     * Get all fields needed for the ADD NEW ENTRY form.
50
     *
51
     * @return array The fields with attributes and fake attributes.
52
     */
53
    public function getCreateFields()
54
    {
55
        return $this->fields();
0 ignored issues
show
Bug introduced by
It seems like fields() 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

55
        return $this->/** @scrutinizer ignore-call */ fields();
Loading history...
56
    }
57
58
    /**
59
     * Get all fields with relation set (model key set on field).
60
     *
61
     * @return array The fields with model key set.
62
     */
63
    public function getRelationFields()
64
    {
65
        $fields = $this->fields();
66
        $relationFields = [];
67
68
        foreach ($fields as $field) {
69
            if (isset($field['model']) && $field['model'] !== false) {
70
                array_push($relationFields, $field);
71
            }
72
73
            if (isset($field['subfields']) &&
74
                is_array($field['subfields']) &&
75
                count($field['subfields'])) {
76
                foreach ($field['subfields'] as $subfield) {
77
                    array_push($relationFields, $subfield);
78
                }
79
            }
80
        }
81
82
        return $relationFields;
83
    }
84
85
    /**
86
     * Get all fields with n-n relation set (pivot table is true).
87
     *
88
     * @return array The fields with n-n relationships.
89
     */
90
    public function getRelationFieldsWithPivot()
91
    {
92
        $all_relation_fields = $this->getRelationFields();
93
94
        return Arr::where($all_relation_fields, function ($value, $key) {
95
            return isset($value['pivot']) && $value['pivot'];
96
        });
97
    }
98
99
    /**
100
     * Create the relations for the current model.
101
     *
102
     * @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
103
     * @param array                               $data The form data.
104
     */
105
    public function createRelations($item, $data)
106
    {
107
        $relationData = $this->getRelationDataFromFormData($data);
108
109
        // handles 1-1 and 1-n relations (HasOne, MorphOne, HasMany, MorphMany)
110
        $this->createRelationsForItem($item, $relationData);
111
112
        // this specifically handles M-M relations that could sync additional information into pivot table
113
        $this->syncPivot($item, $data);
114
    }
115
116
    /**
117
     * Sync the declared many-to-many associations through the pivot field.
118
     *
119
     * @param \Illuminate\Database\Eloquent\Model $model The current CRUD model.
120
     * @param array                               $data  The form data.
121
     */
122
    public function syncPivot($model, $data)
123
    {
124
        $fields_with_relationships = $this->getRelationFieldsWithPivot();
125
        foreach ($fields_with_relationships as $field) {
126
            $values = isset($data[$field['name']]) ? $data[$field['name']] : [];
127
128
            // if a JSON was passed instead of an array, turn it into an array
129
            if (is_string($values)) {
130
                $decoded_values = json_decode($values, true);
131
                $values = [];
132
                // array is not multidimensional
133
                if (count($decoded_values) != count($decoded_values, COUNT_RECURSIVE)) {
134
                    foreach ($decoded_values as $value) {
135
                        $values[] = $value[$field['name']];
136
                    }
137
                } else {
138
                    $values = $decoded_values;
139
                }
140
            }
141
142
            $relation_data = [];
143
144
            foreach ($values as $pivot_id) {
145
                if ($pivot_id != '') {
146
                    $pivot_data = [];
147
148
                    if (isset($field['pivotFields'])) {
149
                        // array is not multidimensional
150
                        if (count($field['pivotFields']) == count($field['pivotFields'], COUNT_RECURSIVE)) {
151
                            foreach ($field['pivotFields'] as $pivot_field_name) {
152
                                $pivot_data[$pivot_field_name] = $data[$pivot_field_name][$pivot_id];
153
                            }
154
                        } else {
155
                            $field_data = json_decode($data[$field['name']], true);
156
157
                            // we grab from the parsed data the specific values for this pivot
158
                            $pivot_data = Arr::first($field_data, function ($item) use ($pivot_id, $field) {
159
                                return $item[$field['name']] === $pivot_id;
160
                            });
161
162
                            // we remove the relation field from extra pivot data as we already have the relation.
163
                            unset($pivot_data[$field['name']]);
164
                        }
165
                    }
166
167
                    $relation_data[$pivot_id] = $pivot_data;
168
                }
169
170
                $model->{$field['name']}()->sync($relation_data);
171
172
                if (isset($field['morph']) && $field['morph'] && isset($data[$field['name']])) {
173
                    $values = $data[$field['name']];
174
                    $model->{$field['name']}()->sync($values);
175
                }
176
            }
177
        }
178
    }
179
180
    /**
181
     * Handles 1-1 and 1-n relations. In case 1-1 it handles subsequent relations in connected models
182
     * For example, a Monster > HasOne Address > BelongsTo a Country.
183
     *
184
     * @param \Illuminate\Database\Eloquent\Model $item          The current CRUD model.
185
     * @param array                               $formattedData The form data.
186
     *
187
     * @return bool|null
188
     */
189
    private function createRelationsForItem($item, $formattedData)
190
    {
191
        if (! isset($formattedData['relations'])) {
192
            return false;
193
        }
194
195
        foreach ($formattedData['relations'] as $relationMethod => $relationData) {
196
            if (! isset($relationData['model'])) {
197
                continue;
198
            }
199
200
            $relation = $item->{$relationMethod}();
201
            $relation_type = get_class($relation);
202
203
            switch ($relation_type) {
204
                case HasOne::class:
205
                case MorphOne::class:
206
                    // we first check if there are relations of the relation
207
                    if (isset($relationData['relations'])) {
208
                        // if there are nested relations, we first add the BelongsTo like in main entry
209
                        $belongsToRelations = Arr::where($relationData['relations'], function ($relation_data) {
210
                            return $relation_data['relation_type'] == 'BelongsTo';
211
                        });
212
213
                        // adds the values of the BelongsTo relations of this entity to the array of values that will
214
                        // be saved at the same time like we do in parent entity belongs to relations
215
                        $valuesWithRelations = $this->associateHasOneBelongsTo($belongsToRelations, $relationData['values'], $relation->getModel());
216
217
                        // remove previously added BelongsTo relations from relation data.
218
                        $relationData['relations'] = Arr::where($relationData['relations'], function ($item) {
219
                            return $item['relation_type'] != 'BelongsTo';
220
                        });
221
222
                        $modelInstance = $relation->updateOrCreate([], $valuesWithRelations);
223
                    } else {
224
                        $modelInstance = $relation->updateOrCreate([], $relationData['values']);
225
                    }
226
                break;
227
                case HasMany::class:
228
                case MorphMany::class:
229
230
                    $relation_values = $relationData['values'][$relationMethod];
231
232
                    if (is_string($relation_values)) {
233
                        $relation_values = json_decode($relationData['values'][$relationMethod], true);
234
                    }
235
236
                    if ($relation_values === null || count($relation_values) == count($relation_values, COUNT_RECURSIVE)) {
237
                        $this->attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values);
238
                    } else {
239
                        $this->createManyEntries($item, $relation, $relationMethod, $relationData);
240
                    }
241
                break;
242
            }
243
244
            if (isset($relationData['relations'])) {
245
                $this->createRelationsForItem($modelInstance, ['relations' => $relationData['relations']]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $modelInstance does not seem to be defined for all execution paths leading up to this point.
Loading history...
246
            }
247
        }
248
    }
249
250
    /**
251
     * Associate and dissociate BelongsTo relations in the model.
252
     *
253
     * @param  Model $item
254
     * @param  array $data The form data.
255
     * @return Model Model with relationships set up.
256
     */
257
    protected function associateOrDissociateBelongsToRelations($item, array $data)
258
    {
259
        $belongsToFields = $this->getFieldsWithRelationType('BelongsTo');
0 ignored issues
show
Bug introduced by
It seems like getFieldsWithRelationType() 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

259
        /** @scrutinizer ignore-call */ 
260
        $belongsToFields = $this->getFieldsWithRelationType('BelongsTo');
Loading history...
260
261
        foreach ($belongsToFields as $relationField) {
262
            if (method_exists($item, $this->getOnlyRelationEntity($relationField))) {
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

262
            if (method_exists($item, $this->/** @scrutinizer ignore-call */ getOnlyRelationEntity($relationField))) {
Loading history...
263
                $relatedId = Arr::get($data, $relationField['name']);
264
                if (isset($relatedId) && $relatedId !== null) {
265
                    $related = $relationField['model']::find($relatedId);
266
267
                    $item->{$this->getOnlyRelationEntity($relationField)}()->associate($related);
268
                } else {
269
                    $item->{$this->getOnlyRelationEntity($relationField)}()->dissociate();
270
                }
271
            }
272
        }
273
274
        return $item;
275
    }
276
277
    /**
278
     * Associate the nested HasOne -> BelongsTo relations by adding the "connecting key"
279
     * to the array of values that is going to be saved with HasOne relation.
280
     *
281
     * @param array $belongsToRelations
282
     * @param array $modelValues
283
     * @param Model $relationInstance
284
     * @return array
285
     */
286
    private function associateHasOneBelongsTo($belongsToRelations, $modelValues, $modelInstance)
287
    {
288
        foreach ($belongsToRelations as $methodName => $values) {
289
            $relation = $modelInstance->{$methodName}();
290
291
            $modelValues[$relation->getForeignKeyName()] = $values['values'][$methodName];
292
        }
293
294
        return $modelValues;
295
    }
296
297
    /**
298
     * Get a relation data array from the form data.
299
     * For each relation defined in the fields through the entity attribute, set the model, the parent model and the
300
     * attribute values.
301
     *
302
     * We traverse this relation array later to create the relations, for example:
303
     *
304
     * Current model HasOne Address, this Address (line_1, country_id) BelongsTo Country through country_id in Address Model.
305
     *
306
     * So when editing current model crud user have two fields address.line_1 and address.country (we infer country_id from relation)
307
     *
308
     * Those will be nested accordingly in this relation array, so address relation will have a nested relation with country.
309
     *
310
     *
311
     * @param array $data The form data.
312
     *
313
     * @return array The formatted relation data.
314
     */
315
    private function getRelationDataFromFormData($data)
316
    {
317
        // exclude the already attached belongs to relations but include nested belongs to.
318
        $relation_fields = Arr::where($this->getRelationFields(), function ($field, $key) {
319
            return $field['relation_type'] !== 'BelongsTo' || $this->isNestedRelation($field);
0 ignored issues
show
Bug introduced by
It seems like isNestedRelation() 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

319
            return $field['relation_type'] !== 'BelongsTo' || $this->/** @scrutinizer ignore-call */ isNestedRelation($field);
Loading history...
320
        });
321
322
        $relationData = [];
323
324
        foreach ($relation_fields as $relation_field) {
325
            $attributeKey = $this->parseRelationFieldNamesFromHtml([$relation_field])[0]['name'];
0 ignored issues
show
Bug introduced by
It seems like parseRelationFieldNamesFromHtml() 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
            $attributeKey = $this->/** @scrutinizer ignore-call */ parseRelationFieldNamesFromHtml([$relation_field])[0]['name'];
Loading history...
326
            if (isset($relation_field['pivot']) && $relation_field['pivot'] !== true) {
327
                $key = implode('.relations.', explode('.', $this->getOnlyRelationEntity($relation_field)));
328
                $fieldData = Arr::get($relationData, 'relations.'.$key, []);
329
                if (! array_key_exists('model', $fieldData)) {
330
                    $fieldData['model'] = $relation_field['model'];
331
                }
332
                if (! array_key_exists('parent', $fieldData)) {
333
                    $fieldData['parent'] = $this->getRelationModel($attributeKey, -1);
0 ignored issues
show
Bug introduced by
The method getRelationModel() does not exist on Backpack\CRUD\app\Library\CrudPanel\Traits\Create. Did you maybe mean getRelationFields()? ( Ignorable by Annotation )

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

333
                    /** @scrutinizer ignore-call */ 
334
                    $fieldData['parent'] = $this->getRelationModel($attributeKey, -1);

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...
334
                }
335
336
                // when using HasMany/MorphMany if fallback_id is provided instead of deleting the models
337
                // from database we resort to this fallback provided by developer
338
                if (array_key_exists('fallback_id', $relation_field)) {
339
                    $fieldData['fallback_id'] = $relation_field['fallback_id'];
340
                }
341
342
                // when using HasMany/MorphMany and column is nullable, by default backpack sets the value to null.
343
                // this allow developers to override that behavior and force deletion from database
344
                $fieldData['force_delete'] = $relation_field['force_delete'] ?? false;
345
346
                if (! array_key_exists('relation_type', $fieldData)) {
347
                    $fieldData['relation_type'] = $relation_field['relation_type'];
348
                }
349
                $relatedAttribute = Arr::last(explode('.', $attributeKey));
350
                $fieldData['values'][$relatedAttribute] = Arr::get($data, $attributeKey);
351
352
                Arr::set($relationData, 'relations.'.$key, $fieldData);
353
            }
354
        }
355
356
        return $relationData;
357
    }
358
359
    /**
360
     * When using the HasMany/MorphMany relations as selectable elements we use this function to sync those relations.
361
     * Here we allow for different functionality than when creating. Developer could use this relation as a
362
     * selectable list of items that can belong to one/none entity at any given time.
363
     *
364
     * @return void
365
     */
366
    public function attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values)
367
    {
368
        $model_instance = $relation->getRelated();
369
        $force_delete = $relationData['force_delete'];
370
        $relation_foreign_key = $relation->getForeignKeyName();
371
        $relation_local_key = $relation->getLocalKeyName();
372
373
        $relation_column_is_nullable = $model_instance->isColumnNullable($relation_foreign_key);
374
375
        if ($relation_values !== null && $relationData['values'][$relationMethod][0] !== null) {
376
            // we add the new values into the relation
377
            $model_instance->whereIn($model_instance->getKeyName(), $relation_values)
378
           ->update([$relation_foreign_key => $item->{$relation_local_key}]);
379
380
            // we clear up any values that were removed from model relation.
381
            // if developer provided a fallback id, we use it
382
            // if column is nullable we set it to null if developer didn't specify `force_delete => true`
383
            // if none of the above we delete the model from database
384
            if (isset($relationData['fallback_id'])) {
385
                $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
386
                            ->where($relation_foreign_key, $item->{$relation_local_key})
387
                            ->update([$relation_foreign_key => $relationData['fallback_id']]);
388
            } else {
389
                if (! $relation_column_is_nullable || $force_delete) {
390
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
391
                            ->where($relation_foreign_key, $item->{$relation_local_key})
392
                            ->delete();
393
                } else {
394
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
395
                            ->where($relation_foreign_key, $item->{$relation_local_key})
396
                            ->update([$relation_foreign_key => null]);
397
                }
398
            }
399
        } else {
400
            // the developer cleared the selection
401
            // we gonna clear all related values by setting up the value to the fallback id, to null or delete.
402
            if (isset($relationData['fallback_id'])) {
403
                $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
404
                            ->update([$relation_foreign_key => $relationData['fallback_id']]);
405
            } else {
406
                if (!$relation_column_is_nullable || $force_delete) {
407
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})->delete();
408
                } else {
409
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
410
                            ->update([$relation_foreign_key => null]);
411
                }
412
            }
413
        }
414
    }
415
416
    /**
417
     * Handle HasMany/MorphMany relations when used as creatable entries in the crud.
418
     * By using repeatable field, developer can allow the creation of such entries
419
     * in the crud forms.
420
     *
421
     * @return void
422
     */
423
    public function createManyEntries($entry, $relation, $relationMethod, $relationData)
424
    {
425
        $items = json_decode($relationData['values'][$relationMethod], true);
426
427
        $relation_local_key = $relation->getLocalKeyName();
428
429
        // if the collection is empty we clear all previous values in database if any.
430
        if (empty($items)) {
431
            $entry->{$relationMethod}()->sync([]);
432
        } else {
433
            $created_ids = [];
434
435
            foreach ($items as $item) {
436
                if (isset($item[$relation_local_key]) && ! empty($item[$relation_local_key])) {
437
                    $entry->{$relationMethod}()->updateOrCreate([$relation_local_key => $item[$relation_local_key]], $item);
438
                } else {
439
                    $created_ids[] = $entry->{$relationMethod}()->create($item)->{$relation_local_key};
440
                }
441
            }
442
443
            // get from $items the sent ids, and merge the ones created.
444
            $relatedItemsSent = array_merge(array_filter(Arr::pluck($items, $relation_local_key)), $created_ids);
445
446
            if (! empty($relatedItemsSent)) {
447
                // we perform the cleanup of removed database items
448
                $entry->{$relationMethod}()->whereNotIn($relation_local_key, $relatedItemsSent)->delete();
449
            }
450
        }
451
    }
452
}
453