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 (#5647)
by Pedro
23:13 queued 08:06
created

Relationships::modelMethodIsRelationship()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 8
eloc 15
nc 7
nop 2
dl 0
loc 33
rs 8.4444
c 1
b 1
f 0
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
                if (! is_a($relation, \Illuminate\Database\Eloquent\Relations\Relation::class, true)) {
28
                    return $model;
29
                }
30
                $model = $relation->getRelated();
31
            }
32
33
            return $relation;
34
        }
35
36
        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.');
37
    }
38
39
    /**
40
     * Grabs an relation instance and returns the class name of the related model.
41
     *
42
     * @param  array  $field
43
     * @return string
44
     */
45
    public function inferFieldModelFromRelationship($field)
46
    {
47
        $relation = $this->getRelationInstance($field);
48
49
        return get_class($relation->getRelated());
50
    }
51
52
    /**
53
     * Return the relation type from a given field: BelongsTo, HasOne ... etc.
54
     *
55
     * @param  array  $field
56
     * @return string
57
     */
58
    public function inferRelationTypeFromRelationship($field)
59
    {
60
        $relation = $this->getRelationInstance($field);
61
62
        return Arr::last(explode('\\', get_class($relation)));
63
    }
64
65
    public function getOnlyRelationEntity($field)
66
    {
67
        $entity = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$field['entity'] : $field['entity'];
68
        $model = new ($this->getRelationModel($entity, -1));
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '(' on line 68 at column 21
Loading history...
69
        $lastSegmentAfterDot = Str::of($field['entity'])->afterLast('.')->value();
70
71
        if (! $this->modelMethodIsRelationship($model, $lastSegmentAfterDot)) {
72
            return (string) Str::of($field['entity'])->beforeLast('.');
73
        }
74
75
        return $field['entity'];
76
    }
77
78
    /**
79
     * Get the fields for relationships, according to the relation type. It looks only for direct
80
     * relations - it will NOT look through relationships of relationships.
81
     *
82
     * @param  string|array  $relation_types  Eloquent relation class or array of Eloquent relation classes. Eg: BelongsTo
83
     * @param  bool  $nested  Should nested fields be included
84
     * @return array The fields with corresponding relation types.
85
     */
86
    public function getFieldsWithRelationType($relation_types, $nested = false): array
87
    {
88
        $relation_types = (array) $relation_types;
89
90
        return collect($this->getCleanStateFields())
91
            ->whereIn('relation_type', $relation_types)
92
            ->filter(function ($item) use ($nested) {
93
                if ($nested) {
94
                    return true;
95
                }
96
97
                return Str::contains($item['entity'], '.') ? false : true;
98
            })
99
            ->toArray();
100
    }
101
102
    /**
103
     * Parse the field name back to the related entity after the form is submitted.
104
     * Its called in getAllFieldNames().
105
     *
106
     * @param  array  $fields
107
     * @return array
108
     */
109
    public function parseRelationFieldNamesFromHtml($fields)
110
    {
111
        foreach ($fields as &$field) {
112
            //we only want to parse fields that has a relation type and their name contains [ ] used in html.
113
            if (isset($field['relation_type']) && preg_match('/[\[\]]/', $field['name']) !== 0) {
114
                $chunks = explode('[', $field['name']);
115
116
                foreach ($chunks as &$chunk) {
117
                    if (strpos($chunk, ']')) {
118
                        $chunk = str_replace(']', '', $chunk);
119
                    }
120
                }
121
                $field['name'] = implode('.', $chunks);
122
            }
123
        }
124
125
        return $fields;
126
    }
127
128
    /**
129
     * Gets the relation fields that DON'T contain the provided relations.
130
     *
131
     * @param  string|array  $relations  - the relations to exclude
132
     * @param  array  $fields
133
     */
134
    private function getRelationFieldsWithoutRelationType($relations, $fields = [])
135
    {
136
        if (! is_array($relations)) {
137
            $relations = [$relations];
138
        }
139
140
        if (empty($fields)) {
141
            $fields = $this->getRelationFields();
142
        }
143
144
        foreach ($relations as $relation) {
145
            $fields = array_filter($fields, function ($field) use ($relation) {
146
                if (! isset($field['relation_type'])) {
147
                    return false;
148
                }
149
150
                return $field['relation_type'] !== $relation;
151
            });
152
        }
153
154
        return $fields;
155
    }
