Completed
Push — master ( f5a087...fbf1fa )
by Song
02:41
created

Form   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 411
Duplicated Lines 2.19 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 9
loc 411
rs 8.64
c 0
b 0
f 0
wmc 47
lcom 1
cbo 9

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 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 20 4
A __call() 0 12 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
 *
57
 * @method mixed                handle(Request $request)
58
 */
59
class Form implements Renderable
60
{
61
    /**
62
     * @var Field[]
63
     */
64
    protected $fields = [];
65
66
    /**
67
     * @var array
68
     */
69
    protected $data = [];
70
71
    /**
72
     * @var array
73
     */
74
    protected $attributes = [];
75
76
    /**
77
     * Available buttons.
78
     *
79
     * @var array
80
     */
81
    protected $buttons = ['reset', 'submit'];
82
83
    /**
84
     * Width for label and submit field.
85
     *
86
     * @var array
87
     */
88
    protected $width = [
89
        'label' => 2,
90
        'field' => 8,
91
    ];
92
93
    /**
94
     * Form constructor.
95
     *
96
     * @param array $data
97
     */
98
    public function __construct($data = [])
99
    {
100
        $this->fill($data);
101
102
        $this->initFormAttributes();
103
    }
104
105
    /**
106
     * @return array
107
     */
108
    public function data()
109
    {
110
        return $this->data;
111
    }
112
113
    /**
114
     * Fill data to form fields.
115
     *
116
     * @param array $data
117
     * @return $this
118
     */
119
    public function fill($data = [])
120
    {
121
        if ($data instanceof Arrayable) {
122
            $data = $data->toArray();
123
        }
124
125
        if (!empty($data)) {
126
            $this->data = $data;
127
        }
128
129
        return $this;
130
    }
131
132
    /**
133
     * @return $this
134
     */
135
    public function sanitize()
136
    {
137
        foreach (['_form_', '_token'] as $key) {
138
            request()->request->remove($key);
139
        }
140
141
        return $this;
142
    }
143
144
    /**
145
     * Initialize the form attributes.
146
     */
147
    protected function initFormAttributes()
148
    {
149
        $this->attributes = [
150
            'method'         => 'POST',
151
            'action'         => '',
152
            'class'          => 'form-horizontal',
153
            'accept-charset' => 'UTF-8',
154
            'pjax-container' => true,
155
        ];
156
    }
157
158
    /**
159
     * Add form attributes.
160
     *
161
     * @param string|array $attr
162
     * @param string       $value
163
     *
164
     * @return $this
165
     */
166
    public function attribute($attr, $value = '')
167
    {
168
        if (is_array($attr)) {
169
            foreach ($attr as $key => $value) {
170
                $this->attribute($key, $value);
171
            }
172
        } else {
173
            $this->attributes[$attr] = $value;
174
        }
175
176
        return $this;
177
    }
178
179
    /**
180
     * Format form attributes form array to html.
181
     *
182
     * @param array $attributes
183
     *
184
     * @return string
185
     */
186
    public function formatAttribute($attributes = [])
187
    {
188
        $attributes = $attributes ?: $this->attributes;
189
190
        if ($this->hasFile()) {
191
            $attributes['enctype'] = 'multipart/form-data';
192
        }
193
194
        $html = [];
195
        foreach ($attributes as $key => $val) {
196
            $html[] = "$key=\"$val\"";
197
        }
198
199
        return implode(' ', $html) ?: '';
200
    }
201
202
    /**
203
     * Action uri of the form.
204
     *
205
     * @param string $action
206
     *
207
     * @return $this
208
     */
209
    public function action($action)
210
    {
211
        return $this->attribute('action', $action);
212
    }
213
214
    /**
215
     * Method of the form.
216
     *
217
     * @param string $method
218
     *
219
     * @return $this
220
     */
221
    public function method($method = 'POST')
222
    {
223
        if (strtolower($method) == 'put') {
224
            $this->hidden('_method')->default($method);
225
226
            return $this;
227
        }
228
229
        return $this->attribute('method', strtoupper($method));
230
    }
231
232
    /**
233
     * Disable Pjax.
234
     *
235
     * @return $this
236
     */
237
    public function disablePjax()
238
    {
239
        Arr::forget($this->attributes, 'pjax-container');
240
241
        return $this;
242
    }
243
244
    /**
245
     * Disable reset button.
246
     *
247
     * @return $this
248
     */
249
    public function disableReset()
250
    {
251
        array_delete($this->buttons, 'reset');
252
253
        return $this;
254
    }
255
256
    /**
257
     * Disable submit button.
258
     *
259
     * @return $this
260
     */
261
    public function disableSubmit()
262
    {
263
        array_delete($this->buttons, 'submit');
264
265
        return $this;
266
    }
267
268
    /**
269
     * Set field and label width in current form.
270
     *
271
     * @param int $fieldWidth
272
     * @param int $labelWidth
273
     *
274
     * @return $this
275
     */
276
    public function setWidth($fieldWidth = 8, $labelWidth = 2)
277
    {
278
        collect($this->fields)->each(function ($field) use ($fieldWidth, $labelWidth) {
279
            /* @var Field $field  */
280
            $field->setWidth($fieldWidth, $labelWidth);
281
        });
282
283
        // set this width
284
        $this->width = [
285
            'label' => $labelWidth,
286
            'field' => $fieldWidth,
287
        ];
288
289
        return $this;
290
    }
291
292
    /**
293
     * Determine if the form has field type.
294
     *
295
     * @param string $name
296
     *
297
     * @return bool
298
     */
299
    public function hasField($name)
300
    {
301
        return isset(BaseForm::$availableFields[$name]);
302
    }
303
304
    /**
305
     * Add a form field to form.
306
     *
307
     * @param Field $field
308
     *
309
     * @return $this
310
     */
311
    public function pushField(Field &$field)
312
    {
313
        array_push($this->fields, $field);
314
315
        return $this;
316
    }
317
318
    /**
319
     * Get all fields of form.
320
     *
321
     * @return Field[]
322
     */
323
    public function fields()
324
    {
325
        return $this->fields;
326
    }
327
328
    /**
329
     * Get variables for render form.
330
     *
331
     * @return array
332
     */
333
    protected function getVariables()
334
    {
335
        foreach ($this->fields as $field) {
336
            $field->fill($this->data());
337
        }
338
339
        return [
340
            'fields'     => $this->fields,
341
            'attributes' => $this->formatAttribute(),
342
            'method'     => $this->attributes['method'],
343
            'buttons'    => $this->buttons,
344
            'width'      => $this->width,
345
        ];
346
    }
347
348
    /**
349
     * Determine if form fields has files.
350
     *
351
     * @return bool
352
     */
353
    public function hasFile()
354
    {
355
        foreach ($this->fields as $field) {
356
            if ($field instanceof Field\File) {
357
                return true;
358
            }
359
        }
360
361
        return false;
362
    }
363
364
    /**
365
     * Validate this form fields.
366
     *
367
     * @param Request $request
368
     *
369
     * @return bool|MessageBag
370
     */
371
    public function validate(Request $request)
372
    {
373
        if (method_exists($this, 'form')) {
374
            $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...
375
        }
376
377
        $failedValidators = [];
378
379
        /** @var Field $field */
380 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...
381
            if (!$validator = $field->getValidator($request->all())) {
382
                continue;
383
            }
384
385
            if (($validator instanceof Validator) && !$validator->passes()) {
386
                $failedValidators[] = $validator;
387
            }
388
        }
389
390
        $message = $this->mergeValidationMessages($failedValidators);
391
392
        return $message->any() ? $message : false;
393
    }
