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

Create::createRelationsForItem()   C

Complexity

Conditions 13
Paths 29

Size

Total Lines 58
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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

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
        $data = $this->changeBelongsToNamesFromRelationshipToForeignKey($data);
0 ignored issues
show
Bug introduced by
It seems like changeBelongsToNamesFromRelationshipToForeignKey() 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

31
        /** @scrutinizer ignore-call */ 
32
        $data = $this->changeBelongsToNamesFromRelationshipToForeignKey($data);
Loading history...
32
33
        // omit the n-n relationships when updating the eloquent item
34
        $nn_relationships = Arr::pluck($this->getRelationFieldsWithPivot(), 'name');
35
36
        $item = $this->model->create(Arr::except($data, $nn_relationships));
37
38
        // if there are any other relations create them.
39
        $this->createRelations($item, $data);
40
41
        return $item;
42
    }
43
44
    /**
45
     * Get all fields needed for the ADD NEW ENTRY form.
46
     *
47
     * @return array The fields with attributes and fake attributes.
48
     */
49
    public function getCreateFields()
50
    {
51
        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

51
        return $this->/** @scrutinizer ignore-call */ fields();
Loading history...
52
    }
53
54
    /**
55
     * Get all fields with relation set (model key set on field).
56
     *
57
     * @return array The fields with model key set.
58
     */
59
    public function getRelationFields()
60
    {
61
        $fields = $this->fields();
62
        $relationFields = [];
63
64
        foreach ($fields as $field) {
65
            if (isset($field['model']) && $field['model'] !== false) {
66
                array_push($relationFields, $field);
67
            }
68
69
            if (isset($field['subfields']) &&
70
                is_array($field['subfields']) &&
71
                count($field['subfields'])) {
72
                foreach ($field['subfields'] as $subfield) {
73
                    $subfield = $this->makeSureFieldHasNecessaryAttributes($subfield);
0 ignored issues
show
Bug introduced by
It seems like makeSureFieldHasNecessaryAttributes() 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

73
                    /** @scrutinizer ignore-call */ 
74
                    $subfield = $this->makeSureFieldHasNecessaryAttributes($subfield);
Loading history...
74
                    array_push($relationFields, $subfield);
75
                }
76
            }
77
        }
78
79
        return $relationFields;
80
    }
81
82
    /**
83
     * Get all fields with n-n relation set (pivot table is true).
84
     *
85
     * @return array The fields with n-n relationships.
86
     */
87
    public function getRelationFieldsWithPivot()
88
    {
89
        $all_relation_fields = $this->getRelationFields();
90
91
        return Arr::where($all_relation_fields, function ($value, $key) {
92
            return isset($value['pivot']) && $value['pivot'];
93
        });
94
    }
95
96
    /**
97
     * Create the relations for the current model.
98
     *
99
     * @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
100
     * @param array                               $data The form data.
101
     */
102
    public function createRelations($item, $data)
103
    {
104
        $relationData = $this->getRelationDataFromFormData($data);
105
106
        // handles 1-1 and 1-n relations (HasOne, MorphOne, HasMany, MorphMany)
107
        $this->createRelationsForItem($item, $relationData);
108
109
        // this specifically handles M-M relations that could sync additional information into pivot table
110
        $this->syncPivot($item, $data);
111
    }
112
113
    /**
114
     * Sync the declared many-to-many associations through the pivot field.
115
     *
116
     * @param \Illuminate\Database\Eloquent\Model $model The current CRUD model.
117
     * @param array                               $data  The form data.
118
     */
119
    public function syncPivot($model, $data)
120
    {
121
        $fields_with_relationships = $this->getRelationFieldsWithPivot();
122
        foreach ($fields_with_relationships as $field) {
123
            $values = isset($data[$field['name']]) ? $data[$field['name']] : [];
124
125
            // if a JSON was passed instead of an array, turn it into an array
126
            if (is_string($values)) {
127
                $decoded_values = json_decode($values, true);
128
                $values = [];
129
                // array is not multidimensional
130
                if (count($decoded_values) != count($decoded_values, COUNT_RECURSIVE)) {
131
                    foreach ($decoded_values as $value) {
132
                        $values[] = $value[$field['name']];
133
                    }
134
                } else {
135
                    $values = $decoded_values;
136
                }
137
            }
138
139
            $relation_data = [];
140
141
            foreach ($values as $pivot_id) {
142
                if ($pivot_id != '') {
143
                    $pivot_data = [];
144
145
                    if (isset($field['pivotFields'])) {
146
                        // array is not multidimensional
147
                        if (count($field['pivotFields']) == count($field['pivotFields'], COUNT_RECURSIVE)) {
148
                            foreach ($field['pivotFields'] as $pivot_field_name) {
149
                                $pivot_data[$pivot_field_name] = $data[$pivot_field_name][$pivot_id];
150
                            }
151
                        } else {
152
                            $field_data = json_decode($data[$field['name']], true);
153
154
                            // we grab from the parsed data the specific values for this pivot
155
                            $pivot_data = Arr::first($field_data, function ($item) use ($pivot_id, $field) {
156
                                return $item[$field['name']] === $pivot_id;
157
                            });
158
159
                            // we remove the relation field from extra pivot data as we already have the relation.
160
                            unset($pivot_data[$field['name']]);
161
                        }
162
                    }
163
164
                    $relation_data[$pivot_id] = $pivot_data;
165
                }
166
            }
167
168
            $model->{$field['name']}()->sync($relation_data);
169
170
            if (isset($field['morph']) && $field['morph'] && isset($data[$field['name']])) {
171
                $values = $data[$field['name']];
172
                $model->{$field['name']}()->sync($values);
173
            }
174
        }
175
    }
