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 — main (#5640)
by Pedro
24:59 queued 10:03
created

Relationships   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 135
dl 0
loc 382
rs 3.12
c 1
b 1
f 0
wmc 66

16 Methods

Rating   Name   Duplication   Size   Complexity  
A inferRelationTypeFromRelationship() 0 5 1
A inferFieldModelFromRelationship() 0 5 1
A parseRelationFieldNamesFromHtml() 0 17 6
A getFieldsWithRelationType() 0 14 3
A getOnlyRelationEntity() 0 11 3
B changeBelongsToNamesFromRelationshipToForeignKey() 0 26 8
A getRelationFieldsWithoutRelationType() 0 21 5
A getPivotFieldStructure() 0 26 3
A getRelationFieldsWithoutPivot() 0 6 2
A guessIfFieldHasPivotFromRelationType() 0 9 4
B guessIfFieldHasMultipleFromRelationType() 0 13 8
A getRelationFieldsWithPivot() 0 6 2
A getRelationInstance() 0 18 5
A isAttributeInRelationString() 0 25 4
B modelMethodIsRelationship() 0 33 8
A getOverwrittenNameForBelongsTo() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Relationships often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Relationships, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;
4
5
use Illuminate\Support\Arr;
6
use Illuminate\Support\Str;
7
8
trait Relationships
9
{
10
    /**
11
     * From the field entity we get the relation instance.
12
     *
13
     * @param  array  $entity
14
     * @return object
15
     */
16
    public function getRelationInstance($field)
17
    {
18
        $entity = $this->getOnlyRelationEntity($field);
19
        $possible_method = Str::before($entity, '.');
20
        $model = isset($field['baseModel']) ? app($field['baseModel']) : $this->model;
21
22
        if (method_exists($model, $possible_method) || $model->isRelation($possible_method)) {
23
            $parts = explode('.', $entity);
24
            // here we are going to iterate through all relation parts to check
25
            foreach ($parts as $i => $part) {
26
                $relation = $model->$part();
27
                $model = $relation->getRelated();
28
            }
29
30
            return $relation;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $relation seems to be defined by a foreach iteration on line 25. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
31
        }
32
33
        abort(500, 'Looks like field <code>'.$field['name'].'</code> is not properly defined. The <code>'.$field['entity'].'()</code> relationship doesn\'t seem to exist on the <code>'.get_class($model).'</code> model.');
34
    }
35
36
    /**
37
     * Grabs an relation instance and returns the class name of the related model.
38
     *
39
     * @param  array  $field
40
     * @return string
41
     */
42
    public function inferFieldModelFromRelationship($field)
43
    {
44
        $relation = $this->getRelationInstance($field);
45
46
        return get_class($relation->getRelated());
47
    }
48
49
    /**
50
     * Return the relation type from a given field: BelongsTo, HasOne ... etc.
51
     *
52
     * @param  array  $field
53
     * @return string
54
     */
55
    public function inferRelationTypeFromRelationship($field)
56
    {
57
        $relation = $this->getRelationInstance($field);
58
59
        return Arr::last(explode('\\', get_class($relation)));
60
    }
61
62
    public function getOnlyRelationEntity($field)
63
    {
64
        $entity = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$field['entity'] : $field['entity'];
65
        $model = $this->getRelationModel($entity, -1);
0 ignored issues
show
Bug introduced by
The method getRelationModel() does not exist on Backpack\CRUD\app\Librar...el\Traits\Relationships. Did you maybe mean getRelationInstance()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

65
        /** @scrutinizer ignore-call */ 
66
        $model = $this->getRelationModel($entity, -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...
66
        $lastSegmentAfterDot = Str::of($field['entity'])->afterLast('.');
67
68
        if (! method_exists($model, $lastSegmentAfterDot)) {
69
            return (string) Str::of($field['entity'])->beforeLast('.');
70
        }
71
72
        return $field['entity'];
73
    }
74
75
    /**
76
     * Get the fields for relationships, according to the relation type. It looks only for direct
77
     * relations - it will NOT look through relationships of relationships.
78
     *
79
     * @param  string|array  $relation_types  Eloquent relation class or array of Eloquent relation classes. Eg: BelongsTo
80
     * @param  bool  $nested  Should nested fields be included
81
     * @return array The fields with corresponding relation types.
82
     */
83
    public function getFieldsWithRelationType($relation_types, $nested = false): array
84
    {
85
        $relation_types = (array) $relation_types;
86
87
        return collect($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

87
        return collect($this->/** @scrutinizer ignore-call */ getCleanStateFields())
Loading history...
88
            ->whereIn('relation_type', $relation_types)
89
            ->filter(function ($item) use ($nested) {
90
                if ($nested) {
91
                    return true;
92
                }
93
94
                return Str::contains($item['entity'], '.') ? false : true;
95
            })
96
            ->toArray();
97
    }
98
99
    /**
100
     * Parse the field name back to the related entity after the form is submitted.
101
     * Its called in getAllFieldNames().
102
     *
103
     * @param  array  $fields
104
     * @return array
105
     */
106
    public function parseRelationFieldNamesFromHtml($fields)
107
    {
108
        foreach ($fields as &$field) {
109
            //we only want to parse fields that has a relation type and their name contains [ ] used in html.
110
            if (isset($field['relation_type']) && preg_match('/[\[\]]/', $field['name']) !== 0) {
111
                $chunks = explode('[', $field['name']);
112
113
                foreach ($chunks as &$chunk) {
114
                    if (strpos($chunk, ']')) {
115
                        $chunk = str_replace(']', '', $chunk);
116
                    }
117
                }
118
                $field['name'] = implode('.', $chunks);
119
            }
120
        }
121
122
        return $fields;
123
    }
124
125
    /**
126
     * Gets the relation fields that DON'T contain the provided relations.
127
     *
128
     * @param  string|array  $relations  - the relations to exclude
129
     * @param  array  $fields
130
     */
131
    private function getRelationFieldsWithoutRelationType($relations, $fields = [])
132
    {
133
        if (! is_array($relations)) {
134
            $relations = [$relations];
135
        }
136
137
        if (empty($fields)) {
138
            $fields = $this->getRelationFields();
0 ignored issues
show
Bug introduced by
The method getRelationFields() does not exist on Backpack\CRUD\app\Librar...el\Traits\Relationships. Did you maybe mean getRelationFieldsWithoutPivot()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

138
            /** @scrutinizer ignore-call */ 
139
            $fields = $this->getRelationFields();

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...
139
        }
140
141
        foreach ($relations as $relation) {
142
            $fields = array_filter($fields, function ($field) use ($relation) {
143
                if (! isset($field['relation_type'])) {
144
                    return false;
145
                }
146
147
                return $field['relation_type'] !== $relation;
148
            });
149
        }
150
151
        return $fields;
152
    }
153
154
    /**
155
     * Changes the input names to use the foreign_key, instead of the relation name,
156
     * for BelongsTo relations (eg. "user_id" instead of "user").
157
     *
158
     * When $fields are provided, we will use those fields to determine the correct
159
     * foreign key. Otherwise, we will use the main CRUD fields.
160
     *
161
     * eg: user -> user_id
162
     *
163
     * @param  array  $input
164
     * @param  array  $belongsToFields
165
     * @return array
166
     */
167
    private function changeBelongsToNamesFromRelationshipToForeignKey($input, $fields = [])
168
    {
169
        if (empty($fields)) {
170
            $fields = $this->getFieldsWithRelationType('BelongsTo');
171
        } else {
172
            foreach ($fields as $field) {
173
                if (isset($field['subfields'])) {
174
                    $fields = array_merge($field['subfields'], $fields);
175
                }
176
            }
177
            $fields = array_filter($fields, function ($field) {
178
                return isset($field['relation_type']) && $field['relation_type'] === 'BelongsTo';
179
            });
180
        }
181
182
        foreach ($fields as $field) {
183
            $foreignKey = $this->getOverwrittenNameForBelongsTo($field);
184
            $lastFieldNameSegment = Str::afterLast($field['name'], '.');
185
186
            if (Arr::has($input, $lastFieldNameSegment) && $lastFieldNameSegment !== $foreignKey) {
187
                Arr::set($input, $foreignKey, Arr::get($input, $lastFieldNameSegment));
188
                Arr::forget($input, $lastFieldNameSegment);
189
            }
190
        }
191
192
        return $input;
193
    }
194
195
    /**
196
     * Based on relation type returns if relation allows multiple entities.
197
     *
198
     * @param  string  $relation_type
199
     * @return bool
200
     */
201
    public function guessIfFieldHasMultipleFromRelationType($relation_type)
202
    {
203
        switch ($relation_type) {
204
            case 'BelongsToMany':
205
            case 'HasMany':
206
            case 'HasManyThrough':
207
            case 'HasOneOrMany':
208
            case 'MorphMany':
209
            case 'MorphOneOrMany':
210
            case 'MorphToMany':
211
                return true;
212
            default:
213
                return false;
214
        }
215
    }
216
217
    /**
218
     * Based on relation type returns if relation has a pivot table.
219
     *
220
     * @param  string  $relation_type
221
     * @return bool
222
     */
223
    public function guessIfFieldHasPivotFromRelationType($relation_type)
224
    {
225
        switch ($relation_type) {
226
            case 'BelongsToMany':
227
            case 'HasManyThrough':
228
            case 'MorphToMany':
229
                return true;
230
            default:
231
                return false;
232
        }
233
    }
234
235
    /**
236
     * Get all relation fields that don't have pivot set.
237
     *
238
     * @return array The fields with model key set.
239
     */
240
    public function getRelationFieldsWithoutPivot()
241
    {
242
        $all_relation_fields = $this->getRelationFields();
243
244
        return Arr::where($all_relation_fields, function ($value, $key) {
245
            return isset($value['pivot']) && ! $value['pivot'];
246
        });
247
    }
248
249
    /**
250
     * Get all fields with n-n relation set (pivot table is true).
251
     *
252
     * @return array The fields with n-n relationships.
253
     */
254
    public function getRelationFieldsWithPivot()
255
    {
256
        $all_relation_fields = $this->getRelationFields();
257
258
        return Arr::where($all_relation_fields, function ($value, $key) {
259
            return isset($value['pivot']) && $value['pivot'];
260
        });
261
    }
262
263
    /**
264
     * Return the name for the BelongTo relation making sure it always has the
265
     * foreign_key instead of relationName (eg. "user_id", not "user").
266
     *
267
     * @param  array  $field  The field we want to get the name from
268
     * @return string
269
     */
270
    private function getOverwrittenNameForBelongsTo($field)
271
    {
272
        $relation = $this->getRelationInstance($field);
273
274
        if (Str::afterLast($field['name'], '.') === $relation->getRelationName() || Str::endsWith($relation->getRelationName(), '{closure}')) {
275
            return $relation->getForeignKeyName();
276
        }
277
278
        return $field['name'];
279
    }
280
281
    /**
282
     * Returns the pivot definition for BelongsToMany/MorphToMany relation provided in $field.
283
     *
284
     * @param  array  $field
285
     * @return array
286
     */
287
    private static function getPivotFieldStructure($field)
288
    {
289
        $pivotSelectorField['name'] = $field['name'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$pivotSelectorField was never initialized. Although not strictly required by PHP, it is generally a good practice to add $pivotSelectorField = array(); before regardless.
Loading history...
290
        $pivotSelectorField['type'] = 'relationship';
291
        $pivotSelectorField['is_pivot_select'] = true;
292
        $pivotSelectorField['multiple'] = false;
293
        $pivotSelectorField['entity'] = $field['name'];
294
        $pivotSelectorField['relation_type'] = $field['relation_type'];
295
        $pivotSelectorField['model'] = $field['model'];
296
        $pivotSelectorField['minimum_input_length'] = 2;
297
        $pivotSelectorField['delay'] = 500;
298
        $pivotSelectorField['placeholder'] = trans('backpack::crud.select_entry');
299
        $pivotSelectorField['label'] = Str::of($field['name'])->singular()->ucfirst();
300
        $pivotSelectorField['validationRules'] = 'required';
301
        $pivotSelectorField['validationMessages'] = [
302
            'required' => trans('backpack::crud.pivot_selector_required_validation_message'),
303
        ];
304
305
        if (isset($field['baseModel'])) {
306
            $pivotSelectorField['baseModel'] = $field['baseModel'];
307
        }
308
        if (isset($field['baseEntity'])) {
309
            $pivotSelectorField['baseEntity'] = $field['baseEntity'];
310
        }
311
312
        return $pivotSelectorField;
313
    }
314
315
    /**
316
     * Checks the properties of the provided method to better verify if it could be a relation.
317
     * Case the method is not public, is not a relation.
318
     * Case the return type is Attribute, or extends Attribute is not a relation method.
319
     * If the return type extends the Relation class is for sure a relation
320
     * Otherwise we just assume it's a relation.
321
     *
322
     * @param  $model
323
     * @param  $method
324
     * @return bool|string
325
     */
326
    private function modelMethodIsRelationship($model, $method)
327
    {
328
        if (! method_exists($model, $method) && $model->isRelation($method)) {
329
            return $method;
330
        }
331
332
        $methodReflection = new \ReflectionMethod($model, $method);
333
334
        // relationship methods function does not have parameters
335
        if ($methodReflection->getNumberOfParameters() > 0) {
336
            return false;
337
        }
338
339
        // relationships are always public methods.
340
        if (! $methodReflection->isPublic()) {
341
            return false;
342
        }
343
344
        $returnType = $methodReflection->getReturnType();
345
346
        if ($returnType) {
347
            $returnType = $returnType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

347
            /** @scrutinizer ignore-call */ 
348
            $returnType = $returnType->getName();
Loading history...
348
349
            if (is_a($returnType, 'Illuminate\Database\Eloquent\Casts\Attribute', true)) {
350
                return false;
351
            }
352
353
            if (is_a($returnType, 'Illuminate\Database\Eloquent\Relations\Relation', true)) {
354
                return $method;
355
            }
356
        }
357
358
        return $method;
359
    }
360
361
    /**
362
     * Check if it's possible that attribute is in the relation string when
363
     * the last part of the string is not a method on the chained relations.
364
     */
365
    public function isAttributeInRelationString(array $field): bool
366
    {
367
        if (! str_contains($field['entity'], '.')) {
368
            return false;
369
        }
370
371
        $parts = explode('.', $field['entity']);
372
373
        $model = $field['baseModel'] ?? $this->model;
374
375
        $model = new $model;
376
377
        // here we are going to iterate through all relation parts to check
378
        // if the attribute is present in the relation string.
379
        foreach ($parts as $i => $part) {
380
            try {
381
                $model = $model->$part()->getRelated();
382
            } catch (\Exception $e) {
383
                // return true if the last part of a relation string is not a method on the model
384
                // so it's probably the attribute that we should show
385
                return true;
386
            }
387
        }
388
389
        return false;
390
    }
391
}
392