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 (#3747)
by Cristian
14:14
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\Relations\BelongsTo;
6
use Illuminate\Database\Eloquent\Relations\HasMany;
7
use Illuminate\Database\Eloquent\Relations\HasOne;
8
use Illuminate\Support\Arr;
9
10
trait Create
11
{
12
    /*
13
    |--------------------------------------------------------------------------
14
    |                                   CREATE
15
    |--------------------------------------------------------------------------
16
    */
17
18
    /**
19
     * Insert a row in the database.
20
     *
21
     * @param array $data All input values to be inserted.
22
     *
23
     * @return \Illuminate\Database\Eloquent\Model
24
     */
25
    public function create($data)
26
    {
27
        $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

27
        /** @scrutinizer ignore-call */ 
28
        $data = $this->decodeJsonCastedAttributes($data);
Loading history...
28
        $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

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

47
        return $this->/** @scrutinizer ignore-call */ fields();
Loading history...
48
    }
49
50
    /**
51
     * Get all fields with relation set (model key set on field).
52
     *
53
     * @return array The fields with model key set.
54
     */
55
    public function getRelationFields()
56
    {
57
        $fields = $this->fields();
58
        $relationFields = [];
59
60
        foreach ($fields as $field) {
61
            if (isset($field['model']) && $field['model'] !== false) {
62
                array_push($relationFields, $field);
63
            }
64
65
            if (isset($field['subfields']) &&
66
                is_array($field['subfields']) &&
67
                count($field['subfields'])) {
68
                foreach ($field['subfields'] as $subfield) {
69
                    array_push($relationFields, $subfield);
70
                }
71
            }
72
        }
73
74
        return $relationFields;
75
    }
76
77
    /**
78
     * Get all fields with n-n relation set (pivot table is true).
79
     *
80
     * @return array The fields with n-n relationships.
81
     */
82
    public function getRelationFieldsWithPivot()
83
    {
84
        $all_relation_fields = $this->getRelationFields();
85
86
        return Arr::where($all_relation_fields, function ($value, $key) {
87
            return isset($value['pivot']) && $value['pivot'];
88
        });
89
    }
90
91
    /**
92
     * Create the relations for the current model.
93
     *
94
     * @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
95
     * @param array                               $data The form data.
96
     */
97
    public function createRelations($item, $data)
98
    {
99
        $this->syncPivot($item, $data);
100
        $this->createOneToOneRelations($item, $data);
101
    }
102
103
    /**
104
     * Sync the declared many-to-many associations through the pivot field.
105
     *
106
     * @param \Illuminate\Database\Eloquent\Model $model The current CRUD model.
107
     * @param array                               $data  The form data.
108
     */
109
    public function syncPivot($model, $data)
110
    {
111
        $fields_with_relationships = $this->getRelationFields();
112
        foreach ($fields_with_relationships as $key => $field) {
113
            if (isset($field['pivot']) && $field['pivot']) {
114
                $values = isset($data[$field['name']]) ? $data[$field['name']] : [];
115
116
                // if a JSON was passed instead of an array, turn it into an array
117
                if (is_string($values)) {
118
                    $values = json_decode($values);
119
                }
120
121
                $relation_data = [];
122
                foreach ($values as $pivot_id) {
123
                    $pivot_data = [];
124
125
                    if (isset($field['pivotFields'])) {
126
                        foreach ($field['pivotFields'] as $pivot_field_name) {
127
                            $pivot_data[$pivot_field_name] = $data[$pivot_field_name][$pivot_id];
128
                        }
129
                    }
130
                    $relation_data[$pivot_id] = $pivot_data;
131
                }
132
133
                $model->{$field['name']}()->sync($relation_data);
134
            }
135
136
            if (isset($field['morph']) && $field['morph'] && isset($data[$field['name']])) {
137
                $values = $data[$field['name']];
138
                $model->{$field['name']}()->sync($values);
139
            }
140
        }
141
    }
142
143
    /**
144
     * Create any existing one to one relations for the current model from the form data.
145
     *
146
     * @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
147
     * @param array                               $data The form data.
148
     */
149
    private function createOneToOneRelations($item, $data)
150
    {
151
        $relationData = $this->getRelationDataFromFormData($data);
152
        $this->createRelationsForItem($item, $relationData);
153
    }
154
155
    /**
156
     * Create any existing one to one relations for the current model from the relation data.
157
     *
158
     * @param \Illuminate\Database\Eloquent\Model $item          The current CRUD model.
159
     * @param array                               $formattedData The form data.
160
     *
161
     * @return bool|null
162
     */
163
    private function createRelationsForItem($item, $formattedData)
164
    {
165
        if (! isset($formattedData['relations'])) {
166
            return false;
167
        }
168
        foreach ($formattedData['relations'] as $relationMethod => $relationData) {
169
            if (! isset($relationData['model'])) {
170
                continue;
171
            }
172
            $model = $relationData['model'];
173
            $relation = $item->{$relationMethod}();
174
175
            if ($relation instanceof BelongsTo) {
176
                $modelInstance = $model::find($relationData['values'])->first();
177
                if ($modelInstance != null) {
178
                    $relation->associate($modelInstance)->save();
179
                } else {
180
                    $relation->dissociate()->save();
181
                }
182
            } elseif ($relation instanceof HasOne) {
183
                if ($item->{$relationMethod} != null) {
184
                    $item->{$relationMethod}->update($relationData['values']);
185
                    $modelInstance = $item->{$relationMethod};
186
                } else {
187
                    $modelInstance = new $model($relationData['values']);
188
                    $relation->save($modelInstance);
189
                }
190
            } elseif ($relation instanceof HasMany) {
191
                $relation_values = $relationData['values'][$relationMethod];
192
                $this->attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values);
193
            }
194
195
            if (isset($relationData['relations'])) {
196
                $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...
197
            }
198
        }
199
    }
200
201
    /**
202
     * When using the HasMany/MorphMany relations as selectable elements we use this function to sync those relations.
203
     * Here we allow for different functionality than when creating. Developer could use this relation as a
204
     * selectable list of items that can belong to one/none entity at any given time.
205
     *
206
     * @return void
207
     */
208
    public function attachManyRelation($item, $relation, $relationMethod, $relationData, $relation_values)
209
    {
210
        $model_instance = $relation->getRelated();
211
        $force_delete = $relationData['force_delete'] ?? false;
212
        $relation_foreign_key = $relation->getForeignKeyName();
213
        $relation_local_key = $relation->getLocalKeyName();
214
215
        $relation_column_is_nullable = $model_instance->isColumnNullable($relation_foreign_key);
216
217
        if ($relation_values !== null && $relationData['values'][$relationMethod][0] !== null) {
218
            // we add the new values into the relation
219
            $model_instance->whereIn($model_instance->getKeyName(), $relation_values)
220
                ->update([$relation_foreign_key => $item->{$relation_local_key}]);
221
222
            // we clear up any values that were removed from model relation.
223
            // if developer provided a fallback id, we use it
224
            // if column is nullable we set it to null if developer didn't specify `force_delete => true`
225
            // if none of the above we delete the model from database
226
            if (isset($relationData['fallback_id'])) {
227
                $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
228
                    ->where($relation_foreign_key, $item->{$relation_local_key})
229
                    ->update([$relation_foreign_key => $relationData['fallback_id']]);
230
            } else {
231
                if (! $relation_column_is_nullable || $force_delete) {
232
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
233
                        ->where($relation_foreign_key, $item->{$relation_local_key})
234
                        ->delete();
235
                } else {
236
                    $model_instance->whereNotIn($model_instance->getKeyName(), $relation_values)
237
                        ->where($relation_foreign_key, $item->{$relation_local_key})
238
                        ->update([$relation_foreign_key => null]);
239
                }
240
            }
241
        } else {
242
            // the developer cleared the selection
243
            // we gonna clear all related values by setting up the value to the fallback id, to null or delete.
244
            if (isset($relationData['fallback_id'])) {
245
                $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
246
                    ->update([$relation_foreign_key => $relationData['fallback_id']]);
247
            } else {
248
                if (! $relation_column_is_nullable || $force_delete) {
249
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})->delete();
250
                } else {
251
                    $model_instance->where($relation_foreign_key, $item->{$relation_local_key})
252
                        ->update([$relation_foreign_key => null]);
253
                }
254
            }
255
        }