394
395
    /**
396
     * Merge validation messages from input validators.
397
     *
398
     * @param \Illuminate\Validation\Validator[] $validators
399
     *
400
     * @return MessageBag
401
     */
402
    protected function mergeValidationMessages($validators)
403
    {
404
        $messageBag = new MessageBag();
405
406
        foreach ($validators as $validator) {
407
            $messageBag = $messageBag->merge($validator->messages());
408
        }
409
410
        return $messageBag;
411
    }
412
413
    /**
414
     * Render the form.
415
     *
416
     * @return string
417
     */
418
    public function render()
419
    {
420
        if (method_exists($this, 'form')) {
421
            $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...
422
        }
423
424
        if (method_exists($this, 'handle')) {
425
            $this->method('POST');
426
            $this->action(route('admin.handle-form'));
427
            $this->hidden('_form_')->default(get_called_class());
428
        }
429
430
        $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...
431
432
        if (!property_exists($this, 'title')) {
433
            return $form;
434
        }
435
436
        return new Box($this->title, $form);
0 ignored issues
show
Bug introduced by
The property title does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
437
    }
438
439
    /**
440
     * Generate a Field object and add to form builder if Field exists.
441
     *
442
     * @param string $method
443
     * @param array  $arguments
444
     *
445
     * @return Field|$this
446
     */
447
    public function __call($method, $arguments)
448
    {
449
        if (! $this->hasField($method)) {
450
            return $this;
451
        }
452
453
        $class = BaseForm::$availableFields[$method];
454
455
        return tap(new $class(...$arguments), function ($field) {
456
            $this->pushField($field);
457
        });
458
    }
459
460
    /**
461
     * Output as string.
462
     *
463
     * @return string
464
     */
465
    public function __toString()
466
    {
467
        return $this->render();
468
    }
469
}
470