Completed
Push — master ( 29b277...2120d8 )
by Song
02:40
created

Form::formatAttribute()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 16
nop 1
dl 0
loc 15
rs 9.4555
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Widgets;
4
5
use Closure;
6
use Encore\Admin\Facades\Admin;
7
use Encore\Admin\Form as BaseForm;
8
use Encore\Admin\Form\Field;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Contracts\Support\Renderable;
11
use Illuminate\Http\Request;
12
use Illuminate\Support\Arr;
13
use Illuminate\Support\MessageBag;
14
use Illuminate\Validation\Validator;
15
16
/**
17
 * Class Form.
18
 *
19
 * @method Field\Text           text($name, $label = '')
20
 * @method Field\Password       password($name, $label = '')
21
 * @method Field\Checkbox       checkbox($name, $label = '')
22
 * @method Field\Radio          radio($name, $label = '')
23
 * @method Field\Select         select($name, $label = '')
24
 * @method Field\MultipleSelect multipleSelect($name, $label = '')
25
 * @method Field\Textarea       textarea($name, $label = '')
26
 * @method Field\Hidden         hidden($name, $label = '')
27
 * @method Field\Id             id($name, $label = '')
28
 * @method Field\Ip             ip($name, $label = '')
29
 * @method Field\Url            url($name, $label = '')
30
 * @method Field\Color          color($name, $label = '')
31
 * @method Field\Email          email($name, $label = '')
32
 * @method Field\Mobile         mobile($name, $label = '')
33
 * @method Field\Slider         slider($name, $label = '')
34
 * @method Field\File           file($name, $label = '')
35
 * @method Field\Image          image($name, $label = '')
36
 * @method Field\Date           date($name, $label = '')
37
 * @method Field\Datetime       datetime($name, $label = '')
38
 * @method Field\Time           time($name, $label = '')
39
 * @method Field\Year           year($column, $label = '')
40
 * @method Field\Month          month($column, $label = '')
41
 * @method Field\DateRange      dateRange($start, $end, $label = '')
42
 * @method Field\DateTimeRange  dateTimeRange($start, $end, $label = '')
43
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
44
 * @method Field\Number         number($name, $label = '')
45
 * @method Field\Currency       currency($name, $label = '')
46
 * @method Field\SwitchField    switch($name, $label = '')
47
 * @method Field\Display        display($name, $label = '')
48
 * @method Field\Rate           rate($name, $label = '')
49
 * @method Field\Divider        divider($title = '')
50
 * @method Field\Decimal        decimal($column, $label = '')
51
 * @method Field\Html           html($html)
52
 * @method Field\Tags           tags($column, $label = '')
53
 * @method Field\Icon           icon($column, $label = '')
54
 * @method Field\Captcha        captcha($column, $label = '')
55
 * @method Field\Listbox        listbox($column, $label = '')
56
 * @method Field\Table          table($column, $label, $builder)
57
 * @method Field\Timezone       timezone($column, $label = '')
58
 * @method Field\KeyValue       keyValue($column, $label = '')
59
 * @method Field\ListField      list($column, $label = '')
60
 * @method mixed                handle(Request $request)
61
 */
