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 Setup Failed
Pull Request — master (#4161)
by
unknown
15:22
created

Create::syncPivot()   C

Complexity

Conditions 12
Paths 11

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 17
nc 11
nop 2
dl 0
loc 30
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;
4
5
use Illuminate\Support\Arr;
6
7
trait Create
8
{
9
    /*
10
    |--------------------------------------------------------------------------
11
    |                                   CREATE
12
    |--------------------------------------------------------------------------
13
    */
14
15
    /**
16
     * Insert a row in the database.
17
     *
18
     * @param  array  $input  All input values to be inserted.
19
     * @return \Illuminate\Database\Eloquent\Model
20
     */
21
    public function create($input)
22
    {
23
        [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($input);
0 ignored issues
show
Bug introduced by
It seems like splitInputIntoDirectAndRelations() 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

23
        /** @scrutinizer ignore-call */ 
24
        [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($input);
Loading history...
24
        $item = $this->model->create($directInputs);
25
        $this->createRelationsForItem($item, $relationInputs);
26
27
        return $item;
28
    }
29
30
    /**
31
     * Get all fields needed for the ADD NEW ENTRY form.
32
     *
33
     * @return array The fields with attributes and fake attributes.
34
     */
35
    public function getCreateFields()
36
    {
37
        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

37
        return $this->/** @scrutinizer ignore-call */ fields();
Loading history...
38
    }
39
40
    /**
41
     * Get all fields with relation set (model key set on field).
42
     *
43
     * @param  array  $fields
44
     * @return array The fields with model key set.
45
     */
46
    public function getRelationFields($fields = [])
47
    {
48
        if (empty($fields)) {
49
            $fields = $this->getCleanStateFields();
0 ignored issues
show
Bug introduced by
It seems like getCleanStateFields() 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

49
            /** @scrutinizer ignore-call */ 
50
            $fields = $this->getCleanStateFields();
Loading history...
50
        }
51
52
        $relationFields = [];
53
54
        foreach ($fields as $field) {
55
            if (isset($field['model']) && $field['model'] !== false) {
56
                array_push($relationFields, $field);
57
            }
58
59
            // if a field has an array name AND subfields
60
            // then take those fields into account (check if they have relationships);
61
            // this is done in particular for the checklist_dependency field,
62
            // but other fields could use it too, in the future;
63
            if (is_array($field['name']) &&
64
                isset($field['subfields']) &&
65
                is_array($field['subfields']) &&
66
                count($field['subfields'])) {
67
                foreach ($field['subfields'] as $subfield) {
68
                    if (isset($subfield['model']) && $subfield['model'] !== false) {
69
                        array_push($relationFields, $subfield);
70
                    }
71
                }
72
            }
73
        }
74
75
        return $relationFields;
76
    }
77
78
    /**
79
     * ---------------
80
     * PRIVATE METHODS
81
     * ---------------.
82
     */
83
84
    /**
85
     * Create relations for the provided model.
86
     *
87
     * @param  \Illuminate\Database\Eloquent\Model  $item  The current CRUD model.
88
     * @param  array  $formattedRelations  The form data.
89
     * @return bool|null
90
     */
91
    private function createRelationsForItem($item, $formattedRelations)
92
    {
93
        // no relations to create
94
        if (empty($formattedRelations)) {
95
            return false;
96
        }
97
98
        foreach ($formattedRelations as $relationMethod => $relationDetails) {
99
            $relation = $item->{$relationMethod}();
100
            $relationType = $relationDetails['relation_type'];
101
102
            switch ($relationType) {
103
                case 'HasOne':
104
                case 'MorphOne':
105
                        $this->createUpdateOrDeleteOneToOneRelation($relation, $relationMethod, $relationDetails);
106
                    break;
107
                case 'HasMany':
108
                case 'MorphMany':
109
                    $relationValues = $relationDetails['values'][$relationMethod];
110
                    // if relation values are null we can only attach, also we check if we sent
111
                    // - a single dimensional array: [1,2,3]
112
                    // - an array of arrays: [[1][2][3]]
113
                    // if is as single dimensional array we can only attach.
114
                    if ($relationValues === null || ! is_multidimensional_array($relationValues)) {
115
                        $this->attachManyRelation($item, $relation, $relationDetails, $relationValues);
116
                    } else {
117
                        $this->createManyEntries($item, $relation, $relationMethod, $relationDetails);
118
                    }
119
                    break;
120
                case 'BelongsToMany':
121
                case 'MorphToMany':
122
                    $values = $relationDetails['values'][$relationMethod] ?? [];
123
                    $values = is_string($values) ? json_decode($values, true) : $values;
124
                    $relationValues = [];
125
126
                    if (is_array($values) && is_multidimensional_array($values)) {
127
                        foreach ($values as $value) {
128
                            if (isset($value[$relationMethod])) {
129
                                $relationValues[$value[$relationMethod]] = Arr::except($value, $relationMethod);
130
                            }
131
                        }
132
                    }
133
134
                    // if there is no relation data, and the values array is single dimensional we have
135
                    // an array of keys with no aditional pivot data. sync those.
136
                    if (empty($relationValues)) {
137
                        $relationValues = array_values($values);
138
                    }
139
140
                    $item->{$relationMethod}()->sync($relationValues);
141
                    break;
142
            }
143
        }
144
    }
145
146
    /**
147
     * Save the attributes of a given HasOne or MorphOne relationship on the
148
     * related entry, create or delete it, depending on what was sent in the form.
149
     *
150
     * For HasOne and MorphOne relationships, the dev might want to a few different things:
151
     * (A) save an attribute on the related entry (eg. passport.number)
152
     * (B) set an attribute on the related entry to NULL (eg. slug.slug)
153
     * (C) save an entire related entry (eg. passport)
154
     * (D) delete the entire related entry (eg. passport)
155
     *
156
     * @param  \Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\MorphOne  $relation
157
     * @param  string  $relationMethod  The name of the relationship method on the main Model.
158
     * @param  array  $relationDetails  Details about that relationship. For example:
159
     *                                  [
160
     *                                  'model' => 'App\Models\Passport',
161
     *                                  'parent' => 'App\Models\Pet',
162
     *                                  'entity' => 'passport',
163
     *                                  'attribute' => 'passport',
164
     *                                  'values' => **THE TRICKY BIT**,
165
     *                                  ]
166
     * @return Model|null
0 ignored issues
show
Bug introduced by
The type Backpack\CRUD\app\Library\CrudPanel\Traits\Model was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
167
     */
168
    private function createUpdateOrDeleteOneToOneRelation($relation, $relationMethod, $relationDetails)
169
    {
170
        // Let's see which scenario we're treating, depending on the contents of $relationDetails:
171
        //      - (A) ['number' => 1315, 'name' => 'Something'] (if passed using a text/number/etc field)
172
        //      - (B) ['slug' => null] (if the 'slug' attribute on the 'slug' related entry needs to be cleared)
173
        //      - (C) ['passport' => [['number' => 1314, 'name' => 'Something']]] (if passed using a repeatable field)
174
        //      - (D) ['passport' => null] (if deleted from the repeatable field)
175
176
        // Scenario C or D
177
        if (array_key_exists($relationMethod, $relationDetails['values'])) {
178
            $relationMethodValue = $relationDetails['values'][$relationMethod];
179
180
            // Scenario D
181
            if (is_null($relationMethodValue) && $relationDetails['entity'] === $relationMethod) {
182
                $relation->delete();
183
184
                return null;
185
            }
186
187
            // Scenario C (when it's an array inside an array, because it's been added as one item inside a repeatable field)
188
            if (gettype($relationMethodValue) == 'array' && is_multidimensional_array($relationMethodValue)) {
189
                $relationMethodValue = $relationMethodValue[0];
190
            }
191
        }
192
        // saving process
193
        $input = $relationMethodValue ?? $relationDetails['values'];
194
        [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($input, $relationDetails, $relationMethod);
195
196
        $item = $relation->updateOrCreate([], $directInputs);
197
198
        $this->createRelationsForItem($item, $relationInputs);
199
200
        return $item;
201
    }
202
203
    /**
204
     * When using the HasMany/MorphMany relations as selectable elements we use this function to "mimic-sync" in those relations.
205
     * Since HasMany/MorphMany does not have the `sync` method, we manually re-create it.
206
     * Here we add the entries that developer added and remove the ones that are not in the list.
207
     * This removal process happens with the following rules:
208
     * - by default Backpack will behave like a `sync` from M-M relations: it deletes previous entries and add only the current ones.
209
     * - `force_delete` is configurable in the field, it's `true` by default. When false, if connecting column is nullable instead of deleting the row we set the column to null.
210
     * - `fallback_id` could be provided. In this case instead of deleting we set the connecting key to whatever developer gives us.
211
     *
212
     * @return mixed
213
     */
214
    private function attachManyRelation($item, $relation, $relationDetails, $relationValues)
215
    {
216
        $modelInstance = $relation->getRelated();
217
        $relationForeignKey = $relation->getForeignKeyName();
218
        $relationLocalKey = $relation->getLocalKeyName();
219
220
        if ($relationValues === null) {
221
            // the developer cleared the selection
222
            // we gonna clear all related values by setting up the value to the fallback id, to null or delete.
223
            $removedEntries = $modelInstance->where($relationForeignKey, $item->{$relationLocalKey});
224
225
            return $this->handleManyRelationItemRemoval($modelInstance, $removedEntries, $relationDetails, $relationForeignKey);
226
        }
227
        // we add the new values into the relation, if it is HasMany we only update the foreign_key,
228
        // otherwise (it's a MorphMany) we need to update the morphs keys too
229
        $toUpdate[$relationForeignKey] = $item->{$relationLocalKey};
0 ignored issues
show
Comprehensibility Best Practice introduced by
$toUpdate was never initialized. Although not strictly required by PHP, it is generally a good practice to add $toUpdate = array(); before regardless.
Loading history...
230
231
        if ($relationDetails['relation_type'] === 'MorphMany') {
232
            $toUpdate[$relation->getQualifiedMorphType()] = $relation->getMorphClass();
233
        }
234
235
        $modelInstance->whereIn($modelInstance->getKeyName(), $relationValues)
236
            ->update($toUpdate);
237
238
        // we clear up any values that were removed from model relation.
239
        // if developer provided a fallback id, we use it
240
        // if column is nullable we set it to null if developer didn't specify `force_delete => true`
241
        // if none of the above we delete the model from database
242
        $removedEntries = $modelInstance->whereNotIn($modelInstance->getKeyName(), $relationValues)
243
                            ->where($relationForeignKey, $item->{$relationLocalKey});
244
245
        // if relation is MorphMany we also match by morph type.
246
        if ($relationDetails['relation_type'] === 'MorphMany') {
247
            $removedEntries->where($relation->getQualifiedMorphType(), $relation->getMorphClass());
248
        }
249
250
        return $this->handleManyRelationItemRemoval($modelInstance, $removedEntries, $relationDetails, $relationForeignKey);
251
    }
252
253
    private function handleManyRelationItemRemoval($modelInstance, $removedEntries, $relationDetails, $relationForeignKey)
254
    {
255
        $relationColumnIsNullable = $modelInstance->isColumnNullable($relationForeignKey);
256
        $forceDelete = $relationDetails['force_delete'] ?? false;
257
        $fallbackId = $relationDetails['fallback_id'] ?? false;
258
259
        if ($fallbackId) {
260
            return $removedEntries->update([$relationForeignKey => $fallbackId]);
261
        }
262
263
        if ($forceDelete) {
264
            return $removedEntries->delete();
265
        }
266
267
        if (! $relationColumnIsNullable && $modelInstance->dbColumnHasDefault($relationForeignKey)) {
268
            return $removedEntries->update([$relationForeignKey => $modelInstance->getDbColumnDefault($relationForeignKey)]);
269
        }
270
271
        return $removedEntries->update([$relationForeignKey => null]);
272
    }
273
274
    /**
275
     * Handle HasMany/MorphMany relations when used as creatable entries in the crud.
276
     * By using repeatable field, developer can allow the creation of such entries
277
     * in the crud forms.
278
     *
279
     * @param $entry - eg: story
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
280
     * @param $relation - eg  story HasMany monsters
281
     * @param $relationMethod - eg: monsters
282
     * @param $relationDetails - eg: info about relation including submited values
283
     * @return void
284
     */
285
    private function createManyEntries($entry, $relation, $relationMethod, $relationDetails)
286
    {
287
        $items = $relationDetails['values'][$relationMethod];
288
289
        $relation_local_key = $relation->getLocalKeyName();
290
291
        $relatedItemsSent = [];
292
293
        foreach ($items as $item) {
294
            [$directInputs, $relationInputs] = $this->splitInputIntoDirectAndRelations($item, $relationDetails, $relationMethod);
295
            // for each item we get the inputs to create and the relations of it.
296
            $relation_local_key_value = $item[$relation_local_key] ?? null;
297
298
            // we either find the matched entry by local_key (usually `id`)
299
            // and update the values from the input
300
            // or create a new item from input
301
            $item = $relation->updateOrCreate([$relation_local_key => $relation_local_key_value], $directInputs);
302
303
            // we store the item local key do we can match them with database and check if any item was deleted
304
            $relatedItemsSent[] = $item->{$relation_local_key};
305
306
            // create the item relations if any
307
            $this->createRelationsForItem($item, $relationInputs);
308
        }
309
310
        // use the collection of sent ids to match agains database ids, delete the ones not found in the submitted ids.
311
        if (! empty($relatedItemsSent)) {
312
            // we perform the cleanup of removed database items
313
            $entry->{$relationMethod}()->whereNotIn($relation_local_key, $relatedItemsSent)->delete();
314
        }
315
    }
316
}
317