Completed
Push — master ( 012343...f524a9 )
by jxlwqq
05:18 queued 02:33
created

Field::setSnakeAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Form;
4
5
use Closure;
6
use Encore\Admin\Admin;
7
use Encore\Admin\Form;
8
use Encore\Admin\Widgets\Form as WidgetForm;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Contracts\Support\Renderable;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Str;
13
use Illuminate\Support\Traits\Macroable;
14
15
/**
16
 * Class Field.
17
 */
18
class Field implements Renderable
19
{
20
    use Macroable;
21
22
    const FILE_DELETE_FLAG = '_file_del_';
23
    const FILE_SORT_FLAG = '_file_sort_';
24
25
    /**
26
     * Element id.
27
     *
28
     * @var array|string
29
     */
30
    protected $id;
31
32
    /**
33
     * Element value.
34
     *
35
     * @var mixed
36
     */
37
    protected $value;
38
39
    /**
40
     * Data of all original columns of value.
41
     *
42
     * @var mixed
43
     */
44
    protected $data;
45
46
    /**
47
     * Field original value.
48
     *
49
     * @var mixed
50
     */
51
    protected $original;
52
53
    /**
54
     * Field default value.
55
     *
56
     * @var mixed
57
     */
58
    protected $default;
59
60
    /**
61
     * Element label.
62
     *
63
     * @var string
64
     */
65
    protected $label = '';
66
67
    /**
68
     * Column name.
69
     *
70
     * @var string|array
71
     */
72
    protected $column = '';
73
74
    /**
75
     * Form element name.
76
     *
77
     * @var string
78
     */
79
    protected $elementName = [];
80
81
    /**
82
     * Form element classes.
83
     *
84
     * @var array
85
     */
86
    protected $elementClass = [];
87
88
    /**
89
     * Variables of elements.
90
     *
91
     * @var array
92
     */
93
    protected $variables = [];
94
95
    /**
96
     * Options for specify elements.
97
     *
98
     * @var array
99
     */
100
    protected $options = [];
101
102
    /**
103
     * Checked for specify elements.
104
     *
105
     * @var array
106
     */
107
    protected $checked = [];
108
109
    /**
110
     * Validation rules.
111
     *
112
     * @var array|\Closure
113
     */
114
    protected $rules = [];
115
116
    /**
117
     * The validation rules for creation.
118
     *
119
     * @var array|\Closure
120
     */
121
    public $creationRules = [];
122
123
    /**
124
     * The validation rules for updates.
125
     *
126
     * @var array|\Closure
127
     */
128
    public $updateRules = [];
129
130
    /**
131
     * @var \Closure
132
     */
133
    protected $validator;
134
135
    /**
136
     * Validation messages.
137
     *
138
     * @var array
139
     */
140
    protected $validationMessages = [];
141
142
    /**
143
     * Css required by this field.
144
     *
145
     * @var array
146
     */
147
    protected static $css = [];
148
149
    /**
150
     * Js required by this field.
151
     *
152
     * @var array
153
     */
154
    protected static $js = [];
155
156
    /**
157
     * Script for field.
158
     *
159
     * @var string
160
     */
161
    protected $script = '';
162
163
    /**
164
     * Element attributes.
165
     *
166
     * @var array
167
     */
168
    protected $attributes = [];
169
170
    /**
171
     * Parent form.
172
     *
173
     * @var Form|WidgetForm
174
     */
175
    protected $form = null;
176
177
    /**
178
     * View for field to render.
179
     *
180
     * @var string
181
     */
182
    protected $view = '';
183
184
    /**
185
     * Help block.
186
     *
187
     * @var array
188
     */
189
    protected $help = [];
190
191
    /**
192
     * Key for errors.
193
     *
194
     * @var mixed
195
     */
196
    protected $errorKey;
197
198
    /**
199
     * Placeholder for this field.
200
     *
201
     * @var string|array
202
     */
203
    protected $placeholder;
204
205
    /**
206
     * Width for label and field.
207
     *
208
     * @var array
209
     */
210
    protected $width = [
211
        'label' => 2,
212
        'field' => 8,
213
    ];
214
215
    /**
216
     * If the form horizontal layout.
217
     *
218
     * @var bool
219
     */
220
    protected $horizontal = true;
221
222
    /**
223
     * column data format.
224
     *
225
     * @var \Closure
226
     */
227
    protected $customFormat = null;
228
229
    /**
230
     * @var bool
231
     */
232
    protected $display = true;
233
234
    /**
235
     * @var array
236
     */
237
    protected $labelClass = [];
238
239
    /**
240
     * @var array
241
     */
242
    protected $groupClass = [];
243
244
    /**
245
     * @var \Closure
246
     */
247
    protected $callback;
248
249
    /**
250
     * column is snake-casing attributes.
251
     *
252
     * @var bool
253
     */
254
    protected $snakeAttributes = false;
255
256
    /**
257
     * @var bool
258
     */
259
    public $isJsonType = false;
260
261
    /**
262
     * Field constructor.
263
     *
264
     * @param       $column
265
     * @param array $arguments
266
     */
267
    public function __construct($column = '', $arguments = [])
268
    {
269
        $this->column = $this->formatColumn($column);
270
        $this->label = $this->formatLabel($arguments);
271
        $this->id = $this->formatId($column);
272
    }
273
274
    /**
275
     * Get assets required by this field.
276
     *
277
     * @return array
278
     */
279
    public static function getAssets()
280
    {
281
        return [
282
            'css' => static::$css,
283
            'js'  => static::$js,
284
        ];
285
    }
286
287
    /**
288
     * Format the field column name.
289
     *
290
     * @param string $column
291
     *
292
     * @return mixed|string
293
     */
294
    protected function formatColumn($column = '')
295
    {
296
        if (Str::contains($column, '->')) {
297
            $this->isJsonType = true;
298
299
            $column = str_replace('->', '.', $column);
300
        }
301
302
        return $column;
303
    }
304
305
    /**
306
     * Format the field element id.
307
     *
308
     * @param string|array $column
309
     *
310
     * @return string|array
311
     */
312
    protected function formatId($column)
313
    {
314
        return str_replace('.', '_', $column);
315
    }
316
317
    /**
318
     * Format the label value.
319
     *
320
     * @param array $arguments
321
     *
322
     * @return string
323
     */
324
    protected function formatLabel($arguments = []): string
325
    {
326
        $column = is_array($this->column) ? current($this->column) : $this->column;
327
328
        $label = $arguments[0] ?? ucfirst($column);
329
330
        return str_replace(['.', '_', '->'], ' ', $label);
331
    }
332
333
    /**
334
     * Format the name of the field.
335
     *
336
     * @param string $column
337
     *
338
     * @return array|mixed|string
339
     */
340
    protected function formatName($column)
341
    {
342
        if (is_string($column)) {
343
            if (Str::contains($column, '->')) {
344
                $name = explode('->', $column);
345
            } else {
346
                $name = explode('.', $column);
347
            }
348
349
            if (count($name) === 1) {
350
                return $name[0];
351
            }
352
353
            $html = array_shift($name);
354
            foreach ($name as $piece) {
355
                $html .= "[$piece]";
356
            }
357
358
            return $html;
359
        }
360
361
        if (is_array($this->column)) {
362
            $names = [];
363
            foreach ($this->column as $key => $name) {
364
                $names[$key] = $this->formatName($name);
365
            }
366
367
            return $names;
368
        }
369
370
        return '';
371
    }
372
373
    /**
374
     * Set form element name.
375
     *
376
     * @param string $name
377
     *
378
     * @return $this
379
     *
380
     * @author Edwin Hui
381
     */
382
    public function setElementName($name): self
383
    {
384
        $this->elementName = $name;
385
386
        return $this;
387
    }
388
389
    /**
390
     * Set snake attributes to the field.
391
     *
392
     * @param bool $snakeAttributes
393
     *
394
     * @return $this
395
     */
396
    public function setSnakeAttributes($snakeAttributes)
397
    {
398
        $this->snakeAttributes = $snakeAttributes;
399
400
        return $this;
401
    }
402
403
    /**
404
     * Get snake attributes of the field.
405
     *
406
     * @return bool
407
     */
408
    public function getSnakeAttributes()
409
    {
410
        return $this->snakeAttributes;
411
    }
412
413
    /**
414
     * Determine if a column needs to be snaked.
415
     *
416
     * @param string|array $column
417
     *
418
     * @return string|array
419
     */
420
    protected function columnShouldSnaked($column)
421
    {
422
        return $this->getSnakeAttributes() ? Str::snake($column) : $column;
0 ignored issues
show
Bug introduced by
It seems like $column defined by parameter $column on line 420 can also be of type array; however, Illuminate\Support\Str::snake() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
423
    }
424
425
    /**
426
     * Fill data to the field.
427
     *
428
     * @param array $data
429
     *
430
     * @return void
431
     */
432
    public function fill($data)
433
    {
434
        $this->data = $data;
435
436
        if (is_array($this->column)) {
437
            foreach ($this->column as $key => $column) {
438
                $this->value[$key] = Arr::get($data, $this->columnShouldSnaked($column));
0 ignored issues
show
Bug introduced by
It seems like $this->columnShouldSnaked($column) targeting Encore\Admin\Form\Field::columnShouldSnaked() can also be of type array; however, Illuminate\Support\Arr::get() does only seem to accept string|integer|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
439
            }
440
441
            return;
442
        }
443
444
        $this->value = Arr::get($data, $this->columnShouldSnaked($this->column));
0 ignored issues
show
Bug introduced by
It seems like $this->columnShouldSnaked($this->column) targeting Encore\Admin\Form\Field::columnShouldSnaked() can also be of type array; however, Illuminate\Support\Arr::get() does only seem to accept string|integer|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
445
446
        $this->formatValue();
447
    }
448
449
    /**
450
     * Format value by passing custom formater.
451
     */
452
    protected function formatValue()
453
    {
454
        if (isset($this->customFormat) && $this->customFormat instanceof \Closure) {
455
            $this->value = call_user_func($this->customFormat, $this->value);
456
        }
457
    }
458
459
    /**
460
     * custom format form column data when edit.
461
     *
462
     * @param \Closure $call
463
     *
464
     * @return $this
465
     */
466
    public function customFormat(\Closure $call): self
467
    {
468
        $this->customFormat = $call;
469
470
        return $this;
471
    }
472
473
    /**
474
     * Set original value to the field.
475
     *
476
     * @param array $data
477
     *
478
     * @return void
479
     */
480
    public function setOriginal($data)
481
    {
482
        if (is_array($this->column)) {
483
            foreach ($this->column as $key => $column) {
484
                $this->original[$key] = Arr::get($data, $column);
485
            }
486
487
            return;
488
        }
489
490
        $this->original = Arr::get($data, $this->column);
491
    }
492
493
    /**
494
     * @param Form $form
495
     *
496
     * @return $this
497
     */
498
    public function setForm(Form $form = null)
499
    {
500
        $this->form = $form;
501
502
        return $this;
503
    }
504
505
    /**
506
     * Set Widget/Form as field parent.
507
     *
508
     * @param WidgetForm $form
509
     *
510
     * @return $this
511
     */
512
    public function setWidgetForm(WidgetForm $form)
513
    {
514
        $this->form = $form;
515
516
        return $this;
517
    }
518
519
    /**
520
     * Set width for field and label.
521
     *
522
     * @param int $field
523
     * @param int $label
524
     *
525
     * @return $this
526
     */
527
    public function setWidth($field = 8, $label = 2): self
528
    {
529
        $this->width = [
530
            'label' => $label,
531
            'field' => $field,
532
        ];
533
534
        return $this;
535
    }
536
537
    /**
538
     * Set the field options.
539
     *
540
     * @param array $options
541
     *
542
     * @return $this
543
     */
544 View Code Duplication
    public function options($options = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
545
    {
546
        if ($options instanceof Arrayable) {
547
            $options = $options->toArray();
548
        }
549
550
        $this->options = array_merge($this->options, $options);
551
552
        return $this;
553
    }
554
555
    /**
556
     * Set the field option checked.
557
     *
558
     * @param array $checked
559
     *
560
     * @return $this
561
     */
562 View Code Duplication
    public function checked($checked = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
563
    {
564
        if ($checked instanceof Arrayable) {
565
            $checked = $checked->toArray();
566
        }
567
568
        $this->checked = array_merge($this->checked, $checked);
569
570
        return $this;
571
    }
572
573
    /**
574
     * Add `required` attribute to current field if has required rule,
575
     * except file and image fields.
576
     *
577
     * @param array $rules
578
     */
579
    protected function addRequiredAttribute($rules)
580
    {
581
        if (!is_array($rules)) {
582
            return;
583
        }
584
585
        if (!in_array('required', $rules, true)) {
586
            return;
587
        }
588
589
        $this->setLabelClass(['asterisk']);
590
591
        // Only text field has `required` attribute.
592
        if (!$this instanceof Form\Field\Text) {
593
            return;
594
        }
595
596
        //do not use required attribute with tabs
597
        if ($this->form && $this->form->getTab()) {
0 ignored issues
show
Bug introduced by
The method getTab does only exist in Encore\Admin\Form, but not in Encore\Admin\Widgets\Form.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
598
            return;
599
        }
600
601
        $this->required();
602
    }
603
604
    /**
605
     * If has `required` rule, add required attribute to this field.
606
     */
607
    protected function addRequiredAttributeFromRules()
608
    {
609
        if ($this->data === null) {
610
            // Create page
611
            $rules = $this->creationRules ?: $this->rules;
612
        } else {
613
            // Update page
614
            $rules = $this->updateRules ?: $this->rules;
615
        }
616
617
        $this->addRequiredAttribute($rules);
0 ignored issues
show
Bug introduced by
It seems like $rules can also be of type object<Closure>; however, Encore\Admin\Form\Field::addRequiredAttribute() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
618
    }
619
620
    /**
621
     * Format validation rules.
622
     *
623
     * @param array|string $rules
624
     *
625
     * @return array
626
     */
627
    protected function formatRules($rules): array
628
    {
629
        if (is_string($rules)) {
630
            $rules = array_filter(explode('|', $rules));
631
        }
632
633
        return array_filter((array) $rules);
634
    }
635
636
    /**
637
     * @param string|array|Closure $input
638
     * @param string|array         $original
639
     *
640
     * @return array|Closure
641
     */
642
    protected function mergeRules($input, $original)
643
    {
644
        if ($input instanceof Closure) {
645
            $rules = $input;
646
        } else {
647
            if (!empty($original)) {
648
                $original = $this->formatRules($original);
649
            }
650
651
            $rules = array_merge($original, $this->formatRules($input));
652
        }
653
654
        return $rules;
655
    }
656
657
    /**
658
     * Set the validation rules for the field.
659
     *
660
     * @param array|callable|string $rules
661
     * @param array                 $messages
662
     *
663
     * @return $this
664
     */
665 View Code Duplication
    public function rules($rules = null, $messages = []): self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
666
    {
667
        $this->rules = $this->mergeRules($rules, $this->rules);
0 ignored issues
show
Documentation introduced by
$rules is of type callable|null, but the function expects a string|array|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $this->rules can also be of type object<Closure>; however, Encore\Admin\Form\Field::mergeRules() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
668
669
        $this->setValidationMessages('default', $messages);
670
671
        return $this;
672
    }
673
674
    /**
675
     * Set the update validation rules for the field.
676
     *
677
     * @param array|callable|string $rules
678
     * @param array                 $messages
679
     *
680
     * @return $this
681
     */
682 View Code Duplication
    public function updateRules($rules = null, $messages = []): self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
683
    {
684
        $this->updateRules = $this->mergeRules($rules, $this->updateRules);
0 ignored issues
show
Documentation introduced by
$rules is of type callable|null, but the function expects a string|array|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $this->updateRules can also be of type object<Closure>; however, Encore\Admin\Form\Field::mergeRules() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
685
686
        $this->setValidationMessages('update', $messages);
687
688
        return $this;
689
    }
690
691
    /**
692
     * Set the creation validation rules for the field.
693
     *
694
     * @param array|callable|string $rules
695
     * @param array                 $messages
696
     *
697
     * @return $this
698
     */
699
    public function creationRules($rules = null, $messages = []): self
700
    {
701
        $this->creationRules = $this->mergeRules($rules, $this->creationRules);
0 ignored issues
show
Documentation introduced by
$rules is of type callable|null, but the function expects a string|array|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $this->creationRules can also be of type object<Closure>; however, Encore\Admin\Form\Field::mergeRules() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
702
703
        $this->setValidationMessages('creation', $messages);
704
705
        return $this;
706
    }
707
708
    /**
709
     * Set validation messages for column.
710
     *
711
     * @param string $key
712
     * @param array  $messages
713
     *
714
     * @return $this
715
     */
716
    public function setValidationMessages($key, array $messages): self
717
    {
718
        $this->validationMessages[$key] = $messages;
719
720
        return $this;
721
    }
722
723
    /**
724
     * Get validation messages for the field.
725
     *
726
     * @return array|mixed
727
     */
728
    public function getValidationMessages()
729
    {
730
        // Default validation message.
731
        $messages = $this->validationMessages['default'] ?? [];
732
733
        if (request()->isMethod('POST')) {
734
            $messages = $this->validationMessages['creation'] ?? $messages;
735
        } elseif (request()->isMethod('PUT')) {
736
            $messages = $this->validationMessages['update'] ?? $messages;
737
        }
738
739
        return $messages;
740
    }
741
742
    /**
743
     * Get field validation rules.
744
     *
745
     * @return string
746
     */
747
    protected function getRules()
748
    {
749
        if (request()->isMethod('POST')) {
750
            $rules = $this->creationRules ?: $this->rules;
751
        } elseif (request()->isMethod('PUT')) {
752
            $rules = $this->updateRules ?: $this->rules;
753
        } else {
754
            $rules = $this->rules;
755
        }
756
757
        if ($rules instanceof \Closure) {
758
            $rules = $rules->call($this, $this->form);
759
        }
760
761
        if (is_string($rules)) {
762
            $rules = array_filter(explode('|', $rules));
763
        }
764
765
        if (!$this->form || !$this->form instanceof Form) {
766
            return $rules;
767
        }
768
769
        if (!$id = $this->form->model()->getKey()) {
770
            return $rules;
771
        }
772
773
        if (is_array($rules)) {
774
            foreach ($rules as &$rule) {
775
                if (is_string($rule)) {
776
                    $rule = str_replace('{{id}}', $id, $rule);
777
                }
778
            }
779
        }
780
781
        return $rules;
782
    }
783
784
    /**
785
     * Remove a specific rule by keyword.
786
     *
787
     * @param string $rule
788
     *
789
     * @return void
790
     */
791
    protected function removeRule($rule)
792
    {
793
        if (is_array($this->rules)) {
794
            array_delete($this->rules, $rule);
795
796
            return;
797
        }
798
799
        if (!is_string($this->rules)) {
800
            return;
801
        }
802
803
        $pattern = "/{$rule}[^\|]?(\||$)/";
804
        $this->rules = preg_replace($pattern, '', $this->rules, -1);
0 ignored issues
show
Documentation Bug introduced by
It seems like preg_replace($pattern, '', $this->rules, -1) of type string is incompatible with the declared type array|object<Closure> of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
805
    }
806
807
    /**
808
     * Set field validator.
809
     *
810
     * @param callable $validator
811
     *
812
     * @return $this
813
     */
814
    public function validator(callable $validator): self
815
    {
816
        $this->validator = $validator;
0 ignored issues
show
Documentation Bug introduced by
It seems like $validator of type callable is incompatible with the declared type object<Closure> of property $validator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
817
818
        return $this;
819
    }
820
821
    /**
822
     * Get key for error message.
823
     *
824
     * @return string|array
825
     */
826
    public function getErrorKey()
827
    {
828
        return $this->errorKey ?: $this->column;
829
    }
830
831
    /**
832
     * Set key for error message.
833
     *
834
     * @param string $key
835
     *
836
     * @return $this
837
     */
838
    public function setErrorKey($key): self
839
    {
840
        $this->errorKey = $key;
841
842
        return $this;
843
    }
844
845
    /**
846
     * Set or get value of the field.
847
     *
848
     * @param null $value
849
     *
850
     * @return mixed
851
     */
852
    public function value($value = null)
853
    {
854
        if ($value === null) {
855
            return $this->value ?? $this->getDefault();
856
        }
857
858
        $this->value = $value;
859
860
        return $this;
861
    }
862
863
    /**
864
     * Set or get data.
865
     *
866
     * @param array $data
867
     *
868
     * @return mixed
869
     */
870
    public function data(array $data = null)
871
    {
872
        if ($data === null) {
873
            return $this->data;
874
        }
875
876
        $this->data = $data;
877
878
        return $this;
879
    }
880
881
    /**
882
     * Set default value for field.
883
     *
884
     * @param $default
885
     *
886
     * @return $this
887
     */
888
    public function default($default): self
889
    {
890
        $this->default = $default;
891
892
        return $this;
893
    }
894
895
    /**
896
     * Get default value.
897
     *
898
     * @return mixed
899
     */
900
    public function getDefault()
901
    {
902
        if ($this->default instanceof \Closure) {
903
            return call_user_func($this->default, $this->form);
904
        }
905
906
        return $this->default;
907
    }
908
909
    /**
910
     * Set help block for current field.
911
     *
912
     * @param string $text
913
     * @param string $icon
914
     *
915
     * @return $this
916
     */
917
    public function help($text = '', $icon = 'fa-info-circle'): self
918
    {
919
        $this->help = compact('text', 'icon');
920
921
        return $this;
922
    }
923
924
    /**
925
     * Get column of the field.
926
     *
927
     * @return string|array
928
     */
929
    public function column()
930
    {
931
        return $this->column;
932
    }
933
934
    /**
935
     * Get label of the field.
936
     *
937
     * @return string
938
     */
939
    public function label(): string
940
    {
941
        return $this->label;
942
    }
943
944
    /**
945
     * Get original value of the field.
946
     *
947
     * @return mixed
948
     */
949
    public function original()
950
    {
951
        return $this->original;
952
    }
953
954
    /**
955
     * Get validator for this field.
956
     *
957
     * @param array $input
958
     *
959
     * @return bool|\Illuminate\Contracts\Validation\Validator|mixed
960
     */
961
    public function getValidator(array $input)
962
    {
963
        if ($this->validator) {
964
            return $this->validator->call($this, $input);
965
        }
966
967
        $rules = $attributes = [];
968
969
        if (!$fieldRules = $this->getRules()) {
970
            return false;
971
        }
972
973
        if (is_string($this->column)) {
974
            if (!Arr::has($input, $this->column)) {
975
                return false;
976
            }
977
978
            $input = $this->sanitizeInput($input, $this->column);
979
980
            $rules[$this->column] = $fieldRules;
981
            $attributes[$this->column] = $this->label;
982
        }
983
984
        if (is_array($this->column)) {
985
            foreach ($this->column as $key => $column) {
986
                if (!array_key_exists($column, $input)) {
987
                    continue;
988
                }
989
                $input[$column.$key] = Arr::get($input, $column);
990
                $rules[$column.$key] = $fieldRules;
991
                $attributes[$column.$key] = $this->label."[$column]";
992
            }
993
        }
994
995
        return \validator($input, $rules, $this->getValidationMessages(), $attributes);
996
    }
997
998
    /**
999
     * Sanitize input data.
1000
     *
1001
     * @param array  $input
1002
     * @param string $column
1003
     *
1004
     * @return array
1005
     */
1006
    protected function sanitizeInput($input, $column)
1007
    {
1008
        if ($this instanceof Field\MultipleSelect) {
1009
            $value = Arr::get($input, $column);
1010
            Arr::set($input, $column, array_filter($value));
1011
        }
1012
1013
        return $input;
1014
    }
1015
1016
    /**
1017
     * Add html attributes to elements.
1018
     *
1019
     * @param array|string $attribute
1020
     * @param mixed        $value
1021
     *
1022
     * @return $this
1023
     */
1024
    public function attribute($attribute, $value = null): self
1025
    {
1026
        if (is_array($attribute)) {
1027
            $this->attributes = array_merge($this->attributes, $attribute);
1028
        } else {
1029
            $this->attributes[$attribute] = (string) $value;
1030
        }
1031
1032
        return $this;
1033
    }
1034
1035
    /**
1036
     * Remove html attributes from elements.
1037
     *
1038
     * @param array|string $attribute
1039
     *
1040
     * @return $this
1041
     */
1042
    public function removeAttribute($attribute): self
1043
    {
1044
        Arr::forget($this->attributes, $attribute);
1045
1046
        return $this;
1047
    }
1048
1049
    /**
1050
     * Set Field style.
1051
     *
1052
     * @param string $attr
1053
     * @param string $value
1054
     *
1055
     * @return $this
1056
     */
1057
    public function style($attr, $value): self
1058
    {
1059
        return $this->attribute('style', "{$attr}: {$value}");
1060
    }
1061
1062
    /**
1063
     * Set Field width.
1064
     *
1065
     * @param string $width
1066
     *
1067
     * @return $this
1068
     */
1069
    public function width($width): self
1070
    {
1071
        return $this->style('width', $width);
1072
    }
1073
1074
    /**
1075
     * Specifies a regular expression against which to validate the value of the input.
1076
     *
1077
     * @param string $regexp
1078
     *
1079
     * @return $this
1080
     */
1081
    public function pattern($regexp): self
1082
    {
1083
        return $this->attribute('pattern', $regexp);
1084
    }
1085
1086
    /**
1087
     * set the input filed required.
1088
     *
1089
     * @param bool $isLabelAsterisked
1090
     *
1091
     * @return $this
1092
     */
1093
    public function required($isLabelAsterisked = true): self
1094
    {
1095
        if ($isLabelAsterisked) {
1096
            $this->setLabelClass(['asterisk']);
1097
        }
1098
1099
        return $this->attribute('required', true);
1100
    }
1101
1102
    /**
1103
     * Set the field automatically get focus.
1104
     *
1105
     * @return $this
1106
     */
1107
    public function autofocus(): self
1108
    {
1109
        return $this->attribute('autofocus', true);
1110
    }
1111
1112
    /**
1113
     * Set the field as readonly mode.
1114
     *
1115
     * @return $this
1116
     */
1117
    public function readonly()
1118
    {
1119
        return $this->attribute('readonly', true);
1120
    }
1121
1122
    /**
1123
     * Set field as disabled.
1124
     *
1125
     * @return $this
1126
     */
1127
    public function disable(): self
1128
    {
1129
        return $this->attribute('disabled', true);
1130
    }
1131
1132
    /**
1133
     * Set field placeholder.
1134
     *
1135
     * @param string $placeholder
1136
     *
1137
     * @return $this
1138
     */
1139
    public function placeholder($placeholder = ''): self
1140
    {
1141
        $this->placeholder = $placeholder;
1142
1143
        return $this;
1144
    }
1145
1146
    /**
1147
     * Get placeholder.
1148
     *
1149
     * @return mixed
1150
     */
1151
    public function getPlaceholder()
1152
    {
1153
        return $this->placeholder ?: trans('admin.input').' '.$this->label;
1154
    }
1155
1156
    /**
1157
     * Add a divider after this field.
1158
     *
1159
     * @return $this
1160
     */
1161
    public function divider()
1162
    {
1163
        $this->form->divider();
1164
1165
        return $this;
1166
    }
1167
1168
    /**
1169
     * Prepare for a field value before update or insert.
1170
     *
1171
     * @param $value
1172
     *
1173
     * @return mixed
1174
     */
1175
    public function prepare($value)
1176
    {
1177
        return $value;
1178
    }
1179
1180
    /**
1181
     * Format the field attributes.
1182
     *
1183
     * @return string
1184
     */
1185 View Code Duplication
    protected function formatAttributes(): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1186
    {
1187
        $html = [];
1188
1189
        foreach ($this->attributes as $name => $value) {
1190
            $html[] = $name.'="'.e($value).'"';
1191
        }
1192
1193
        return implode(' ', $html);
1194
    }
1195
1196
    /**
1197
     * @return $this
1198
     */
1199
    public function disableHorizontal(): self
1200
    {
1201
        $this->horizontal = false;
1202
1203
        return $this;
1204
    }
1205
1206
    /**
1207
     * @return array
1208
     */
1209
    public function getViewElementClasses(): array
1210
    {
1211
        if ($this->horizontal) {
1212
            return [
1213
                'label'      => "col-sm-{$this->width['label']} {$this->getLabelClass()}",
1214
                'field'      => "col-sm-{$this->width['field']}",
1215
                'form-group' => $this->getGroupClass(true),
1216
            ];
1217
        }
1218
1219
        return ['label' => $this->getLabelClass(), 'field' => '', 'form-group' => ''];
1220
    }
1221
1222
    /**
1223
     * Set form element class.
1224
     *
1225
     * @param string|array $class
1226
     *
1227
     * @return $this
1228
     */
1229
    public function setElementClass($class): self
1230
    {
1231
        $this->elementClass = array_merge($this->elementClass, (array) $class);
1232
1233
        return $this;
1234
    }
1235
1236
    /**
1237
     * Get element class.
1238
     *
1239
     * @return array
1240
     */
1241
    public function getElementClass(): array
1242
    {
1243
        if (!$this->elementClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->elementClass of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1244
            $name = $this->elementName ?: $this->formatName($this->column);
0 ignored issues
show
Bug introduced by
It seems like $this->column can also be of type array; however, Encore\Admin\Form\Field::formatName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1245
1246
            $this->elementClass = (array) str_replace(['[', ']'], '_', $name);
1247
        }
1248
1249
        return $this->elementClass;
1250
    }
1251
1252
    /**
1253
     * Get element class string.
1254
     *
1255
     * @return mixed
1256
     */
1257 View Code Duplication
    public function getElementClassString()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1258
    {
1259
        $elementClass = $this->getElementClass();
1260
1261
        if (Arr::isAssoc($elementClass)) {
1262
            $classes = [];
1263
1264
            foreach ($elementClass as $index => $class) {
1265
                $classes[$index] = is_array($class) ? implode(' ', $class) : $class;
1266
            }
1267
1268
            return $classes;
1269
        }
1270
1271
        return implode(' ', $elementClass);
1272
    }
1273
1274
    /**
1275
     * Get element class selector.
1276
     *
1277
     * @return string|array
1278
     */
1279 View Code Duplication
    public function getElementClassSelector()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1280
    {
1281
        $elementClass = $this->getElementClass();
1282
1283
        if (Arr::isAssoc($elementClass)) {
1284
            $classes = [];
1285
1286
            foreach ($elementClass as $index => $class) {
1287
                $classes[$index] = '.'.(is_array($class) ? implode('.', $class) : $class);
1288
            }
1289
1290
            return $classes;
1291
        }
1292
1293
        return '.'.implode('.', $elementClass);
1294
    }
1295
1296
    /**
1297
     * Add the element class.
1298
     *
1299
     * @param $class
1300
     *
1301
     * @return $this
1302
     */
1303
    public function addElementClass($class): self
1304
    {
1305
        if (is_array($class) || is_string($class)) {
1306
            $this->elementClass = array_unique(array_merge($this->elementClass, (array) $class));
1307
        }
1308
1309
        return $this;
1310
    }
1311
1312
    /**
1313
     * Remove element class.
1314
     *
1315
     * @param $class
1316
     *
1317
     * @return $this
1318
     */
1319
    public function removeElementClass($class): self
1320
    {
1321
        $delClass = [];
1322
1323
        if (is_string($class) || is_array($class)) {
1324
            $delClass = (array) $class;
1325
        }
1326
1327
        foreach ($delClass as $del) {
1328
            if (($key = array_search($del, $this->elementClass, true)) !== false) {
1329
                unset($this->elementClass[$key]);
1330
            }
1331
        }
1332
1333
        return $this;
1334
    }
1335
1336
    /**
1337
     * Set form group class.
1338
     *
1339
     * @param string|array $class
1340
     *
1341
     * @return $this
1342
     */
1343
    public function setGroupClass($class): self
1344
    {
1345
        if (is_array($class)) {
1346
            $this->groupClass = array_merge($this->groupClass, $class);
1347
        } else {
1348
            $this->groupClass[] = $class;
1349
        }
1350
1351
        return $this;
1352
    }
1353
1354
    /**
1355
     * Get element class.
1356
     *
1357
     * @param bool $default
1358
     *
1359
     * @return string
1360
     */
1361
    protected function getGroupClass($default = false): string
1362
    {
1363
        return ($default ? 'form-group ' : '').implode(' ', array_filter($this->groupClass));
1364
    }
1365
1366
    /**
1367
     * reset field className.
1368
     *
1369
     * @param string $className
1370
     * @param string $resetClassName
1371
     *
1372
     * @return $this
1373
     */
1374
    public function resetElementClassName(string $className, string $resetClassName): self
1375
    {
1376
        if (($key = array_search($className, $this->getElementClass())) !== false) {
1377
            $this->elementClass[$key] = $resetClassName;
1378
        }
1379
1380
        return $this;
1381
    }
1382
1383
    /**
1384
     * Add variables to field view.
1385
     *
1386
     * @param array $variables
1387
     *
1388
     * @return $this
1389
     */
1390
    public function addVariables(array $variables = []): self
1391
    {
1392
        $this->variables = array_merge($this->variables, $variables);
1393
1394
        return $this;
1395
    }
1396
1397
    /**
1398
     * @return string
1399
     */
1400
    public function getLabelClass(): string
1401
    {
1402
        return implode(' ', $this->labelClass);
1403
    }
1404
1405
    /**
1406
     * @param array $labelClass
1407
     * @param bool  $replace
1408
     *
1409
     * @return self
1410
     */
1411
    public function setLabelClass(array $labelClass, $replace = false): self
1412
    {
1413
        $this->labelClass = $replace ? $labelClass : array_merge($this->labelClass, $labelClass);
1414
1415
        return $this;
1416
    }
1417
1418
    /**
1419
     * Get the view variables of this field.
1420
     *
1421
     * @return array
1422
     */
1423
    public function variables(): array
1424
    {
1425
        return array_merge($this->variables, [
1426
            'id'          => $this->id,
1427
            'name'        => $this->elementName ?: $this->formatName($this->column),
0 ignored issues
show
Bug introduced by
It seems like $this->column can also be of type array; however, Encore\Admin\Form\Field::formatName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1428
            'help'        => $this->help,
1429
            'class'       => $this->getElementClassString(),
1430
            'value'       => $this->value(),
1431
            'label'       => $this->label,
1432
            'viewClass'   => $this->getViewElementClasses(),
1433
            'column'      => $this->column,
1434
            'errorKey'    => $this->getErrorKey(),
1435
            'attributes'  => $this->formatAttributes(),
1436
            'placeholder' => $this->getPlaceholder(),
1437
        ]);
1438
    }
1439
1440
    /**
1441
     * Get view of this field.
1442
     *
1443
     * @return string
1444
     */
1445
    public function getView(): string
1446
    {
1447
        if (!empty($this->view)) {
1448
            return $this->view;
1449
        }
1450
1451
        $class = explode('\\', static::class);
1452
1453
        return 'admin::form.'.strtolower(end($class));
1454
    }
1455
1456
    /**
1457
     * Set view of current field.
1458
     *
1459
     * @param string $view
1460
     *
1461
     * @return string
1462
     */
1463
    public function setView($view): self
1464
    {
1465
        $this->view = $view;
1466
1467
        return $this;
1468
    }
1469
1470
    /**
1471
     * Get script of current field.
1472
     *
1473
     * @return string
1474
     */
1475
    public function getScript(): string
1476
    {
1477
        return $this->script;
1478
    }
1479
1480
    /**
1481
     * Set script of current field.
1482
     *
1483
     * @param string $script
1484
     *
1485
     * @return $this
1486
     */
1487
    public function setScript($script): self
1488
    {
1489
        $this->script = $script;
1490
1491
        return $this;
1492
    }
1493
1494
    /**
1495
     * To set this field should render or not.
1496
     *
1497
     * @param bool $display
1498
     *
1499
     * @return $this
1500
     */
1501
    public function setDisplay(bool $display): self
1502
    {
1503
        $this->display = $display;
1504
1505
        return $this;
1506
    }
1507
1508
    /**
1509
     * If this field should render.
1510
     *
1511
     * @return bool
1512
     */
1513
    protected function shouldRender(): bool
1514
    {
1515
        if (!$this->display) {
1516
            return false;
1517
        }
1518
1519
        return true;
1520
    }
1521
1522
    /**
1523
     * @param \Closure $callback
1524
     *
1525
     * @return \Encore\Admin\Form\Field
1526
     */
1527
    public function with(Closure $callback): self
1528
    {
1529
        $this->callback = $callback;
1530
1531
        return $this;
1532
    }
1533
1534
    /**
1535
     * Render this filed.
1536
     *
1537
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|string
1538
     */
1539
    public function render()
1540
    {
1541
        if (!$this->shouldRender()) {
1542
            return '';
1543
        }
1544
1545
        $this->addRequiredAttributeFromRules();
1546
1547
        if ($this->callback instanceof Closure) {
1548
            $this->value = $this->callback->call($this->form->model(), $this->value, $this);
0 ignored issues
show
Bug introduced by
The method model does only exist in Encore\Admin\Form, but not in Encore\Admin\Widgets\Form.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1549
        }
1550
1551
        Admin::script($this->script);
1552
1553
        return Admin::component($this->getView(), $this->variables());
1554
    }
1555
1556
    /**
1557
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|string
1558
     */
1559
    protected function fieldRender(array $variables = [])
1560
    {
1561
        if (!empty($variables)) {
1562
            $this->addVariables($variables);
1563
        }
1564
1565
        return self::render();
1566
    }
1567
1568
    /**
1569
     * @return string
1570
     */
1571
    public function __toString()
1572
    {
1573
        return $this->render()->render();
0 ignored issues
show
Bug introduced by
The method render does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1574
    }
1575
}
1576