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 Cristian
12:28
created

Create::createManyEntries()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 3
nop 4
dl 0
loc 25
rs 9.3888
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\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);
0 ignored issues
show
Bug introduced by
It seems like associateOrDissociateBelongsToRelations() 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

39
        /** @scrutinizer ignore-call */ 
40
        $item = $this->associateOrDissociateBelongsToRelations($item, $data);
Loading history...
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
        $this->syncPivot($item, $data);
108
        $this->createOneToOneRelations($item, $data);
109
    }
110
111
    /**
112
     * Sync the declared many-to-many associations through the pivot field.
113
     *
114
     * @param \Illuminate\Database\Eloquent\Model $model The current CRUD model.
115
     * @param array                               $data  The form data.
116
     */
117
    public function syncPivot($model, $data)
118
    {
119
        $fields_with_relationships = $this->getRelationFieldsWithPivot();
120
        foreach ($fields_with_relationships as $key => $field) {
121
            $values = isset($data[$field['name']]) ? $data[$field['name']] : [];
122
123
            // if a JSON was passed instead of an array, turn it into an array
124
            if (is_string($values)) {
125
                $decoded_values = json_decode($values, true);
126
                $values = [];
127
                //array is not multidimensional
128
                if (count($decoded_values) != count($decoded_values, COUNT_RECURSIVE)) {
129
                    foreach ($decoded_values as $value) {
130
                        $values[] = $value[$field['name']];
131
                    }
132
                } else {
133
                    $values = $decoded_values;
134
                }
135
            }
136
137
            $relation_data = [];
138
139
            foreach ($values as $pivot_id) {
140
                if ($pivot_id != '') {
141
                    $pivot_data = [];
142
143
                    if (isset($field['pivotFields'])) {
144
                        //array is not multidimensional
145
                        if (count($field['pivotFields']) == count($field['pivotFields'], COUNT_RECURSIVE)) {
146
                            foreach ($field['pivotFields'] as $pivot_field_name) {
147
                                $pivot_data[$pivot_field_name] = $data[$pivot_field_name][$pivot_id];
148
                            }
149
                        } else {
150
                            $field_data = json_decode($data[$field['name']], true);
151
152
                            //we grab from the parsed data the specific values for this pivot
153
                            $pivot_data = Arr::first(Arr::where($field_data, function ($item) use ($pivot_id, $field) {
154
                                return $item[$field['name']] === $pivot_id;
155
                            }));
156
157
                            //we remove the relation field from extra pivot data as we already have the relation.
158
                            unset($pivot_data[$field['name']]);
159
                        }
160
                    }
161
162
                    $relation_data[$pivot_id] = $pivot_data;
163
                }
164
165
                $model->{$field['name']}()->sync($relation_data);
166
167
                if (isset($field['morph']) && $field['morph'] && isset($data[$field['name']])) {
168
                    $values = $data[$field['name']];
169
                    $model->{$field['name']}()->sync($values);
170
                }
171
            }
172
        }
173
    }
174
175
    /**
176
     * Create any existing one to one relations and subsquent relations for the item.
177
     *
178
     * @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
179
     * @param array                               $data The form data.
180
     */
181
    private function createOneToOneRelations($item, $data)
182
    {
183
        $relationData = $this->getRelationDataFromFormData($data);
184
        $this->createRelationsForItem($item, $relationData);
185
    }
186
187
    /**
188
     * Create any existing one to one relations and subsquent relations from form data.
189
     *
190
     * @param \Illuminate\Database\Eloquent\Model $item          The current CRUD model.
191
     * @param array                               $formattedData The form data.
192
     *
193
     * @return bool|null
194
     */
195
    private function createRelationsForItem($item, $formattedData)
