Completed
Push — master ( f71a30...1681a3 )
by Song
02:56
created

Form   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 432
Duplicated Lines 2.08 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 9
loc 432
rs 8.5599
c 0
b 0
f 0
wmc 48
lcom 1
cbo 9

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A title() 0 4 1
A data() 0 4 1
A fill() 0 12 3
A sanitize() 0 8 2
A initFormAttributes() 0 10 1
A attribute() 0 12 3
A formatAttribute() 0 15 5
A action() 0 4 1
A method() 0 10 2
A disablePjax() 0 6 1
A disableReset() 0 6 1
A disableSubmit() 0 6 1
A setWidth() 0 15 1
A hasField() 0 4 1
A pushField() 0 6 1
A fields() 0 4 1
A getVariables() 0 14 2
A hasFile() 0 10 3
B validate() 9 23 7
A mergeValidationMessages() 0 10 2
A render() 0 22 4
A __call() 0 14 2
A __toString() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Form often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Form, and based on these observations, apply Extract Interface, too.

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