Completed
Pull Request — master (#3476)
by Song
02:43
created

Form::pushField()   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\Widgets;
4
5
use Closure;
6
use Encore\Admin\Form as BaseForm;
7
use Encore\Admin\Form\Field;
8
use Illuminate\Contracts\Support\Arrayable;
9
use Illuminate\Contracts\Support\Renderable;
10
use Illuminate\Http\Request;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\MessageBag;
13
use Illuminate\Validation\Validator;
14
15
/**
16
 * Class Form.
17
 *
18
 * @method Field\Text           text($name, $label = '')
19
 * @method Field\Password       password($name, $label = '')
20
 * @method Field\Checkbox       checkbox($name, $label = '')
21
 * @method Field\Radio          radio($name, $label = '')
22
 * @method Field\Select         select($name, $label = '')
23
 * @method Field\MultipleSelect multipleSelect($name, $label = '')
24
 * @method Field\Textarea       textarea($name, $label = '')
25
 * @method Field\Hidden         hidden($name, $label = '')
26
 * @method Field\Id             id($name, $label = '')
27
 * @method Field\Ip             ip($name, $label = '')
28
 * @method Field\Url            url($name, $label = '')
29
 * @method Field\Color          color($name, $label = '')
30
 * @method Field\Email          email($name, $label = '')
31
 * @method Field\Mobile         mobile($name, $label = '')
32
 * @method Field\Slider         slider($name, $label = '')
33
 * @method Field\File           file($name, $label = '')
34
 * @method Field\Image          image($name, $label = '')
35
 * @method Field\Date           date($name, $label = '')
36
 * @method Field\Datetime       datetime($name, $label = '')
37
 * @method Field\Time           time($name, $label = '')
38
 * @method Field\Year           year($column, $label = '')
39
 * @method Field\Month          month($column, $label = '')
40
 * @method Field\DateRange      dateRange($start, $end, $label = '')
41
 * @method Field\DateTimeRange  dateTimeRange($start, $end, $label = '')
42
 * @method Field\TimeRange      timeRange($start, $end, $label = '')
43
 * @method Field\Number         number($name, $label = '')
44
 * @method Field\Currency       currency($name, $label = '')
45
 * @method Field\SwitchField    switch($name, $label = '')
46
 * @method Field\Display        display($name, $label = '')
47
 * @method Field\Rate           rate($name, $label = '')
48
 * @method Field\Divider        divider($title = '')
49
 * @method Field\Decimal        decimal($column, $label = '')
50
 * @method Field\Html           html($html)
51
 * @method Field\Tags           tags($column, $label = '')
52
 * @method Field\Icon           icon($column, $label = '')
53
 * @method Field\Captcha        captcha($column, $label = '')
54
 * @method Field\Listbox        listbox($column, $label = '')
55
 * @method Field\Table          table($column, $label, $builder)
56
 * @method Field\Timezone       timezone($column, $label = '')
57
 * @method Field\KeyValue       keyValue($column, $label = '')
58
 * @method Field\ListField      list($column, $label = '')
59
 * @method mixed                handle(Request $request)
60
 */
61
class Form implements Renderable
62
{
63
    /**
64
     * The title of form.
65
     *
66
     * @var string
67
     */
68
    public $title;
69
70
    /**
71
     * @var Field[]
72
     */
73
    protected $fields = [];
74
75
    /**
76
     * @var array
77
     */
78
    protected $data = [];
79
80
    /**
81
     * @var array
82
     */
83
    protected $attributes = [];
84
85
    /**
86
     * Available buttons.
87
     *
88
     * @var array
89
     */
90
    protected $buttons = ['reset', 'submit'];
91
92
    /**
93
     * Width for label and submit field.
94
     *
95
     * @var array
96
     */
97
    protected $width = [
98
        'label' => 2,
99
        'field' => 8,
100
    ];
101
102
    /**
103
     * @var bool
104
     */
105
    public $inbox = true;
106
107
    /**
108
     * Form constructor.
109
     *
110
     * @param array $data
111
     */
112
    public function __construct($data = [])
113
    {
114
        $this->fill($data);
115
116
        $this->initFormAttributes();
117
    }
118
119
    /**
120
     * Get form title.
121
     *
122
     * @return mixed
123
     */
124
    protected function title()
125
    {
126
        return $this->title;
127
    }
128
129
    /**
130
     * @return array
131
     */
132
    public function data()
133
    {
134
        return $this->data;
135
    }
136
137
    /**
138
     * Fill data to form fields.
139
     *
140
     * @param array $data
141
     *
142
     * @return $this
143
     */
144
    public function fill($data = [])
145
    {
146
        if ($data instanceof Arrayable) {
147
            $data = $data->toArray();
148
        }
149
150
        if (!empty($data)) {
151
            $this->data = $data;
152
        }
153
154
        return $this;
155
    }
156
157
    /**
158
     * @return $this
159
     */
160
    public function sanitize()
161
    {
162
        foreach (['_form_', '_token'] as $key) {
163
            request()->request->remove($key);
164
        }
165
166
        return $this;
167
    }
168
169
    /**
170
     * Initialize the form attributes.
171
     */
172
    protected function initFormAttributes()
173
    {
174
        $this->attributes = [
175
            'method'         => 'POST',
176
            'action'         => '',
177
            'class'          => 'form-horizontal',
178
            'accept-charset' => 'UTF-8',
179
            'pjax-container' => true,
180
        ];
181
    }
182
183
    /**
184
     * Add form attributes.
185
     *
186
     * @param string|array $attr
187
     * @param string       $value
188
     *
189
     * @return $this
190
     */
191
    public function attribute($attr, $value = '')
192
    {
193
        if (is_array($attr)) {
194
            foreach ($attr as $key => $value) {
195
                $this->attribute($key, $value);
196
            }
197
        } else {
198
            $this->attributes[$attr] = $value;
199
        }
200
201
        return $this;
202
    }
203
204
    /**
205
     * Format form attributes form array to html.
206
     *
207
     * @param array $attributes
208
     *
209
     * @return string
210
     */
211
    public function formatAttribute($attributes = [])
212
    {
213
        $attributes = $attributes ?: $this->attributes;
214
215
        if ($this->hasFile()) {
216
            $attributes['enctype'] = 'multipart/form-data';
217
        }
218
219
        $html = [];
220
        foreach ($attributes as $key => $val) {
221
            $html[] = "$key=\"$val\"";
222
        }
223
224
        return implode(' ', $html) ?: '';
225
    }
226
227
    /**
228
     * Action uri of the form.
229
     *
230
     * @param string $action
231
     *
232
     * @return $this
233
     */
234
    public function action($action)
235
    {
236
        return $this->attribute('action', $action);
237
    }
238
239
    /**
240
     * Method of the form.
241
     *
242
     * @param string $method
243
     *
244
     * @return $this
245
     */
246
    public function method($method = 'POST')
247
    {
248
        if (strtolower($method) == 'put') {
249
            $this->hidden('_method')->default($method);
250
251
            return $this;
252
        }
253
254
        return $this->attribute('method', strtoupper($method));
255
    }
256
257
    /**
258
     * Disable Pjax.
259
     *
260
     * @return $this
261
     */
262
    public function disablePjax()
263
    {
264
        Arr::forget($this->attributes, 'pjax-container');
265
266
        return $this;
267
    }
268
269
    /**
270
     * Disable reset button.
271
     *
272
     * @return $this
273
     */
274
    public function disableReset()
275
    {
276
        array_delete($this->buttons, 'reset');
277
278
        return $this;
279
    }
280
281
    /**
282
     * Disable submit button.
283
     *
284
     * @return $this
285
     */
286
    public function disableSubmit()
287
    {
288
        array_delete($this->buttons, 'submit');
289
290
        return $this;
291
    }
292
293
    /**
294
     * Set field and label width in current form.
295
     *
296
     * @param int $fieldWidth
297
     * @param int $labelWidth
298
     *
299
     * @return $this
300
     */
301
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
302
    {
303
        collect($this->fields)->each(function ($field) use ($fieldWidth, $labelWidth) {
304
            /* @var Field $field  */
305
            $field->setWidth($fieldWidth, $labelWidth);
306
        });
307
308
        // set this width
309
        $this->width = [
310
            'label' => $labelWidth,
311
            'field' => $fieldWidth,
312
        ];
313
314
        return $this;
315
    }
316
317
    /**
318
     * Determine if the form has field type.
319
     *
320
     * @param string $name
321
     *
322
     * @return bool
323
     */
324
    public function hasField($name)
325
    {
326
        return isset(BaseForm::$availableFields[$name]);
327
    }
328
329
    /**
330
     * Add a form field to form.
331
     *
332
     * @param Field $field
333
     *
334
     * @return $this
335
     */
336
    public function pushField(Field &$field)
337
    {
338
        array_push($this->fields, $field);
339
340
        return $this;
341
    }
342
343
    /**
344
     * Get all fields of form.
345
     *
346
     * @return Field[]
347
     */
348
    public function fields()
349
    {
350
        return $this->fields;
351
    }
352
353
    /**
354
     * Get variables for render form.
355
     *
356
     * @return array
357
     */
358
    protected function getVariables()
359
    {
360
        foreach ($this->fields as $field) {
361
            $field->fill($this->data());
362
        }
363
364
        return [
365
            'fields'     => $this->fields,
366
            'attributes' => $this->formatAttribute(),
367
            'method'     => $this->attributes['method'],
368
            'buttons'    => $this->buttons,
369
            'width'      => $this->width,
370
        ];
371
    }
372
373
    /**
374
     * Determine if form fields has files.
375
     *
376
     * @return bool
377
     */
378
    public function hasFile()
379
    {
380
        foreach ($this->fields as $field) {
381
            if ($field instanceof Field\File) {
382
                return true;
383
            }
384
        }
385
386
        return false;
387
    }
388
389
    /**
390
     * Validate this form fields.
391
     *
392
     * @param Request $request
393
     *
394
     * @return bool|MessageBag
395
     */
396
    public function validate(Request $request)
397
    {
398
        if (method_exists($this, 'form')) {
399
            $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...
400
        }
401
402
        $failedValidators = [];
403
404
        /** @var Field $field */
405 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...
406
            if (!$validator = $field->getValidator($request->all())) {
407
                continue;
408
            }
409
410
            if (($validator instanceof Validator) && !$validator->passes()) {
411
                $failedValidators[] = $validator;
412
            }
413
        }
414
415
        $message = $this->mergeValidationMessages($failedValidators);
416
417
        return $message->any() ? $message : false;
418
    }
419
420
    /**
421
     * Merge validation messages from input validators.
422
     *
423
     * @param \Illuminate\Validation\Validator[] $validators
424
     *
425
     * @return MessageBag
426
     */
427
    protected function mergeValidationMessages($validators)
428
    {
429
        $messageBag = new MessageBag();
430
431
        foreach ($validators as $validator) {
432
            $messageBag = $messageBag->merge($validator->messages());
433
        }
434
435
        return $messageBag;
436
    }
437
438
    /**
439
     * Add a fieldset to form.
440
     *
441
     * @param string  $title
442
     * @param Closure $setCallback
443
     *
444
     * @return Field\Fieldset
445
     */
446 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...
447
    {
448
        $fieldset = new Field\Fieldset();
449
450
        $this->html($fieldset->start($title))->plain();
451
452
        $setCallback($this);
453
454
        $this->html($fieldset->end())->plain();
455
456
        return $fieldset;
457
    }
458
459
    public function unbox()
460
    {
461
        $this->inbox = false;
462
463
        return $this;
464
    }
465
466
    protected function prepareForm()
467
    {
468
        if (method_exists($this, 'form')) {
469
            $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...
470
        }
471
    }
472
473
    protected function prepareHandle()
474
    {
475
        if (method_exists($this, 'handle')) {
476
            $this->method('POST');
477
            $this->action(route('admin.handle-form'));
478
            $this->hidden('_form_')->default(get_called_class());
479
        }
480
    }
481
482
    /**
483
     * Render the form.
484
     *
485
     * @return string
486
     */
487
    public function render()
488
    {
489
        $this->prepareForm();
490
491
        $this->prepareHandle();
492
493
        $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...
494
495
        if (!($title = $this->title()) || !$this->inbox) {
496
            return $form;
497
        }
498
499
        return (new Box($title, $form))->render();
500
    }
501
502
    /**
503
     * Generate a Field object and add to form builder if Field exists.
504
     *
505
     * @param string $method
506
     * @param array  $arguments
507
     *
508
     * @return Field|$this
509
     */
510
    public function __call($method, $arguments)
511
    {
512
        if (!$this->hasField($method)) {
513
            return $this;
514
        }
515
516
        $class = BaseForm::$availableFields[$method];
517
518
        $field = new $class(Arr::get($arguments, 0), array_slice($arguments, 1));
519
520
        return tap($field, function ($field) {
521
            $this->pushField($field);
522
        });
523
    }
524
}
525