196
    {
197
        if (! isset($formattedData['relations'])) {
198
            return false;
199
        }
200
201
        foreach ($formattedData['relations'] as $relationMethod => $relationData) {
202
            if (! isset($relationData['model'])) {
203
                continue;
204
            }
205
206
            $relation = $item->{$relationMethod}();
207
            $relation_type = (new \ReflectionClass($relation))->getShortName();
208
209
            switch ($relation_type) {
210
                case 'HasOne':
211
                case 'MorphOne':
212
                    // we first check if there are relations of the relation
213
                    if (isset($relationData['relations'])) {
214
                        $belongsToRelations = Arr::where($relationData['relations'], function ($relation_data) {
215
                            return $relation_data['relation_type'] == 'BelongsTo';
216
                        });
217
                        // adds the values of the BelongsTo relations of this entity to the array of values that will
218
                        // be saved at the same time like we do in parent entity belongs to relations
219
                        $valuesWithRelations = $this->associateHasOneBelongsTo($belongsToRelations, $relationData['values'], $relation->getModel());
220
221
                        $relationData['relations'] = Arr::where($relationData['relations'], function ($item) {
222
                            return $item['relation_type'] != 'BelongsTo';
223
                        });
224
225
                        $modelInstance = $relation->updateOrCreate([], $valuesWithRelations);
226
                    } else {
227
                        $modelInstance = $relation->updateOrCreate([], $relationData['values']);
228
                    }
229
                break;
230
            }
231
232
            if ($relation instanceof HasOne || $relation instanceof MorphOne) {
233
                if (isset($relationData['relations'])) {
234
                    $belongsToRelations = Arr::where($relationData['relations'], function ($relation_data) {
235
                        return $relation_data['relation_type'] == 'BelongsTo';
236
                    });
237
                    // adds the values of the BelongsTo relations of this entity to the array of values that will
238
                    // be saved at the same time like we do in parent entity belongs to relations
239
                    $valuesWithRelations = $this->associateHasOneBelongsTo($belongsToRelations, $relationData['values'], $relation->getModel());
240
241
                    $relationData['relations'] = Arr::where($relationData['relations'], function ($item) {
242
                        return $item['relation_type'] != 'BelongsTo';
243
                    });
244
245
                    $modelInstance = $relation->updateOrCreate([], $valuesWithRelations);
246
                } else {
247
                    $modelInstance = $relation->updateOrCreate([], $relationData['values']);
248
                }
249
            } elseif ($relation instanceof HasMany || $relation instanceof MorphMany) {
250
                $relation_values = $relationData['values'][$relationMethod];
251
252
                if (is_string($relation_values)) {
253
                    $relation_values = json_decode($relationData['values'][$relationMethod], true);
254
                }
255
256
                if (is_null($relation_values) || count($relation_values) == count($relation_values, COUNT_RECURSIVE)) {
257
                    $this->attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values);
258
                } else {
259
                    $this->createManyEntries($item, $relation, $relationMethod, $relationData);
260
                }
261
            }
262
263
            if (isset($relationData['relations'])) {
264
                $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...
265
            }
266
        }
267
    }
268
269
    private function associateHasOneBelongsTo($belongsToRelations, $modelValues, $modelInstance)
270
    {
271
        foreach ($belongsToRelations as $methodName => $values) {
272
            $relation = $modelInstance->{$methodName}();
273
            $modelValues[$relation->getForeignKeyName()] = $values['values'][$methodName];
274
        }
275
276
        return $modelValues;
277
    }
278
279
    /**
280
     * Get a relation data array from the form data.
281
     * For each relation defined in the fields through the entity attribute, set the model, the parent model and the
282
     * attribute values.
283
     *
284
     * We traverse this relation array later to create the relations, for example:
285
     *
286
     * Current model HasOne Address, this Address (line_1, country_id) BelongsTo Country through country_id in Address Model.
287
     *
288
     * So when editing current model crud user have two fields address.line_1 and address.country (we infer country_id from relation)
289
     *
290
     * Those will be nested accordingly in this relation array, so address relation will have a nested relation with country.
291
     *
292
     *
293
     * @param array $data The form data.
294
     *
295
     * @return array The formatted relation data.
296
     */
297
    private function getRelationDataFromFormData($data)
298
    {
299
        // exclude the already attached belongs to relations but include nested belongs to.
300
        $relation_fields = Arr::where($this->getRelationFields(), function ($field, $key) {
301
            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

301
            return $field['relation_type'] !== 'BelongsTo' || $this->/** @scrutinizer ignore-call */ isNestedRelation($field);
Loading history...
302
        });
303
304
        $relationData = [];
305
306
        foreach ($relation_fields as $relation_field) {
307
            $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

307
            $attributeKey = $this->/** @scrutinizer ignore-call */ parseRelationFieldNamesFromHtml([$relation_field])[0]['name'];
Loading history...
308
            if (isset($relation_field['pivot']) && $relation_field['pivot'] !== true) {
309
                $key = implode('.relations.', explode('.', $this->getOnlyRelationEntity($relation_field)));
310
                $fieldData = Arr::get($relationData, 'relations.'.$key, []);
311
                if (! array_key_exists('model', $fieldData)) {
312
                    $fieldData['model'] = $relation_field['model'];
313
                }
314
                if (! array_key_exists('parent', $fieldData)) {
315
                    $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

315
                    /** @scrutinizer ignore-call */ 
316
                    $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...
316
                }
317
318
                // when using HasMany/MorphMany if fallback_id is provided instead of deleting the models
319
                // from database we resort to this fallback provided by developer
320
                if (array_key_exists('fallback_id', $relation_field)) {
321
                    $fieldData['fallback_id'] = $relation_field['fallback_id'];
322
                }
323
324
                // when using HasMany/MorphMany and column is nullable, by default backpack sets the value to null.
325
                // this allow developers to override that behavior and force deletion from database
326
                $fieldData['force_delete'] = $relation_field['force_delete'] ?? false;
327
328
                if (! array_key_exists('relation_type', $fieldData)) {
329
                    $fieldData['relation_type'] = $relation_field['relation_type'];
330
                }
331
                $relatedAttribute = Arr::last(explode('.', $attributeKey));
332
                $fieldData['values'][$relatedAttribute] = Arr::get($data, $attributeKey);
333
334
                Arr::set($relationData, 'relations.'.$key, $fieldData);
335
            }
336
        }
337
338
        return $relationData;
339
    }