176
177
    /**
178
     * Handles 1-1 and 1-n relations. In case 1-1 it handles subsequent relations in connected models
179
     * For example, a Monster > HasOne Address > BelongsTo a Country.
180
     *
181
     * @param \Illuminate\Database\Eloquent\Model $item          The current CRUD model.
182
     * @param array                               $formattedData The form data.
183
     *
184
     * @return bool|null
185
     */
186
    private function createRelationsForItem($item, $formattedData)
187
    {
188
        if (! isset($formattedData['relations'])) {
189
            return false;
190
        }
191
192
        foreach ($formattedData['relations'] as $relationMethod => $relationData) {
193
            if (! isset($relationData['model'])) {
194
                continue;
195
            }
196
197
            $relation = $item->{$relationMethod}();
198
            $relation_type = get_class($relation);
199
200
            switch ($relation_type) {
201
                case HasOne::class:
202
                case MorphOne::class:
203
204
                    // we first check if there are relations of the relation
205
                    if (isset($relationData['relations'])) {
206
                        // if there are nested relations, we first add the BelongsTo like in main entry
207
                        $belongsToRelations = Arr::where($relationData['relations'], function ($relation_data) {
208
                            return $relation_data['relation_type'] == 'BelongsTo';
209
                        });
210
211
                        // adds the values of the BelongsTo relations of this entity to the array of values that will
212
                        // be saved at the same time like we do in parent entity belongs to relations
213
                        $valuesWithRelations = $this->associateHasOneBelongsTo($belongsToRelations, $relationData['values'], $relation->getModel());
214
215
                        // remove previously added BelongsTo relations from relation data.
216
                        $relationData['relations'] = Arr::where($relationData['relations'], function ($item) {
217
                            return $item['relation_type'] != 'BelongsTo';
218
                        });
219
220
                        $modelInstance = $relation->updateOrCreate([], $valuesWithRelations);
221
                    } else {
222
                        $modelInstance = $relation->updateOrCreate([], $relationData['values']);
223
                    }
224
                    break;
225
226
                case HasMany::class:
227
                case MorphMany::class:
228
                    $relation_values = $relationData['values'][$relationMethod];
229
230
                    if (is_string($relation_values)) {
231
                        $relation_values = json_decode($relationData['values'][$relationMethod], true);
232
                    }
233
234
                    if ($relation_values === null || count($relation_values) == count($relation_values, COUNT_RECURSIVE)) {
235
                        $this->attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values);
236
                    } else {
237
                        $this->createManyEntries($item, $relation, $relationMethod, $relationData);
238
                    }
239
                    break;
240
            }
241
242
            if (isset($relationData['relations'])) {
243
                $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...
244
            }
245
        }
246
    }
247
248
    /**
249
     * Associate the nested HasOne -> BelongsTo relations by adding the "connecting key"
250
     * to the array of values that is going to be saved with HasOne relation.
251
     *
252
     * @param array $belongsToRelations
253
     * @param array $modelValues
254
     * @param Model $relationInstance
255
     * @return array
256
     */
257
    private function associateHasOneBelongsTo($belongsToRelations, $modelValues, $modelInstance)
258
    {
259
        foreach ($belongsToRelations as $methodName => $values) {
260
            $relation = $modelInstance->{$methodName}();
261
262
            $modelValues[$relation->getForeignKeyName()] = $values['values'][$methodName];
263
        }
264
265
        return $modelValues;
266
    }
267
268
    /**
269
     * Get a relation data array from the form data.
270
     * For each relation defined in the fields through the entity attribute, set the model, the parent model and the
271
     * attribute values.
272
     *
273
     * We traverse this relation array later to create the relations, for example:
274
     *
275
     * Current model HasOne Address, this Address (line_1, country_id) BelongsTo Country through country_id in Address Model.
276
     *
277
     * So when editing current model crud user have two fields address.line_1 and address.country (we infer country_id from relation)
278
     *
279
     * Those will be nested accordingly in this relation array, so address relation will have a nested relation with country.
280
     *
281
     *
282
     * @param array $data The form data.
283
     *
284
     * @return array The formatted relation data.
285
     */
286
    private function getRelationDataFromFormData($data)
287
    {
288
        // exclude the already attached belongs to relations but include nested belongs to.
289
        $relation_fields = Arr::where($this->getRelationFields(), function ($field, $key) {
290
            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

290
            return $field['relation_type'] !== 'BelongsTo' || $this->/** @scrutinizer ignore-call */ isNestedRelation($field);
Loading history...
291
        });
292
293
        $relationData = [];
294
295
        foreach ($relation_fields as $relation_field) {
296
            $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

296
            $attributeKey = $this->/** @scrutinizer ignore-call */ parseRelationFieldNamesFromHtml([$relation_field])[0]['name'];
Loading history...
297
298
            if ((! is_null(Arr::get($data, $attributeKey)) || $this->isNestedRelation($relation_field)) && isset($relation_field['pivot']) && $relation_field['pivot'] !== true) {
299
                $key = implode('.relations.', explode('.', $this->getOnlyRelationEntity($relation_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

299
                $key = implode('.relations.', explode('.', $this->/** @scrutinizer ignore-call */ getOnlyRelationEntity($relation_field)));
Loading history...
300
                $fieldData = Arr::get($relationData, 'relations.'.$key, []);
301
                if (! array_key_exists('model', $fieldData)) {
302
                    $fieldData['model'] = $relation_field['model'];
303
                }
304
                if (! array_key_exists('parent', $fieldData)) {
305
                    $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

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