Completed
Pull Request — master (#255)
by Shaun
06:13
created

FormField   C

Complexity

Total Complexity 76

Size/Duplication

Total Lines 611
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.33%

Importance

Changes 38
Bugs 17 Features 16
Metric Value
c 38
b 17
f 16
dl 0
loc 611
ccs 183
cts 194
cp 0.9433
rs 5.439
wmc 76
lcom 1
cbo 6

34 Methods

Rating   Name   Duplication   Size   Complexity  
getTemplate() 0 1 ?
A getViewTemplate() 0 4 1
A __construct() 0 10 1
B setupValue() 0 15 6
A getModelValueAttribute() 0 11 4
B render() 0 36 6
A transformKey() 0 4 1
D prepareOptions() 0 57 13
A getName() 0 4 1
A setName() 0 6 1
A getNameKey() 0 4 1
A getOptions() 0 4 1
A getOption() 0 4 1
A setOptions() 0 6 1
A setOption() 0 6 1
A getType() 0 4 1
A setType() 0 8 2
A getParent() 0 4 1
A isRendered() 0 4 1
A getDefaults() 0 4 1
A allDefaults() 0 19 1
A getRealName() 0 4 1
B setValue() 0 20 5
A setTemplate() 0 4 1
B addErrorClass() 0 14 5
A setDefaultOptions() 0 6 1
A setupLabel() 0 14 3
A needsLabel() 0 11 4
A disable() 0 6 1
A enable() 0 6 1
B getValidationRules() 0 26 5
A getValue() 0 4 1
A getDefaultValue() 0 4 1
A isValidValue() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like FormField 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 FormField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kris\LaravelFormBuilder\Fields;
4
5
use Illuminate\Database\Eloquent\Collection;
6
use Illuminate\Database\Eloquent\Model;
7
use Kris\LaravelFormBuilder\Form;
8
use Kris\LaravelFormBuilder\FormHelper;
9
use Kris\LaravelFormBuilder\RulesParser;
10
11
/**
12
 * Class FormField
13
 *
14
 * @package Kris\LaravelFormBuilder\Fields
15
 */
16
abstract class FormField
17
{
18
    /**
19
     * Name of the field
20
     *
21
     * @var
22
     */
23
    protected $name;
24
25
    /**
26
     * Type of the field
27
     *
28
     * @var
29
     */
30
    protected $type;
31
32
    /**
33
     * All options for the field
34
     *
35
     * @var
36
     */
37
    protected $options = [];
38
39
    /**
40
     * Is field rendered
41
     *
42
     * @var bool
43
     */
44
    protected $rendered = false;
45
46
    /**
47
     * @var Form
48
     */
49
    protected $parent;
50
51
    /**
52
     * @var string
53
     */
54
    protected $template;
55
56
    /**
57
     * @var FormHelper
58
     */
59
    protected $formHelper;
60
61
    /**
62
     * Name of the property for value setting
63
     *
64
     * @var string
65
     */
66
    protected $valueProperty = 'value';
67
68
    /**
69
     * Name of the property for default value
70
     *
71
     * @var string
72
     */
73
    protected $defaultValueProperty = 'default_value';
74
75
    /**
76
     * Is default value set?
77
     * @var bool
78
     */
79
    protected $hasDefault = false;
80
81
    /**
82
     * @var \Closure|null
83
     */
84
    protected $valueClosure = null;
85
86
    /**
87
     * @param             $name
88
     * @param             $type
89
     * @param Form        $parent
90
     * @param array       $options
91
     */
92 71
    public function __construct($name, $type, Form $parent, array $options = [])
93
    {
94 71
        $this->name = $name;
95 71
        $this->type = $type;
96 71
        $this->parent = $parent;
97 71
        $this->formHelper = $this->parent->getFormHelper();
98 71
        $this->setTemplate();
99 71
        $this->setDefaultOptions($options);
100 71
        $this->setupValue();
101 66
    }
102
103 71
    protected function setupValue()
104
    {
105 71
        $value = $this->getOption($this->valueProperty);
106 71
        $isChild = $this->getOption('is_child');
107
108 71
        if ($value instanceof \Closure) {
109
            $this->valueClosure = $value;
110
        }
111
112 71
        if (($value === null || $value instanceof \Closure) && !$isChild) {
113 62
            $this->setValue($this->getModelValueAttribute($this->parent->getModel(), $this->name));
114 18
        } elseif (!$isChild) {
115 12
            $this->hasDefault = true;
116
        }
117 66
    }
118
119
    /**
120
     * Get the template, can be config variable or view path
121
     *
122
     * @return string
123
     */
124
    abstract protected function getTemplate();
125
126
    /**
127
     * @return string
128
     */
129 27
    protected function getViewTemplate()
130
    {
131 27
        return $this->parent->getTemplatePrefix() . $this->getOption('template', $this->template);
132
    }
133
134
    /**
135
     * @param array $options
136
     * @param bool  $showLabel
137
     * @param bool  $showField
138
     * @param bool  $showError
139
     * @return string
140
     */
141 27
    public function render(array $options = [], $showLabel = true, $showField = true, $showError = true)
142
    {
143 27
        $this->prepareOptions($options);
144 27
        $value = $this->getValue();
145 27
        $defaultValue = $this->getDefaultValue();
146
147 27
        if ($showField) {
148 27
            $this->rendered = true;
149
        }
150
151
        // Override default value with value
152 27
        if (!$this->isValidValue($value) && $this->isValidValue($defaultValue)) {
153
            $this->setOption($this->valueProperty, $defaultValue);
154
        }
155
156 27
        if (!$this->needsLabel()) {
157 8
            $showLabel = false;
158
        }
159
160 27
        if ($showError) {
161 26
            $showError = $this->parent->haveErrorsEnabled();
162
        }
163
164 27
        return $this->formHelper->getView()->make(
165 27
            $this->getViewTemplate(),
166
            [
167 27
                'name' => $this->name,
168 27
                'nameKey' => $this->getNameKey(),
169 27
                'type' => $this->type,
170 27
                'options' => $this->options,
171 27
                'showLabel' => $showLabel,
172 27
                'showField' => $showField,
173 27
                'showError' => $showError
174
            ]
175 27
        )->render();
176
    }
177
178
    /**
179
     * Get the attribute value from the model by name
180
     *
181
     * @param mixed $model
182
     * @param string $name
183
     * @return mixed
184
     */
185 64
    protected function getModelValueAttribute($model, $name)
186
    {
187 64
        $transformedName = $this->transformKey($name);
188 64
        if (is_string($model)) {
189
            return $model;
190 64
        } elseif (is_object($model)) {
191 2
            return object_get($model, $transformedName);
192 64
        } elseif (is_array($model)) {
193 63
            return array_get($model, $transformedName);
194
        }
195 5
    }
196
197
    /**
198
     * Transform array like syntax to dot syntax
199
     *
200
     * @param $key
201
     * @return mixed
202
     */
203 71
    protected function transformKey($key)
204
    {
205 71
        return $this->formHelper->transformToDotSyntax($key);
206
    }
207
208
    /**
209
     * Prepare options for rendering
210
     *
211
     * @param array $options
212
     * @return array
213
     */
214 71
    protected function prepareOptions(array $options = [])
215
    {
216 71
        $helper = $this->formHelper;
217 71
        $rulesParser = new RulesParser($this);
218 71
        $rules = $this->getOption('rules');
219 71
        $parsedRules = $rules ? $rulesParser->parse($rules) : [];
220
221 71
        $this->options = $helper->mergeOptions($this->options, $options);
222
223
        // Append class attribute
224 71
        if ($this->getOption('attr.class_append')) {
225
            $appendedClass = $this->getOption('attr.class', '') . ' ' . $this->getOption('attr.class_append');
226
            $this->setOption('attr.class', $appendedClass);
227
        }
228
        
229 71
        if ($this->getOption('attr.multiple') && !$this->getOption('tmp.multipleBracesSet')) {
230 2
            $this->name = $this->name.'[]';
231 2
            $this->setOption('tmp.multipleBracesSet', true);
232
        }
233
234 71
        if ($this->parent->haveErrorsEnabled()) {
235 71
            $this->addErrorClass();
236
        }
237
238 71
        if ($this->parent->clientValidationEnabled()) {
239 71
            if ($this->getOption('required') === true || isset($parsedRules['required'])) {
240 3
                $lblClass = $this->getOption('label_attr.class', '');
241 3
                $requiredClass = $helper->getConfig('defaults.required_class', 'required');
242 3
                if (!str_contains($lblClass, $requiredClass)) {
243 3
                    $lblClass .= ' ' . $requiredClass;
244 3
                    $this->setOption('label_attr.class', $lblClass);
245 3
                    $this->setOption('attr.required', 'required');
246
                }
247
            }
248
249 71
            if ($parsedRules) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parsedRules 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...
250 1
                $attrs = $this->getOption('attr') + $parsedRules;
251 1
                $this->setOption('attr', $attrs);
252
            }
253
        }
254
255 71
        $this->setOption('wrapperAttrs', $helper->prepareAttributes($this->getOption('wrapper')));
256 71
        $this->setOption('errorAttrs', $helper->prepareAttributes($this->getOption('errors')));
257
258 71
        if ($this->getOption('is_child')) {
259 16
            $this->setOption('labelAttrs', $helper->prepareAttributes($this->getOption('label_attr')));
260
        }
261
262 71
        if ($this->getOption('help_block.text')) {
263 1
            $this->setOption(
264 1
                'help_block.helpBlockAttrs',
265 1
                $helper->prepareAttributes($this->getOption('help_block.attr'))
266
            );
267
        }
268
269 71
        return $this->options;
270
    }
271
272
    /**
273
     * Get name of the field
274
     *
275
     * @return string
276
     */
277 23
    public function getName()
278
    {
279 23
        return $this->name;
280
    }
281
282
    /**
283
     * Set name of the field
284
     *
285
     * @param string $name
286
     * @return $this
287
     */
288 11
    public function setName($name)
289
    {
290 11
        $this->name = $name;
291
292 11
        return $this;
293
    }
294
295
    /**
296
     * Get dot notation key for fields
297
     *
298
     * @return string
299
     **/
300 39
    public function getNameKey()
301
    {
302 39
        return $this->transformKey($this->name);
303
    }
304
305
    /**
306
     * Get field options
307
     *
308
     * @return array
309
     */
310 10
    public function getOptions()
311
    {
312 10
        return $this->options;
313
    }
314
315
    /**
316
     * Get single option from options array. Can be used with dot notation ('attr.class')
317
     *
318
     * @param        $option
319
     * @param mixed  $default
320
     *
321
     * @return mixed
322
     */
323 71
    public function getOption($option, $default = null)
324
    {
325 71
        return array_get($this->options, $option, $default);
326
    }
327
328
    /**
329
     * Set field options
330
     *
331
     * @param array $options
332
     * @return $this
333
     */
334 11
    public function setOptions($options)
335
    {
336 11
        $this->options = $this->prepareOptions($options);
337
338 11
        return $this;
339
    }
340
341
    /**
342
     * Set single option on the field
343
     *
344
     * @param string $name
345
     * @param mixed $value
346
     * @return $this
347
     */
348 71
    public function setOption($name, $value)
349
    {
350 71
        array_set($this->options, $name, $value);
351
352 71
        return $this;
353
    }
354
355
    /**
356
     * Get the type of the field
357
     *
358
     * @return string
359
     */
360 41
    public function getType()
361
    {
362 41
        return $this->type;
363
    }
364
365
    /**
366
     * Set type of the field
367
     *
368
     * @param mixed $type
369
     * @return $this
370
     */
371 1
    public function setType($type)
372
    {
373 1
        if ($this->formHelper->getFieldType($type)) {
374 1
            $this->type = $type;
375
        }
376
377 1
        return $this;
378
    }
379
380
    /**
381
     * @return Form
382
     */
383 71
    public function getParent()
384
    {
385 71
        return $this->parent;
386
    }
387
388
    /**
389
     * Check if the field is rendered
390
     *
391
     * @return bool
392
     */
393 4
    public function isRendered()
394
    {
395 4
        return $this->rendered;
396
    }
397
398
    /**
399
     * Default options for field
400
     *
401
     * @return array
402
     */
403 51
    protected function getDefaults()
404
    {
405 51
        return [];
406
    }
407
408
    /**
409
     * Defaults used across all fields
410
     *
411
     * @return array
412
     */
413 71
    private function allDefaults()
414
    {
415
        return [
416 71
            'wrapper' => ['class' => $this->formHelper->getConfig('defaults.wrapper_class')],
417 71
            'attr' => ['class' => $this->formHelper->getConfig('defaults.field_class')],
418 71
            'help_block' => ['text' => null, 'tag' => 'p', 'attr' => [
419 71
                'class' => $this->formHelper->getConfig('defaults.help_block_class')
420
            ]],
421
            'value' => null,
422
            'default_value' => null,
423
            'label' => null,
424
            'label_show' => true,
425
            'is_child' => false,
426 71
            'label_attr' => ['class' => $this->formHelper->getConfig('defaults.label_class')],
427 71
            'errors' => ['class' => $this->formHelper->getConfig('defaults.error_class')],
428
            'rules' => [],
429
            'error_messages' => []
430
        ];
431
    }
432
433
    /**
434
     * Get real name of the field without form namespace
435
     *
436
     * @return string
437
     */
438 70
    public function getRealName()
439
    {
440 70
        return $this->getOption('real_name', $this->name);
441
    }
442
443
    /**
444
     * @param $value
445
     * @return $this
446
     */
447 65
    public function setValue($value)
448
    {
449 65
        if ($this->hasDefault) {
450 1
            return $this;
451
        }
452
453 65
        $closure = $this->valueClosure;
454
455 65
        if ($closure instanceof \Closure) {
456
            $value = $closure($value ?: null);
457
        }
458
459 65
        if (!$this->isValidValue($value)) {
460 63
            $value = $this->getOption($this->defaultValueProperty);
461
        }
462
463 65
        $this->options[$this->valueProperty] = $value;
464
465 65
        return $this;
466
    }
467
468
    /**
469
     * Set the template property on the object
470
     */
471 71
    private function setTemplate()
472
    {
473 71
        $this->template = $this->formHelper->getConfig($this->getTemplate(), $this->getTemplate());
474 71
    }
475
476
    /**
477
     * Add error class to wrapper if validation errors exist
478
     */
479 71
    protected function addErrorClass()
480
    {
481 71
        $errors = $this->parent->getRequest()->session()->get('errors');
482
483 71
        if ($errors && $errors->has($this->getNameKey())) {
484
            $errorClass = $this->formHelper->getConfig('defaults.wrapper_error_class');
485
            $wrapperClass = $this->getOption('wrapper.class');
486
487
            if ($this->getOption('wrapper') && !str_contains($wrapperClass, $errorClass)) {
488
                $wrapperClass .= ' ' . $errorClass;
489
                $this->setOption('wrapper.class', $wrapperClass);
490
            }
491
        }
492 71
    }
493
494
495
    /**
496
     * Merge all defaults with field specific defaults and set template if passed
497
     *
498
     * @param array $options
499
     */
500 71
    protected function setDefaultOptions(array $options = [])
501
    {
502 71
        $this->options = $this->formHelper->mergeOptions($this->allDefaults(), $this->getDefaults());
503 71
        $this->options = $this->prepareOptions($options);
504 71
        $this->setupLabel();
505 71
    }
506
507 71
    protected function setupLabel()
508
    {
509 71
        if ($this->getOption('label') !== null) {
510 18
            return;
511
        }
512
513 69
        if ($langName = $this->parent->getLanguageName()) {
514 4
            $label = sprintf('%s.%s', $langName, $this->getRealName());
515
        } else {
516 66
            $label = $this->getRealName();
517
        }
518
519 69
        $this->setOption('label', $this->formHelper->formatLabel($label));
520 69
    }
521
522
    /**
523
     * Check if fields needs label
524
     *
525
     * @return bool
526
     */
527 27
    protected function needsLabel()
528
    {
529
        // If field is <select> and child of choice, we don't need label for it
530 27
        $isChildSelect = $this->type == 'select' && $this->getOption('is_child') === true;
531
532 27
        if ($this->type == 'hidden' || $isChildSelect) {
533 8
            return false;
534
        }
535
536 24
        return true;
537
    }
538
539
    /**
540
     * Disable field
541
     *
542
     * @return $this
543
     */
544 1
    public function disable()
545
    {
546 1
        $this->setOption('attr.disabled', 'disabled');
547
548 1
        return $this;
549
    }
550
551
    /**
552
     * Enable field
553
     *
554
     * @return $this
555
     */
556 1
    public function enable()
557
    {
558 1
        array_forget($this->options, 'attr.disabled');
559
560 1
        return $this;
561
    }
562
563
    /**
564
     * Get validation rules for a field if any with label for attributes
565
     *
566
     * @return array|null
567
     */
568 4
    public function getValidationRules()
569
    {
570 4
        $rules = $this->getOption('rules', []);
571 4
        $name = $this->getNameKey();
572 4
        $messages = $this->getOption('error_messages', []);
573 4
        $formName = $this->parent->getName();
574
575 4
        if ($messages && $formName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $formName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
576 1
            $newMessages = [];
577 1
            foreach ($messages as $messageKey => $message) {
578 1
                $messageKey = sprintf('%s.%s', $formName, $messageKey);
579 1
                $newMessages[$messageKey] = $message;
580
            }
581 1
            $messages = $newMessages;
582
        }
583
584 4
        if (!$rules) {
585 1
            return [];
586
        }
587
588
        return [
589 4
            'rules' => [$name => $rules],
590 4
            'attributes' => [$name => $this->getOption('label')],
591 4
            'error_messages' => $messages
592
        ];
593
    }
594
595
    /**
596
     * Get value property
597
     *
598
     * @param mixed|null $default
599
     * @return mixed
600
     */
601 30
    public function getValue($default = null)
602
    {
603 30
        return $this->getOption($this->valueProperty, $default);
604
    }
605
606
    /**
607
     * Get default value property
608
     *
609
     * @param mixed|null $default
610
     * @return mixed
611
     */
612 27
    public function getDefaultValue($default = null)
613
    {
614 27
        return $this->getOption($this->defaultValueProperty, $default);
615
    }
616
617
    /**
618
     * Check if provided value is valid for this type
619
     *
620
     * @return bool
621
     */
622 66
    protected function isValidValue($value)
623
    {
624 66
        return $value !== null;
625
    }
626
}
627