340
341
    /**
342
     * Return the relation without any model attributes there.
343
     * Eg. user.entity_id would return user, as entity_id is not a relation in user.
344
     *
345
     * @param array $relation_field
346
     * @return string
347
     */
348
    public function getOnlyRelationEntity($relation_field)
349
    {
350
        $entity_array = explode('.', $relation_field['entity']);
351
352
        $relation_model = $this->getRelationModel($relation_field['entity'], -1);
353
354
        $related_method = Arr::last($entity_array);
355
356
        if (! method_exists($relation_model, $related_method)) {
357
            if (count($entity_array) <= 1) {
358
                return $relation_field['entity'];
359
            } else {
360
                array_pop($entity_array);
361
            }
362
363
            return implode('.', $entity_array);
364
        }
365
366
        return $relation_field['entity'];
367
    }
368
369
    /**
370
     * When using the HasMany/MorphMany relations as selectable elements we use this function to sync those relations.
371
     * Here we allow for different functionality than when creating. Developer could use this relation as a
372
     * selectable list of items that can belong to one/none entity at any given time.
373
     *
374
     * @return void
375
     */
376
    public function attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values)
377
    {
378
        $model_instance = $relation->getRelated();
379
        $force_delete = $relationData['force_delete'];
380
        $relation_foreign_key = $relation->getForeignKeyName();
381
        $relation_local_key = $relation->getLocalKeyName();
382
383
        $relation_column_is_nullable = $model_instance->isColumnNullable($relation_foreign_key);
384
385
        if (! is_null($relation_values) && $relationData['values'][$relationMethod][0] !== null) {
386
            //we add the new values into the relation
387
            $model_instance->whereIn($model_instance->getKeyName(), $relation_values)
388
           ->update([$relation_foreign_key => $item->{$relation_local_key}]);
389
390
            //we clear up any values that were removed from model relation.
391
            //if developer provided a fallback id, we use it
392
            //if column is nullable we set it to null
393
            //if none of the above we delete the model from database
394
            if (isset($relationData['fallback_id'])) {
395
                $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
396
                            ->where($relation_foreign_key, $item->{$relation_local_key})
397
                            ->update([$relation_foreign_key => $relationData['fallback_id']]);
398
            } else {
399
                if (! $relation_column_is_nullable || $force_delete) {
400
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
401
                            ->where($relation_foreign_key, $item->{$relation_local_key})
402
                            ->delete();
403
                } else {
404
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
405
                            ->where($relation_foreign_key, $item->{$relation_local_key})
406
                            ->update([$relation_foreign_key => null]);
407
                }
408
            }
409
        } else {
410
            //the developer cleared the selection
411
            //we gonna clear all related values by setting up the value to the fallback id, to null or delete.
412
            if (isset($relationData['fallback_id'])) {
413
                $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
414
                            ->update([$relation_foreign_key => $relationData['fallback_id']]);
415
            } else {
416
                if (! $relation_column_is_nullable || $force_delete) {
417
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})->delete();
418
                } else {
419
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
420
                            ->update([$relation_foreign_key => null]);
421
                }
422
            }
423
        }
424
    }
425
426
    /**
427
     * Handle HasMany/MorphMany relations when used as creatable entries in the crud.
428
     * By using repeatable field, developer can allow the creation of such entries
429
     * in the crud forms.
430
     *
431
     * @return void
432
     */
433
    public function createManyEntries($entry, $relation, $relationMethod, $relationData)
434
    {
435
        $items = collect(json_decode($relationData['values'][$relationMethod], true));
436
        $relatedModel = $relation->getRelated();
437
        $itemsInDatabase = $entry->{$relationMethod};
0 ignored issues
show
Unused Code introduced by
The assignment to $itemsInDatabase is dead and can be removed.
Loading history...
438
        //if the collection is empty we clear all previous values in database if any.
439
        if ($items->isEmpty()) {
440
            $entry->{$relationMethod}()->sync([]);
441
        } else {
442
            $items->each(function (&$item, $key) use ($relatedModel, $entry, $relationMethod) {
443
                if (isset($item[$relatedModel->getKeyName()])) {
444
                    $entry->{$relationMethod}()->updateOrCreate([$relatedModel->getKeyName() => $item[$relatedModel->getKeyName()]], $item);
445
                } else {
446
                    $entry->{$relationMethod}()->updateOrCreate([], $item);
447
                }
448
            });
449
450
            $relatedItemsSent = $items->pluck($relatedModel->getKeyName());
451
452
            if (! $relatedItemsSent->isEmpty()) {
453
                $itemsInDatabase = $entry->{$relationMethod};
454
                //we perform the cleanup of removed database items
455
                $itemsInDatabase->each(function ($item, $key) use ($relatedItemsSent) {
456
                    if (! $relatedItemsSent->contains($item->getKey())) {
457
                        $item->delete();
458
                    }
459
                });
460
            }
461
        }
462
    }
463
}
464