62
class Form implements Renderable
63
{
64
    /**
65
     * The title of form.
66
     *
67
     * @var string
68
     */
69
    public $title;
70
71
    /**
72
     * @var Field[]
73
     */
74
    protected $fields = [];
75
76
    /**
77
     * @var array
78
     */
79
    protected $data = [];
80
81
    /**
82
     * @var array
83
     */
84
    protected $attributes = [];
85
86
    /**
87
     * Available buttons.
88
     *
89
     * @var array
90
     */
91
    protected $buttons = ['reset', 'submit'];
92
93
    /**
94
     * Width for label and submit field.
95
     *
96
     * @var array
97
     */
98
    protected $width = [
99
        'label' => 2,
100
        'field' => 8,
101
    ];
102
103
    /**
104
     * @var bool
105
     */
106
    public $inbox = true;
107
108
    /**
109
     * @var string
110
     */
111
    public $confirm = '';
112
113
    /**
114
     * Form constructor.
115
     *
116
     * @param array $data
117
     */
118
    public function __construct($data = [])
119
    {
120
        $this->fill($data);
121
122
        $this->initFormAttributes();
123
    }
124
125
    /**
126
     * Get form title.
127
     *
128
     * @return mixed
129
     */
130
    public function title()
131
    {
132
        return $this->title;
133
    }
134
135
    /**
136
     * @return array
137
     */
138
    public function data()
139
    {
140
        return $this->data;
141
    }
142
143
    /**
144
     * @return array
145
     */
146
    public function confirm($message)
147
    {
148
        $this->confirm = $message;
149
150
        return $this;
151
    }
152
153
    /**
154
     * Fill data to form fields.
155
     *
156
     * @param array $data
157
     *
158
     * @return $this
159
     */
160
    public function fill($data = [])
161
    {
162
        if ($data instanceof Arrayable) {
163
            $data = $data->toArray();
164
        }
165
166
        if (!empty($data)) {
167
            $this->data = $data;
168
        }
169
170
        return $this;
171
    }
172
173
    /**
174
     * @return $this
175
     */
176
    public function sanitize()
177
    {
178
        foreach (['_form_', '_token'] as $key) {
179
            request()->request->remove($key);
180
        }
181
182
        return $this;
183
    }
184
185
    /**
186
     * Initialize the form attributes.
187
     */
188
    protected function initFormAttributes()
189
    {
190
        $this->attributes = [
191
            'id'             => 'widget-form-'.uniqid(),
192
            'method'         => 'POST',
193
            'action'         => '',
194
            'class'          => 'form-horizontal',
195
            'accept-charset' => 'UTF-8',
196
            'pjax-container' => true,
197
        ];
198
    }
199
200
    /**
201
     * Add form attributes.
202
     *
203
     * @param string|array $attr
204
     * @param string       $value
205
     *
206
     * @return $this
207
     */
208
    public function attribute($attr, $value = '')
209
    {
210
        if (is_array($attr)) {
211
            foreach ($attr as $key => $value) {
212
                $this->attribute($key, $value);
213
            }
214
        } else {
215
            $this->attributes[$attr] = $value;
216
        }
217
218
        return $this;
219
    }
220
221
    /**
222
     * Format form attributes form array to html.
223
     *
224
     * @param array $attributes
225
     *
226
     * @return string
227
     */
228
    public function formatAttribute($attributes = [])
229
    {
230
        $attributes = $attributes ?: $this->attributes;
231
232
        if ($this->hasFile()) {
233
            $attributes['enctype'] = 'multipart/form-data';
234
        }
235
236
        $html = [];
237
        foreach ($attributes as $key => $val) {
238
            $html[] = "$key=\"$val\"";
239
        }
240
241
        return implode(' ', $html) ?: '';
242
    }
243
244
    /**
245
     * Action uri of the form.
246
     *
247
     * @param string $action
248
     *
249
     * @return $this
250
     */
251
    public function action($action)
252
    {
253
        return $this->attribute('action', $action);
254
    }
255
256
    /**
257
     * Method of the form.
258
     *
259
     * @param string $method
260
     *
261
     * @return $this
262
     */
263
    public function method($method = 'POST')
264
    {
265
        if (strtolower($method) == 'put') {
266
            $this->hidden('_method')->default($method);
267
268
            return $this;
269
        }
270
271
        return $this->attribute('method', strtoupper($method));
272
    }
273
274
    /**
275
     * Disable Pjax.
276
     *
277
     * @return $this
278
     */
279
    public function disablePjax()
280
    {
281
        Arr::forget($this->attributes, 'pjax-container');
282
283
        return $this;
284
    }
285
286
    /**
287
     * Disable reset button.
288
     *
289
     * @return $this
290
     */
291
    public function disableReset()
292
    {
293
        array_delete($this->buttons, 'reset');
294
295
        return $this;
296
    }
297
298
    /**
299
     * Disable submit button.
300
     *
301
     * @return $this
302
     */
303
    public function disableSubmit()
304
    {
305
        array_delete($this->buttons, 'submit');
306
307
        return $this;
308
    }
309
310
    /**
311
     * Set field and label width in current form.
312
     *
313
     * @param int $fieldWidth
314
     * @param int $labelWidth
315
     *
316
     * @return $this
317
     */
318
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
319
    {
320
        collect($this->fields)->each(function ($field) use ($fieldWidth, $labelWidth) {
321
            /* @var Field $field  */
322
            $field->setWidth($fieldWidth, $labelWidth);
323
        });
324
325
        // set this width
326
        $this->width = [
327
            'label' => $labelWidth,
328
            'field' => $fieldWidth,
329
        ];
330
331
        return $this;
332
    }
333
334
    /**
335
     * Determine if the form has field type.
336
     *
337
     * @param string $name
338
     *
339
     * @return bool
340
     */
341
    public function hasField($name)
342
    {
343
        return isset(BaseForm::$availableFields[$name]);
344
    }
345
346
    /**
347
     * Add a form field to form.
348
     *
349
     * @param Field $field
350
     *
351
     * @return $this
352
     */
353
    public function pushField(Field &$field)
354
    {
355
        array_push($this->fields, $field);
356
357
        return $this;
358
    }
359
360
    /**
361
     * Get all fields of form.
362
     *
363
     * @return Field[]
364
     */
365
    public function fields()
366
    {
367
        return $this->fields;
368
    }
369
370
    /**
371
     * Get variables for render form.
372
     *
373
     * @return array
374
     */
375
    protected function getVariables()
376
    {
377
        collect($this->fields())->each->fill($this->data());
378
379
        return [
380
            'fields'     => $this->fields,
381
            'attributes' => $this->formatAttribute(),
382
            'method'     => $this->attributes['method'],
383
            'buttons'    => $this->buttons,
384
            'width'      => $this->width,
385
        ];
386
    }
387
388
    /**
389
     * Determine if form fields has files.
390
     *
391
     * @return bool
392
     */
393
    public function hasFile()
394
    {
395
        foreach ($this->fields as $field) {
396
            if ($field instanceof Field\File) {
397
                return true;
398
            }
399
        }
400
401
        return false;
402
    }
403
404
    /**
405
     * Validate this form fields.
406
     *
407
     * @param Request $request
408
     *
409
     * @return bool|MessageBag
410
     */
411
    public function validate(Request $request)
412
    {
413
        if (method_exists($this, 'form')) {
414
            $this->form();
0 ignored issues
show
Documentation Bug introduced by
The method form does not exist on object<Encore\Admin\Widgets\Form>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
415
        }
416
417
        $failedValidators = [];
418
419
        /** @var Field $field */
420 View Code Duplication
        foreach ($this->fields() as $field) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
421
            if (!$validator = $field->getValidator($request->all())) {
422
                continue;
423
            }
424
425
            if (($validator instanceof Validator) && !$validator->passes()) {
426
                $failedValidators[] = $validator;
427
            }
428
        }
429
430
        $message = $this->mergeValidationMessages($failedValidators);
431
432
        return $message->any() ? $message : false;
433
    }
