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 Failed
Push — revamp-tabs ( e2fe97 )
by Pedro
11:02
created

Fields   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 583
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 141
dl 0
loc 583
rs 2
c 2
b 0
f 1
wmc 82

36 Methods

Rating   Name   Duplication   Size   Complexity  
A fields() 0 3 1
A getCleanStateFields() 0 3 1
A makeFirstField() 0 8 2
A fieldTypeNotLoaded() 0 3 1
A makeSureFieldHasRelationshipAttributes() 0 10 1
A afterField() 0 4 1
A getLoadedFieldTypes() 0 3 1
A makeSureFieldTabsCanRender() 0 14 5
A setFieldLabel() 0 3 1
A removeFields() 0 5 3
A hasFieldWhere() 0 7 2
A firstFieldWhere() 0 4 2
A removeAllFields() 0 6 3
A removeFieldAttribute() 0 7 1
A fieldTypeLoaded() 0 3 1
A checkIfFieldIsFirstOfItsType() 0 10 3
A makeSureFieldHasNecessaryAttributes() 0 18 3
A removeField() 0 6 1
A field() 0 3 1
A getStrippedSaveRequest() 0 18 5
A orderFields() 0 4 1
A addField() 0 9 1
A setLoadedFieldTypes() 0 3 1
B hasUploadFields() 0 17 7
A beforeField() 0 4 1
A getFieldTypeWithNamespace() 0 12 3
A getFields() 0 3 1
A addLoadedFieldType() 0 13 2
A addFields() 0 5 3
A getCurrentFields() 0 3 1
A getAllFieldNames() 0 16 3
B decodeJsonCastedAttributes() 0 24 11
A registerFieldEvents() 0 6 4
A holdsMultipleInputs() 0 3 1
A markFieldTypeAsLoaded() 0 3 1
A modifyField() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like Fields 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 Fields, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;
4
5
use Backpack\CRUD\app\Library\CrudPanel\CrudField;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Str;
8
9
trait Fields
10
{
11
    use FieldsProtectedMethods;
12
    use FieldsPrivateMethods;
13
14
    // ------------
15
    // FIELDS
16
    // ------------
17
18
    /**
19
     * Get the CRUD fields for the current operation with name processed to be usable in HTML.
20
     *
21
     * @return array
22
     */
23
    public function fields()
24
    {
25
        return $this->overwriteFieldNamesFromDotNotationToArray($this->getOperationSetting('fields') ?? []);
0 ignored issues
show
Bug introduced by
It seems like getOperationSetting() 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

25
        return $this->overwriteFieldNamesFromDotNotationToArray($this->/** @scrutinizer ignore-call */ getOperationSetting('fields') ?? []);
Loading history...
26
    }
27
28
    /**
29
     * Returns the fields as they are stored inside operation setting, not running the
30
     * presentation callbacks like converting the `dot.names` into `dot[names]` for html for example.
31
     */
32
    public function getCleanStateFields()
33
    {
34
        return $this->getOperationSetting('fields') ?? [];
35
    }
36
37
    /**
38
     * The only REALLY MANDATORY attribute when defining a field is the 'name'.
39
     * Everything else Backpack can probably guess. This method makes sure  the
40
     * field definition array is complete, by guessing missing attributes.
41
     *
42
     * @param  string|array  $field  The definition of a field (string or array).
43
     * @return array The correct definition of that field.
44
     */
45
    public function makeSureFieldHasNecessaryAttributes($field)
46
    {
47
        $field = $this->makeSureFieldHasName($field);
48
        $field = $this->makeSureFieldHasEntity($field);
49
        $field = $this->makeSureFieldHasLabel($field);
50
51
        if (isset($field['entity']) && $field['entity'] !== false) {
52
            $field = $this->makeSureFieldHasRelationshipAttributes($field);
53
        }
54
55
        $field = $this->makeSureFieldHasType($field);
56
        $field = $this->makeSureSubfieldsHaveNecessaryAttributes($field);
57
        $field = $this->makeSureMorphSubfieldsAreDefined($field);
0 ignored issues
show
Bug introduced by
The method makeSureMorphSubfieldsAreDefined() does not exist on Backpack\CRUD\app\Library\CrudPanel\Traits\Fields. Did you maybe mean fields()? ( Ignorable by Annotation )

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

57
        /** @scrutinizer ignore-call */ 
58
        $field = $this->makeSureMorphSubfieldsAreDefined($field);

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...
58
        $field = $this->makeSureFieldTabsCanRender($field);
59
60
        $this->setupFieldValidation($field, $field['parentFieldName'] ?? false);
0 ignored issues
show
Bug introduced by
It seems like setupFieldValidation() 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

60
        $this->/** @scrutinizer ignore-call */ 
61
               setupFieldValidation($field, $field['parentFieldName'] ?? false);
Loading history...
61
62
        return $field;
63
    }
64
65
    /**
66
     * When field is a relationship, Backpack will try to guess some basic attributes from the relation.
67
     *
68
     * @param  array  $field
69
     * @return array
70
     */
71
    public function makeSureFieldHasRelationshipAttributes($field)
72
    {
73
        $field = $this->makeSureFieldHasRelationType($field);
74
        $field = $this->makeSureFieldHasModel($field);
75
        $field = $this->makeSureFieldHasAttribute($field);
76
        $field = $this->makeSureFieldHasMultiple($field);
77
        $field = $this->makeSureFieldHasPivot($field);
78
        $field = $this->makeSureFieldHasType($field);
79
80
        return $field;
81
    }
82
83
    public function makeSureFieldTabsCanRender($field) {
84
        if (!isset($field['tab'])) {
85
            return $field;
86
        }
87
88
        if(empty(Str::slug($field['tab'])) && !isset($field['tab_hash'])) {
89
            abort(500, 'The tab '.$field['tab'].' is defined in an unsupported encoding. Please define `tab_hash` without special characters do be displayed in the #url');
90
        }
91
92
        if(empty(Str::slug($field['tab_hash']))) {
93
            abort(500, 'The `tab_hash` string «'.$field['tab_hash'].'» does not have a valid encoding. Please ensure that you `tab_hash` string returns something from `Str::slug($yourHashString)`');
94
        }
95
96
        return $field;
97
    }
98
99
    /**
100
     * Register all Eloquent Model events that are defined on fields.
101
     * Eg. saving, saved, creating, created, updating, updated.
102
     *
103
     * @see https://laravel.com/docs/master/eloquent#events
104
     *
105
     * @return void
106
     */
107
    public function registerFieldEvents()
108
    {
109
        foreach ($this->getCleanStateFields() as $key => $field) {
110
            if (isset($field['events'])) {
111
                foreach ($field['events'] as $event => $closure) {
112
                    $this->model->{$event}($closure);
113
                }
114
            }
115
        }
116
    }
117
118
    /**
119
     * Add a field to the create/update form or both.
120
     *
121
     * @param  string|array  $field  The new field.
122
     * @return self
123
     */
124
    public function addField($field)
125
    {
126
        $field = $this->makeSureFieldHasNecessaryAttributes($field);
127
128
        $this->enableTabsIfFieldUsesThem($field);
129
        $this->addFieldToOperationSettings($field);
130
        (new CrudField($field['name']))->callRegisteredAttributeMacros();
131
132
        return $this;
133
    }
134
135
    /**
136
     * Add multiple fields to the create/update form or both.
137
     *
138
     * @param  array  $fields  The new fields.
139
     */
140
    public function addFields($fields)
141
    {
142
        if (count($fields)) {
143
            foreach ($fields as $field) {
144
                $this->addField($field);
145
            }
146
        }
147
    }
148
149
    /**
150
     * Move the most recently added field after the given target field.
151
     *
152
     * @param  string  $targetFieldName  The target field name.
153
     */
154
    public function afterField($targetFieldName)
155
    {
156
        $this->transformFields(function ($fields) use ($targetFieldName) {
157
            return $this->moveField($fields, $targetFieldName, false);
158
        });
159
    }
160
161
    /**
162
     * Move the most recently added field before the given target field.
163
     *
164
     * @param  string  $targetFieldName  The target field name.
165
     */
166
    public function beforeField($targetFieldName)
167
    {
168
        $this->transformFields(function ($fields) use ($targetFieldName) {
169
            return $this->moveField($fields, $targetFieldName, true);
170
        });
171
    }
172
173
    /**
174
     * Move this field to be first in the fields list.
175
     *
176
     * @return bool|null
177
     */
178
    public function makeFirstField()
179
    {
180
        if (! $this->fields()) {
181
            return false;
182
        }
183
184
        $firstField = array_keys(array_slice($this->getCleanStateFields(), 0, 1))[0];
185
        $this->beforeField($firstField);
186
    }
187
188
    /**
189
     * Remove a certain field from the create/update/both forms by its name.
190
     *
191
     * @param  string  $name  Field name (as defined with the addField() procedure)
192
     */
193
    public function removeField($name)
194
    {
195
        $this->transformFields(function ($fields) use ($name) {
196
            Arr::forget($fields, $name);
197
198
            return $fields;
199
        });
200
    }
201
202
    /**
203
     * Remove many fields from the create/update/both forms by their name.
204
     *
205
     * @param  array  $array_of_names  A simple array of the names of the fields to be removed.
206
     */
207
    public function removeFields($array_of_names)
208
    {
209
        if (! empty($array_of_names)) {
210
            foreach ($array_of_names as $name) {
211
                $this->removeField($name);
212
            }
213
        }
214
    }
215
216
    /**
217
     * Remove all fields from the create/update/both forms.
218
     */
219
    public function removeAllFields()
220
    {
221
        $current_fields = $this->getCleanStateFields();
222
        if (! empty($current_fields)) {
223
            foreach ($current_fields as $field) {
224
                $this->removeField($field['name']);
225
            }
226
        }
227
    }
228
229
    /**
230
     * Remove an attribute from one field's definition array.
231
     *
232
     * @param  string  $field  The name of the field.
233
     * @param  string  $attribute  The name of the attribute being removed.
234
     */
235
    public function removeFieldAttribute($field, $attribute)
236
    {
237
        $fields = $this->getCleanStateFields();
238
239
        unset($fields[$field][$attribute]);
240
241
        $this->setOperationSetting('fields', $fields);
0 ignored issues
show
Bug introduced by
It seems like setOperationSetting() 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

241
        $this->/** @scrutinizer ignore-call */ 
242
               setOperationSetting('fields', $fields);
Loading history...
242
    }
243
244
    /**
245
     * Update value of a given key for a current field.
246
     *
247
     * @param  string  $fieldName  The field name
248
     * @param  array  $modifications  An array of changes to be made.
249
     */
250
    public function modifyField($fieldName, $modifications)
251
    {
252
        $fieldsArray = $this->getCleanStateFields();
253
        $field = $this->firstFieldWhere('name', $fieldName);
254
255
        foreach ($modifications as $attributeName => $attributeValue) {
256
            $fieldsArray[$field['name']][$attributeName] = $attributeValue;
257
        }
258
259
        $this->enableTabsIfFieldUsesThem($modifications);
260
261
        $this->setOperationSetting('fields', $fieldsArray);
262
    }
263
264
    /**
265
     * Set label for a specific field.
266
     *
267
     * @param  string  $field
268
     * @param  string  $label
269
     */
270
    public function setFieldLabel($field, $label)
271
    {
272
        $this->modifyField($field, ['label' => $label]);
273
    }
274
275
    /**
276
     * Check if field is the first of its type in the given fields array.
277
     * It's used in each field_type.blade.php to determine wether to push the css and js content or not (we only need to push the js and css for a field the first time it's loaded in the form, not any subsequent times).
278
     *
279
     * @param  array  $field  The current field being tested if it's the first of its type.
280
     * @return bool true/false
281
     */
282
    public function checkIfFieldIsFirstOfItsType($field)
283
    {
284
        $fields_array = $this->getCleanStateFields();
285
        $first_field = $this->getFirstOfItsTypeInArray($field['type'], $fields_array);
0 ignored issues
show
Bug introduced by
It seems like getFirstOfItsTypeInArray() 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

285
        /** @scrutinizer ignore-call */ 
286
        $first_field = $this->getFirstOfItsTypeInArray($field['type'], $fields_array);
Loading history...
286
287
        if ($first_field && $field['name'] == $first_field['name']) {
288
            return true;
289
        }
290
291
        return false;
292
    }
293
294
    /**
295
     * Decode attributes that are casted as array/object/json in the model.
296
     * So that they are not json_encoded twice before they are stored in the db
297
     * (once by Backpack in front-end, once by Laravel Attribute Casting).
298
     *
299
     * @param  array  $input
300
     * @param  mixed  $model
301
     * @return array
302
     */
303
    public function decodeJsonCastedAttributes($input, $model = false)
304
    {
305
        $model = $model ? $model : $this->model;
306
        $fields = $this->getCleanStateFields();
307
        $casted_attributes = $model->getCastedAttributes();
308
309
        foreach ($fields as $field) {
310
            // Test the field is castable
311
            if (isset($field['name']) && is_string($field['name']) && array_key_exists($field['name'], $casted_attributes)) {
312
                // Handle JSON field types
313
                $jsonCastables = ['array', 'object', 'json'];
314
                $fieldCasting = $casted_attributes[$field['name']];
315
316
                if (in_array($fieldCasting, $jsonCastables) && isset($input[$field['name']]) && ! empty($input[$field['name']]) && ! is_array($input[$field['name']])) {
317
                    try {
318
                        $input[$field['name']] = json_decode($input[$field['name']]);
319
                    } catch (\Exception $e) {
320
                        $input[$field['name']] = [];
321
                    }
322
                }
323
            }
324
        }
325
326
        return $input;
327
    }
328
329
    /**
330
     * @return array
331
     */
332
    public function getCurrentFields()
333
    {
334
        return $this->fields();
335
    }
336
337
    /**
338
     * Order the CRUD fields. If certain fields are missing from the given order array, they will be
339
     * pushed to the new fields array in the original order.
340
     *
341
     * @param  array  $order  An array of field names in the desired order.
342
     */
343
    public function orderFields($order)
344
    {
345
        $this->transformFields(function ($fields) use ($order) {
346
            return $this->applyOrderToFields($fields, $order);
347
        });
348
    }
349
350
    /**
351
     * Get the fields for the create or update forms.
352
     *
353
     * @return array all the fields that need to be shown and their information
354
     */
355
    public function getFields()
356
    {
357
        return $this->fields();
358
    }
359
360
    /**
361
     * Check if the create/update form has upload fields.
362
     * Upload fields are the ones that have "upload" => true defined on them.
363
     *
364
     * @return bool
365
     */
366
    public function hasUploadFields()
367
    {
368
        $fields = $this->getCleanStateFields();
369
        $upload_fields = Arr::where($fields, function ($value, $key) {
370
            // check if any subfields have uploads
371
            if (isset($value['subfields'])) {
372
                foreach ($value['subfields'] as $subfield) {
373
                    if (isset($subfield['upload']) && $subfield['upload'] === true) {
374
                        return true;
375
                    }
376
                }
377
            }
378
379
            return isset($value['upload']) && $value['upload'] == true;
380
        });
381
382
        return count($upload_fields) ? true : false;
383
    }
384
385
    // ----------------------
386
    // FIELD ASSET MANAGEMENT
387
    // ----------------------
388
389
    /**
390
     * Get all the field types whose resources (JS and CSS) have already been loaded on page.
391
     *
392
     * @return array Array with the names of the field types.
393
     */
394
    public function getLoadedFieldTypes()
395
    {
396
        return $this->getOperationSetting('loadedFieldTypes') ?? [];
397
    }
398
399
    /**
400
     * Set an array of field type names as already loaded for the current operation.
401
     *
402
     * @param  array  $fieldTypes
403
     */
404
    public function setLoadedFieldTypes($fieldTypes)
405
    {
406
        $this->setOperationSetting('loadedFieldTypes', $fieldTypes);
407
    }
408
409
    /**
410
     * Get a namespaced version of the field type name.
411
     * Appends the 'view_namespace' attribute of the field to the `type', using dot notation.
412
     *
413
     * @param  mixed  $field
414
     * @return string Namespaced version of the field type name. Ex: 'text', 'custom.view.path.text'
415
     */
416
    public function getFieldTypeWithNamespace($field)
417
    {
418
        if (is_array($field)) {
419
            $fieldType = $field['type'];
420
            if (isset($field['view_namespace'])) {
421
                $fieldType = implode('.', [$field['view_namespace'], $field['type']]);
422
            }
423
        } else {
424
            $fieldType = $field;
425
        }
426
427
        return $fieldType;
428
    }
429
430
    /**
431
     * Add a new field type to the loadedFieldTypes array.
432
     *
433
     * @param  string  $field  Field array
434
     * @return bool Successful operation true/false.
435
     */
436
    public function addLoadedFieldType($field)
437
    {
438
        $alreadyLoaded = $this->getLoadedFieldTypes();
439
        $type = $this->getFieldTypeWithNamespace($field);
440
441
        if (! in_array($type, $this->getLoadedFieldTypes(), true)) {
442
            $alreadyLoaded[] = $type;
443
            $this->setLoadedFieldTypes($alreadyLoaded);
444
445
            return true;
446
        }
447
448
        return false;
449
    }
450
451
    /**
452
     * Alias of the addLoadedFieldType() method.
453
     * Adds a new field type to the loadedFieldTypes array.
454
     *
455
     * @param  string  $field  Field array
456
     * @return bool Successful operation true/false.
457
     */
458
    public function markFieldTypeAsLoaded($field)
459
    {
460
        return $this->addLoadedFieldType($field);
461
    }
462
463
    /**
464
     * Check if a field type's reasources (CSS and JS) have already been loaded.
465
     *
466
     * @param  string  $field  Field array
467
     * @return bool Whether the field type has been marked as loaded.
468
     */
469
    public function fieldTypeLoaded($field)
470
    {
471
        return in_array($this->getFieldTypeWithNamespace($field), $this->getLoadedFieldTypes());
472
    }
473
474
    /**
475
     * Check if a field type's reasources (CSS and JS) have NOT been loaded.
476
     *
477
     * @param  string  $field  Field array
478
     * @return bool Whether the field type has NOT been marked as loaded.
479
     */
480
    public function fieldTypeNotLoaded($field)
481
    {
482
        return ! in_array($this->getFieldTypeWithNamespace($field), $this->getLoadedFieldTypes());
483
    }
484
485
    /**
486
     * Get a list of all field names for the current operation.
487
     *
488
     * @return array
489
     */
490
    public function getAllFieldNames()
491
    {
492
        $fieldNamesArray = array_column($this->getCleanStateFields(), 'name');
493
494
        return array_reduce($fieldNamesArray, function ($names, $item) {
495
            if (strpos($item, ',') === false) {
496
                $names[] = $item;
497
498
                return $names;
499
            }
500
501
            foreach (explode(',', $item) as $fieldName) {
502
                $names[] = $fieldName;
503
            }
504
505
            return $names;
506
        });
507
    }
508
509
    /**
510
     * Returns the request without anything that might have been maliciously inserted.
511
     * Only specific field names that have been introduced with addField() are kept in the request.
512
     *
513
     * @param  \Illuminate\Http\Request  $request
514
     * @return array
515
     */
516
    public function getStrippedSaveRequest($request)
517
    {
518
        $setting = $this->getOperationSetting('strippedRequest');
519
520
        // if a closure was passed
521
        if (is_callable($setting)) {
522
            return $setting($request);
523
        }
524
525
        // if an invokable class was passed
526
        // eg. \App\Http\Requests\BackpackStrippedRequest
527
        if (is_string($setting) && class_exists($setting)) {
528
            $setting = new $setting();
529
530
            return is_callable($setting) ? $setting($request) : abort(500, get_class($setting).' is not invokable.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of abort(500, get_class($se.... ' is not invokable.') is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
531
        }
532
533
        return $request->only($this->getAllFieldNames());
534
    }
535
536
    /**
537
     * Check if a field exists, by any given attribute.
538
     *
539
     * @param  string  $attribute  Attribute name on that field definition array.
540
     * @param  string  $value  Value of that attribute on that field definition array.
541
     * @return bool
542
     */
543
    public function hasFieldWhere($attribute, $value)
544
    {
545
        $match = Arr::first($this->getCleanStateFields(), function ($field, $fieldKey) use ($attribute, $value) {
546
            return isset($field[$attribute]) && $field[$attribute] == $value;
547
        });
548
549
        return (bool) $match;
550
    }
551
552
    /**
553
     * Get the first field where a given attribute has the given value.
554
     *
555
     * @param  string  $attribute  Attribute name on that field definition array.
556
     * @param  string  $value  Value of that attribute on that field definition array.
557
     * @return bool
558
     */
559
    public function firstFieldWhere($attribute, $value)
560
    {
561
        return Arr::first($this->getCleanStateFields(), function ($field, $fieldKey) use ($attribute, $value) {
562
            return isset($field[$attribute]) && $field[$attribute] == $value;
563
        });
564
    }
565
566
    /**
567
     * The field hold multiple inputs (one field represent multiple model attributes / relations)
568
     * eg: date range or checklist dependency.
569
     */
570
    public function holdsMultipleInputs(string $fieldName): bool
571
    {
572
        return Str::contains($fieldName, ',');
573
    }
574
575
    /**
576
     * Create and return a CrudField object for that field name.
577
     *
578
     * Enables developers to use a fluent syntax to declare their fields,
579
     * in addition to the existing options:
580
     * - CRUD::addField(['name' => 'price', 'type' => 'number']);
581
     * - CRUD::field('price')->type('number');
582
     *
583
     * And if the developer uses the CrudField object as Field in their CrudController:
584
     * - Field::name('price')->type('number');
585
     *
586
     * @param  string  $name  The name of the column in the db, or model attribute.
587
     * @return CrudField
588
     */
589
    public function field($name)
590
    {
591
        return new CrudField($name);
592
    }
593
}
594