156
157
    /**
158
     * Changes the input names to use the foreign_key, instead of the relation name,
159
     * for BelongsTo relations (eg. "user_id" instead of "user").
160
     *
161
     * When $fields are provided, we will use those fields to determine the correct
162
     * foreign key. Otherwise, we will use the main CRUD fields.
163
     *
164
     * eg: user -> user_id
165
     *
166
     * @param  array  $input
167
     * @param  array  $belongsToFields
168
     * @return array
169
     */
170
    private function changeBelongsToNamesFromRelationshipToForeignKey($input, $fields = [])
171
    {
172
        if (empty($fields)) {
173
            $fields = $this->getFieldsWithRelationType('BelongsTo');
174
        } else {
175
            foreach ($fields as $field) {
176
                if (isset($field['subfields'])) {
177
                    $fields = array_merge($field['subfields'], $fields);
178
                }
179
            }
180
            $fields = array_filter($fields, function ($field) {
181
                return isset($field['relation_type']) && $field['relation_type'] === 'BelongsTo';
182
            });
183
        }
184
185
        foreach ($fields as $field) {
186
            $foreignKey = $this->getOverwrittenNameForBelongsTo($field);
187
            $lastFieldNameSegment = Str::afterLast($field['name'], '.');
188
189
            if (Arr::has($input, $lastFieldNameSegment) && $lastFieldNameSegment !== $foreignKey) {
190
                Arr::set($input, $foreignKey, Arr::get($input, $lastFieldNameSegment));
191
                Arr::forget($input, $lastFieldNameSegment);
192
            }
193
        }
194
195
        return $input;
196
    }
197
198
    /**
199
     * Based on relation type returns if relation allows multiple entities.
200
     *
201
     * @param  string  $relation_type
202
     * @return bool
203
     */
204
    public function guessIfFieldHasMultipleFromRelationType($relation_type)
205
    {
206
        switch ($relation_type) {
207
            case 'BelongsToMany':
208
            case 'HasMany':
209
            case 'HasManyThrough':
210
            case 'HasOneOrMany':
211
            case 'MorphMany':
212
            case 'MorphOneOrMany':
213
            case 'MorphToMany':
214
                return true;
215
            default:
216
                return false;
217
        }
218
    }
219
220
    /**
221
     * Based on relation type returns if relation has a pivot table.
222
     *
223
     * @param  string  $relation_type
224
     * @return bool
225
     */
226
    public function guessIfFieldHasPivotFromRelationType($relation_type)
227
    {
228
        switch ($relation_type) {
229
            case 'BelongsToMany':
230
            case 'HasManyThrough':
231
            case 'MorphToMany':
232
                return true;
233
            default:
234
                return false;
235
        }
236
    }
237
238
    /**
239
     * Get all relation fields that don't have pivot set.
240
     *
241
     * @return array The fields with model key set.
242
     */
243
    public function getRelationFieldsWithoutPivot()
244
    {
245
        $all_relation_fields = $this->getRelationFields();
246
247
        return Arr::where($all_relation_fields, function ($value, $key) {
248
            return isset($value['pivot']) && ! $value['pivot'];
249
        });
250
    }
251
252
    /**
253
     * Get all fields with n-n relation set (pivot table is true).
254
     *
255
     * @return array The fields with n-n relationships.
256
     */
257
    public function getRelationFieldsWithPivot()
258
    {
259
        $all_relation_fields = $this->getRelationFields();
260
261
        return Arr::where($all_relation_fields, function ($value, $key) {
262
            return isset($value['pivot']) && $value['pivot'];
263
        });
264
    }
265
266
    /**
267
     * Return the name for the BelongTo relation making sure it always has the
268
     * foreign_key instead of relationName (eg. "user_id", not "user").
269
     *
270
     * @param  array  $field  The field we want to get the name from
271
     * @return string
272
     */
273
    private function getOverwrittenNameForBelongsTo($field)
274
    {
275
        $relation = $this->getRelationInstance($field);
276
277
        if (Str::afterLast($field['name'], '.') === $relation->getRelationName() || Str::endsWith($relation->getRelationName(), '{closure}')) {
278
            return $relation->getForeignKeyName();
279
        }
280
281
        return $field['name'];
282
    }
