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
11:29
created

Create::createRelations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 9
rs 10
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
171
            $model->{$field['name']}()->sync($relation_data);
172
173
            if (isset($field['morph']) && $field['morph'] && isset($data[$field['name']])) {
174
                $values = $data[$field['name']];
175
                $model->{$field['name']}()->sync($values);
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
228
                case HasMany::class:
229
                case MorphMany::class:
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