434
435
    /**
436
     * Merge validation messages from input validators.
437
     *
438
     * @param \Illuminate\Validation\Validator[] $validators
439
     *
440
     * @return MessageBag
441
     */
442
    protected function mergeValidationMessages($validators)
443
    {
444
        $messageBag = new MessageBag();
445
446
        foreach ($validators as $validator) {
447
            $messageBag = $messageBag->merge($validator->messages());
448
        }
449
450
        return $messageBag;
451
    }
452
453
    /**
454
     * Add a fieldset to form.
455
     *
456
     * @param string  $title
457
     * @param Closure $setCallback
458
     *
459
     * @return Field\Fieldset
460
     */
461 View Code Duplication
    public function fieldset(string $title, Closure $setCallback)
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...
462
    {
463
        $fieldset = new Field\Fieldset();
464
465
        $this->html($fieldset->start($title))->plain();
466
467
        $setCallback($this);
468
469
        $this->html($fieldset->end())->plain();
470
471
        return $fieldset;
472
    }
473
474
    /**
475
     * @return $this
476
     */
477
    public function unbox()
478
    {
479
        $this->inbox = false;
480
481
        return $this;
482
    }
483
484
    protected function addConfirmScript()
485
    {
486
        $id = $this->attributes['id'];
487
488
        $trans = [
489
            'cancel' => trans('admin.cancel'),
490
            'submit' => trans('admin.submit'),
491
        ];
492
493
        $settings = [
494
            'type'                => 'question',
495
            'showCancelButton'    => true,
496
            'confirmButtonText'   => $trans['submit'],
497
            'cancelButtonText'    => $trans['cancel'],
498
            'title'               => $this->confirm,
499
            'text'                => '',
500
        ];
501
502
        $settings = trim(json_encode($settings, JSON_PRETTY_PRINT));
503
504
        $script = <<<SCRIPT
505
506
$('form#{$id}').off('submit').on('submit', function (e) {
507
    e.preventDefault();
508
    var form = this;
509
    $.admin.swal($settings).then(function (result) {
510
        if (result.value == true) {
511
            form.submit();
512
        }
513
    });
514
    return false;
515
});
516
SCRIPT;
517
518
        Admin::script($script);
519
    }
520
521
    protected function prepareForm()
522
    {
523
        if (method_exists($this, 'form')) {
524
            $this->form();
0 ignored issues
show
Documentation Bug introduced by
The method form does not exist on object<Encore\Admin\Widgets\Form>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
525
        }
526
527
        if (!empty($this->confirm)) {
528
            $this->addConfirmScript();
529
        }
530
    }
531
532
    protected function prepareHandle()
533
    {
534
        if (method_exists($this, 'handle')) {
535
            $this->method('POST');
536
            $this->action(admin_url('_handle_form_'));
0 ignored issues
show
Bug introduced by
It seems like admin_url('_handle_form_') targeting admin_url() can also be of type object<Illuminate\Contracts\Routing\UrlGenerator>; however, Encore\Admin\Widgets\Form::action() does only seem to accept string, 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...
537
            $this->hidden('_form_')->default(get_called_class());
538
        }
539
    }
540
541
    /**
542
     * Render the form.
543
     *
544
     * @return string
545
     */
546
    public function render()
547
    {
548
        $this->prepareForm();
549
550
        $this->prepareHandle();
551
552
        $form = view('admin::widgets.form', $this->getVariables())->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...
553
554
        if (!($title = $this->title()) || !$this->inbox) {
555
            return $form;
556
        }
557
558
        return (new Box($title, $form))->render();
559
    }
560
561
    /**
562
     * Generate a Field object and add to form builder if Field exists.
563
     *
564
     * @param string $method
565
     * @param array  $arguments
566
     *
567
     * @return Field|$this
568
     */
569
    public function __call($method, $arguments)
570
    {
571
        if (!$this->hasField($method)) {
572
            return $this;
573
        }
574
575
        $class = BaseForm::$availableFields[$method];
576
577
        $field = new $class(Arr::get($arguments, 0), array_slice($arguments, 1));
578
579
        return tap($field, function ($field) {
580
            $this->pushField($field);
581
        });
582
    }
583
}
584