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
13:58
created

Create::attachManyRelation()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 45
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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