283
284
    /**
285
     * Returns the pivot definition for BelongsToMany/MorphToMany relation provided in $field.
286
     *
287
     * @param  array  $field
288
     * @return array
289
     */
290
    private static function getPivotFieldStructure($field)
291
    {
292
        $pivotSelectorField['name'] = $field['name'];
293
        $pivotSelectorField['type'] = 'relationship';
294
        $pivotSelectorField['is_pivot_select'] = true;
295
        $pivotSelectorField['multiple'] = false;
296
        $pivotSelectorField['entity'] = $field['name'];
297
        $pivotSelectorField['relation_type'] = $field['relation_type'];
298
        $pivotSelectorField['model'] = $field['model'];
299
        $pivotSelectorField['minimum_input_length'] = 2;
300
        $pivotSelectorField['delay'] = 500;
301
        $pivotSelectorField['placeholder'] = trans('backpack::crud.select_entry');
302
        $pivotSelectorField['label'] = Str::of($field['name'])->singular()->ucfirst();
303
        $pivotSelectorField['validationRules'] = 'required';
304
        $pivotSelectorField['validationMessages'] = [
305
            'required' => trans('backpack::crud.pivot_selector_required_validation_message'),
306
        ];
307
308
        if (isset($field['baseModel'])) {
309
            $pivotSelectorField['baseModel'] = $field['baseModel'];
310
        }
311
        if (isset($field['baseEntity'])) {
312
            $pivotSelectorField['baseEntity'] = $field['baseEntity'];
313
        }
314
315
        return $pivotSelectorField;
316
    }
317
318
    /**
319
     * Checks the properties of the provided method to better verify if it could be a relation.
320
     * Case the method is not public, is not a relation.
321
     * Case the return type is Attribute, or extends Attribute is not a relation method.
322
     * If the return type extends the Relation class is for sure a relation
323
     * Otherwise we just assume it's a relation.
324
     *
325
     * @param  $model
326
     * @param  $method
327
     * @return bool|string
328
     */
329
    private function modelMethodIsRelationship($model, $method)
330
    {
331
        if (! method_exists($model, $method)) {
332
            if ($model->isRelation($method)) {
333
                return $method;
334
            }
335
336
            return false;
337
        }
338
339
        $methodReflection = new \ReflectionMethod($model, $method);
340
341
        // relationship methods function does not have parameters
342
        if ($methodReflection->getNumberOfParameters() > 0) {
343
            return false;
344
        }
345
346
        // relationships are always public methods.
347
        if (! $methodReflection->isPublic()) {
348
            return false;
349
        }
350
351
        $returnType = $methodReflection->getReturnType();
352
353
        if ($returnType) {
354
            $returnType = $returnType->getName();
355
356
            if (is_a($returnType, 'Illuminate\Database\Eloquent\Casts\Attribute', true)) {
357
                return false;
358
            }
359
360
            if (is_a($returnType, 'Illuminate\Database\Eloquent\Relations\Relation', true)) {
361
                return $method;
362
            }
363
        }
364
365
        return $method;
366
    }
367
368
    /**
369
     * Check if it's possible that attribute is in the relation string when
370
     * the last part of the string is not a method on the chained relations.
371
     */
372
    public function isAttributeInRelationString(array $field): bool
373
    {
374
        if (! str_contains($field['entity'], '.')) {
375
            return false;
376
        }
377
378
        $parts = explode('.', $field['entity']);
379
380
        $model = $field['baseModel'] ?? $this->model;
381
382
        $model = new $model;
383
384
        // here we are going to iterate through all relation parts to check
385
        // if the attribute is present in the relation string.
386
        foreach ($parts as $i => $part) {
387
            try {
388
                $model = $model->$part();
389
390
                if (! is_a($model, \Illuminate\Database\Eloquent\Relations\Relation::class, true)) {
391
                    return true;
392
                }
393
394
                $model = $model->getRelated();
395
            } catch (\Exception $e) {
396
                // return true if the last part of a relation string is not a method on the model
397
                // so it's probably the attribute that we should show
398
                return true;
399
            }
400
        }
401
402
        return false;
403
    }
404
}
405