Completed
Push — master ( b95bca...290fd1 )
by Propa
03:01
created

TranslatableBootForm   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%
Metric Value
wmc 41
lcom 1
cbo 0
dl 0
loc 405
ccs 115
cts 115
cp 1
rs 8.2769

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A __call() 0 18 3
A __toString() 0 4 1
A reset() 0 10 1
A locales() 0 6 2
A element() 0 6 2
A arguments() 0 6 2
A methods() 0 6 2
A cloneElement() 0 6 2
A translatableIndicator() 0 6 2
A overwriteArgument() 0 8 1
A addMethod() 0 10 2
B render() 0 33 6
C createInput() 0 35 8
A applyElementBehavior() 0 8 3
A mapArguments() 0 6 2
A setTranslatableLabelIndicator() 0 5 1

How to fix   Complexity   

Complex Class

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

1
<?php namespace Propaganistas\LaravelTranslatableBootForms;
2
3
use AdamWathan\BootForms\BootForm;
4
5
class TranslatableBootForm
6
{
7
8
    /**
9
     * BootForm implementation.
10
     *
11
     * @var \AdamWathan\BootForms\BootForm
12
     */
13
    protected $form;
14
15
    /**
16
     * Array holding config values.
17
     *
18
     * @var array
19
     */
20
    protected $config;
21
22
    /**
23
     * Array of locale keys.
24
     *
25
     * @var array
26
     */
27
    protected $locales;
28
29
    /**
30
     * The current element type this class is working on.
31
     *
32
     * @var string
33
     */
34
    protected $element;
35
36
    /**
37
     * The array of arguments to pass in when creating the element.
38
     *
39
     * @var array
40
     */
41
    protected $arguments = [];
42
43
    /**
44
     * A keyed array of method => arguments to call on the created input.
45
     *
46
     * @var array
47
     */
48
    protected $methods = [];
49
50
    /**
51
     * Boolean indicating if the element should be cloned with corresponding translation name attributes.
52
     *
53
     * @var bool
54
     */
55
    protected $cloneElement = false;
56
57
    /**
58
     * Boolean indicating if the element should have an indication that is it a translation.
59
     *
60
     * @var bool
61
     */
62
    protected $translatableIndicator = false;
63
64
    /**
65
     * Array holding the mappable element arguments.
66
     *
67
     * @var array
68
     */
69
    private $mappableArguments = [
70
        'text'           => ['label', 'name'],
71
        'textarea'       => ['label', 'name'],
72
        'password'       => ['label', 'name'],
73
        'date'           => ['label', 'name'],
74
        'email'          => ['label', 'name'],
75
        'file'           => ['label', 'name'],
76
        'inputGroup'     => ['label', 'name'],
77
        'radio'          => ['label', 'name'],
78
        'inlineRadio'    => ['label', 'name'],
79
        'checkbox'       => ['label', 'name'],
80
        'inlineCheckbox' => ['label', 'name'],
81
        'select'         => ['label', 'name', 'options'],
82
        'button'         => ['label', 'name', 'type'],
83
        'submit'         => ['value', 'type'],
84
        'hidden'         => ['name'],
85
        'label'          => ['label'],
86
        'open'           => [],
87
        'openHorizontal' => ['columnSizes'],
88
        'bind'           => ['model'],
89
        'close'          => [],
90
    ];
91
92
    /**
93
     * Array holding the methods to call during element behavior processing.
94
     *
95
     * @var array
96
     */
97
    private $elementBehaviors = [
98
        'text'           => ['cloneElement', 'translatableIndicator'],
99
        'textarea'       => ['cloneElement', 'translatableIndicator'],
100
        'password'       => ['cloneElement', 'translatableIndicator'],
101
        'date'           => ['cloneElement', 'translatableIndicator'],
102
        'email'          => ['cloneElement', 'translatableIndicator'],
103
        'file'           => ['cloneElement', 'translatableIndicator'],
104
        'inputGroup'     => ['cloneElement', 'translatableIndicator'],
105
        'radio'          => ['cloneElement', 'translatableIndicator'],
106
        'inlineRadio'    => ['cloneElement', 'translatableIndicator'],
107
        'checkbox'       => ['cloneElement', 'translatableIndicator'],
108
        'inlineCheckbox' => ['cloneElement', 'translatableIndicator'],
109
        'select'         => ['cloneElement', 'translatableIndicator'],
110
        'button'         => ['cloneElement'],
111
        'submit'         => ['cloneElement'],
112
        'hidden'         => ['cloneElement'],
113
        'label'          => [],
114
        'open'           => [],
115
        'openHorizontal' => [],
116
        'close'          => [],
117
    ];
118
119
    /**
120
     * Form constructor.
121
     *
122
     * @param \AdamWathan\BootForms\BootForm $form
123
     */
124 12
    public function __construct(BootForm $form)
125
    {
126 12
        $this->form = $form;
127 12
        $this->config = config('translatable-bootforms');
128 12
    }
129
130
    /**
131
     * Magic __call method.
132
     *
133
     * @param string $method
134
     * @param array  $parameters
135
     * @return \Propaganistas\LaravelTranslatableBootForms\TranslatableBootForm
136
     */
137 11
    public function __call($method, $parameters)
138
    {
139
        // New translatable form element.
140 11
        if (is_null($this->element())) {
141 11
            $this->element($method);
142 11
            $this->arguments($this->mapArguments($parameters));
143 11
        } // Calling methods on the translatable form element.
144
        else {
145 2
            $this->addMethod($method, $parameters);
146
        }
147
148
        // Execute bind or close immediately.
149 11
        if (in_array($method, ['bind', 'close'])) {
150 4
            return $this->render();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->render(); (string) is incompatible with the return type documented by Propaganistas\LaravelTra...latableBootForm::__call of type Propaganistas\LaravelTra...ms\TranslatableBootForm.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
151
        }
152
153 9
        return $this;
154
    }
155
156
    /**
157
     * Magic __toString method.
158
     *
159
     * @return string
160
     */
161 1
    public function __toString()
162
    {
163 1
        return $this->render();
164
    }
165
166
    /**
167
     * Resets the properties.
168
     *
169
     * @return $this
170
     */
171 11
    protected function reset()
172
    {
173 11
        $this->element = null;
174 11
        $this->arguments = [];
175 11
        $this->methods = [];
176 11
        $this->cloneElement = false;
177 11
        $this->translatableIndicator = false;
178
179 11
        return $this;
180
    }
181
182
    /**
183
     * Get or set the available locales.
184
     *
185
     * @param array|null $locales
186
     * @return array
187
     */
188 12
    public function locales(array $locales = null)
189
    {
190 12
        return is_null($locales)
191 12
            ? $this->locales
192 12
            : ($this->locales = $locales);
193
    }
194
195
    /**
196
     * Get or set the current element.
197
     *
198
     * @param string|null $element
199
     * @return string
200
     */
201 11
    protected function element($element = null)
202
    {
203 11
        return is_null($element)
204 11
            ? $this->element
205 11
            : ($this->element = $element);
206
    }
207
208
    /**
209
     * Get or set the arguments.
210
     *
211
     * @param array|null $arguments
212
     * @return array
213
     */
214 11
    protected function arguments(array $arguments = null)
215
    {
216 11
        return is_null($arguments)
217 11
            ? $this->arguments
218 11
            : ($this->arguments = $arguments);
219
    }
220
221
    /**
222
     * Get or set the methods.
223
     *
224
     * @param array|null $methods
225
     * @return array
226
     */
227 10
    protected function methods(array $methods = null)
228
    {
229 10
        return is_null($methods)
230 10
            ? $this->methods
231 10
            : ($this->methods = $methods);
232
    }
233
234
    /**
235
     * Get or set the current element.
236
     *
237
     * @param bool|null $clone
238
     * @return bool
239
     */
240 11
    protected function cloneElement($clone = null)
241
    {
242 11
        return is_null($clone)
243 11
            ? $this->cloneElement
244 11
            : ($this->cloneElement = (bool) $clone);
245
    }
246
247
    /**
248
     * Get or set the translatable indicator boolean.
249
     *
250
     * @param bool|null $add
251
     * @return bool
252
     */
253 5
    protected function translatableIndicator($add = null)
254
    {
255 5
        return is_null($add)
256 5
            ? $this->translatableIndicator
257 5
            : ($this->translatableIndicator = (bool) $add);
258
    }
259
260
    /**
261
     * Overwrites an argument.
262
     *
263
     * @param string       $argument
264
     * @param string|array $value
265
     */
266 5
    protected function overwriteArgument($argument, $value)
267
    {
268 5
        $arguments = $this->arguments();
269
270 5
        $arguments[$argument] = $value;
271
272 5
        $this->arguments($arguments);
273 5
    }
274
275
    /**
276
     * Adds a method.
277
     *
278
     * @param string       $name
279
     * @param string|array $parameters
280
     */
281 5
    protected function addMethod($name, $parameters)
282
    {
283 5
        $methods = $this->methods();
284
285 5
        $parameters = is_array($parameters) ? $parameters : [$parameters];
286
287 5
        $methods[] = compact('name', 'parameters');
288
289 5
        $this->methods($methods);
290 5
    }
291
292
    /**
293
     * Renders the current translatable form element.
294
     *
295
     * @return string
296
     */
297 11
    public function render()
298
    {
299 11
        $this->applyElementBehavior();
300
301 11
        $elements = [];
302
303 11
        if ($this->cloneElement()) {
304 5
            $originalArguments = $this->arguments();
305 5
            $originalMethods = $this->methods();
306
307 5
            foreach ($this->locales() as $locale) {
308 5
                $this->arguments($originalArguments);
309 5
                $this->methods($originalMethods);
310 5
                $this->overwriteArgument('name', $locale . '[' . $originalArguments['name'] . ']');
311 5
                if ($this->translatableIndicator()) {
312 5
                    $this->setTranslatableLabelIndicator($locale);
313 5
                }
314 5
                if (!empty($this->config['form-group-class'])) {
315 5
                    $this->addMethod('addGroupClass', str_replace('%locale', $locale, 'form-group-translation'));
316 5
                }
317 5
                if (!empty($this->config['input-locale-attribute'])) {
318 5
                    $this->addMethod('attribute', [$this->config['input-locale-attribute'], $locale]);
319 5
                }
320 5
                $elements[] = $this->createInput($locale);
321 5
            }
322 5
        } else {
323 11
            $elements[] = $this->createInput();
324
        }
325
326 11
        $this->reset();
327
328 11
        return implode('', $elements);
329
    }
330
331
    /**
332
     * Creates an input element using the supplied arguments and methods.
333
     *
334
     * @param string|null $currentLocale
335
     * @return mixed
336
     */
337 11
    protected function createInput($currentLocale = null)
338
    {
339
        // Create element using arguments.
340 11
        $element = $this->form->{$this->element()}(...array_values($this->arguments()));
341
342
        // Elements such as 'bind' do not return renderable stuff and do not accept methods.
343 11
        if ($element) {
344
            // Apply requested methods.
345 10
            foreach ($this->methods() as $method) {
346 5
                $methodName = $method['name'];
347 5
                $methodParameters = $method['parameters'];
348
349
                // Check if method is locale-specific.
350 5
                if (ends_with($methodName, 'ForLocale')) {
351 1
                    $methodName = strstr($methodName, 'ForLocale', true);
352 1
                    $locales = array_shift($methodParameters);
353 1
                    $locales = is_array($locales) ? $locales : [$locales];
354 1
                    if (!is_null($currentLocale) && !in_array($currentLocale, $locales)) {
355
                        // Method should not be applied for this locale.
356 1
                        continue;
357
                    }
358 1
                }
359
360
                // Call method.
361 5
                if (!empty($methodParameters)) {
362 5
                    $element->{$methodName}(...$methodParameters);
363 5
                } else {
364 2
                    $element->{$methodName}();
365
                }
366
367 10
            }
368 10
        }
369
370 11
        return $element;
371
    }
372
373
    /**
374
     * Add specific element behavior to the current translatable form element.
375
     */
376 11
    protected function applyElementBehavior()
377
    {
378 11
        $behaviors = isset($this->elementBehaviors[$this->element()]) ? $this->elementBehaviors[$this->element()] : [];
379
380 11
        foreach ($behaviors as $behavior) {
381 5
            $this->{$behavior}(true);
382 11
        }
383 11
    }
384
385
    /**
386
     * Maps the form element arguments to their name.
387
     *
388
     * @param array $arguments
389
     * @return array
390
     */
391 11
    protected function mapArguments(array $arguments)
392
    {
393 11
        $keys = isset($this->mappableArguments[$this->element()]) ? $this->mappableArguments[$this->element()] : [];
394
395 11
        return array_combine(array_slice($keys, 0, count($arguments)), $arguments);
396
    }
397
398
    /**
399
     * Add a locale indicator to the label.
400
     *
401
     * @param string $locale
402
     */
403 5
    protected function setTranslatableLabelIndicator($locale)
404
    {
405 5
        $localizedLabel = str_replace('%label', $this->arguments()['label'], $this->config['label-locale-indicator']);
406 5
        $this->overwriteArgument('label', str_replace('%locale', $locale, $localizedLabel));
407 5
    }
408
409
}