256
    }
257
258
    /**
259
     * Get a relation data array from the form data.
260
     * For each relation defined in the fields through the entity attribute, set the model, the parent model and the
261
     * attribute values.
262
     *
263
     * We traverse this relation array later to create the relations, for example:
264
     *
265
     * Current model HasOne Address, this Address (line_1, country_id) BelongsTo Country through country_id in Address Model.
266
     *
267
     * So when editing current model crud user have two fields address.line_1 and address.country (we infer country_id from relation)
268
     *
269
     * Those will be nested accordingly in this relation array, so address relation will have a nested relation with country.
270
     *
271
     *
272
     * @param array $data The form data.
273
     *
274
     * @return array The formatted relation data.
275
     */
276
    private function getRelationDataFromFormData($data)
277
    {
278
        $relation_fields = $this->getRelationFields();
279
280
        $relation_fields = $this->parseRelationFieldNamesFromHtml($relation_fields);
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

280
        /** @scrutinizer ignore-call */ 
281
        $relation_fields = $this->parseRelationFieldNamesFromHtml($relation_fields);
Loading history...
281
282
        //remove fields that are not in the submitted form.
283
        $relation_fields = array_filter($relation_fields, function ($item) use ($data) {
284
            return Arr::has($data, $item['name']);
285
        });
286
287
        $relationData = [];
288
        foreach ($relation_fields as $relation_field) {
289
            $attributeKey = $relation_field['name'];
290
291
            if (isset($relation_field['pivot']) && $relation_field['pivot'] !== true) {
292
                $key = implode('.relations.', explode('.', $this->getOnlyRelationEntity($relation_field)));
293
                $fieldData = Arr::get($relationData, 'relations.'.$key, []);
294
                if (! array_key_exists('model', $fieldData)) {
295
                    $fieldData['model'] = $relation_field['model'];
296
                }
297
                if (! array_key_exists('parent', $fieldData)) {
298
                    $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

298
                    /** @scrutinizer ignore-call */ 
299
                    $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...
299
                }
300
                $relatedAttribute = Arr::last(explode('.', $attributeKey));
301
                $fieldData['values'][$relatedAttribute] = Arr::get($data, $attributeKey);
302
303
                Arr::set($relationData, 'relations.'.$key, $fieldData);
304
            }
305
        }
306
307
        return $relationData;
308
    }
309
310
    public function getOnlyRelationEntity($relation_field)
311
    {
312
        $entity_array = explode('.', $relation_field['entity']);
313
314
        $relation_model = $this->getRelationModel($relation_field['entity'], -1);
315
316
        $related_method = Arr::last($entity_array);
317
318
        if (! method_exists($relation_model, $related_method)) {
319
            if (count($entity_array) <= 1) {
320
                return $relation_field['entity'];
321
            } else {
322
                array_pop($entity_array);
323
            }
324
325
            return implode('.', $entity_array);
326
        }
327
328
        return $relation_field['entity'];
329
    }
330
}
331