Completed
Push — master ( 750af5...855623 )
by Propa
04:20
created

TranslatableBootForm   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 449
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 0
dl 0
loc 449
ccs 128
cts 128
cp 1
rs 8.5454
c 0
b 0
f 0

19 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 element() 0 6 2
A arguments() 0 6 2
A translatableIndicator() 0 6 2
A overwriteArgument() 0 8 1
A addMethod() 0 10 2
A renderLocale() 0 4 1
A applyElementBehavior() 0 8 3
A setTranslatableLabelIndicator() 0 5 1
A locales() 0 6 2
A methods() 0 6 2
A cloneElement() 0 6 2
D render() 0 43 9
C createInput() 0 35 8
A replacePlaceholdersRecursively() 0 16 4
A mapArguments() 0 6 2

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
class TranslatableBootForm
4
{
5
6
    /**
7
     * BootForm implementation.
8
     *
9
     * @var \AdamWathan\BootForms\BootForm
10
     */
11
    protected $form;
12
13
    /**
14
     * Array holding config values.
15
     *
16
     * @var array
17
     */
18
    protected $config;
19
20
    /**
21
     * Array of locale keys.
22
     *
23
     * @var array
24
     */
25
    protected $locales;
26
27
    /**
28
     * The current element type this class is working on.
29
     *
30
     * @var string
31
     */
32
    protected $element;
33
34
    /**
35
     * The array of arguments to pass in when creating the element.
36
     *
37
     * @var array
38
     */
39
    protected $arguments = [];
40
41
    /**
42
     * A keyed array of method => arguments to call on the created input.
43
     *
44
     * @var array
45
     */
46
    protected $methods = [];
47
48
    /**
49
     * Boolean indicating if the element should be cloned with corresponding translation name attributes.
50
     *
51
     * @var bool
52
     */
53
    protected $cloneElement = false;
54
55
    /**
56
     * Boolean indicating if the element should have an indication that is it a translation.
57
     *
58
     * @var bool
59
     */
60
    protected $translatableIndicator = false;
61
62
    /**
63
     * Array holding the mappable element arguments.
64
     *
65
     * @var array
66
     */
67
    private $mappableArguments = [
68
        'text'           => ['label', 'name', 'value'],
69
        'textarea'       => ['label', 'name'],
70
        'password'       => ['label', 'name'],
71
        'date'           => ['label', 'name', 'value'],
72
        'email'          => ['label', 'name', 'value'],
73
        'file'           => ['label', 'name', 'value'],
74
        'inputGroup'     => ['label', 'name', 'value'],
75
        'radio'          => ['label', 'name', 'value'],
76
        'inlineRadio'    => ['label', 'name', 'value'],
77
        'checkbox'       => ['label', 'name'],
78
        'inlineCheckbox' => ['label', 'name'],
79
        'select'         => ['label', 'name', 'options'],
80
        'button'         => ['label', 'name', 'type'],
81
        'submit'         => ['value', 'type'],
82
        'hidden'         => ['name'],
83
        'label'          => ['label'],
84
        'open'           => [],
85
        'openHorizontal' => ['columnSizes'],
86
        'bind'           => ['model'],
87
        'close'          => [],
88
    ];
89
90
    /**
91
     * Array holding the methods to call during element behavior processing.
92
     *
93
     * @var array
94
     */
95
    private $elementBehaviors = [
96
        'text'           => ['cloneElement', 'translatableIndicator'],
97
        'textarea'       => ['cloneElement', 'translatableIndicator'],
98
        'password'       => ['cloneElement', 'translatableIndicator'],
99
        'date'           => ['cloneElement', 'translatableIndicator'],
100
        'email'          => ['cloneElement', 'translatableIndicator'],
101
        'file'           => ['cloneElement', 'translatableIndicator'],
102
        'inputGroup'     => ['cloneElement', 'translatableIndicator'],
103
        'radio'          => ['cloneElement', 'translatableIndicator'],
104
        'inlineRadio'    => ['cloneElement', 'translatableIndicator'],
105
        'checkbox'       => ['cloneElement', 'translatableIndicator'],
106
        'inlineCheckbox' => ['cloneElement', 'translatableIndicator'],
107
        'select'         => ['cloneElement', 'translatableIndicator'],
108
        'button'         => ['cloneElement'],
109
        'submit'         => ['cloneElement'],
110
        'hidden'         => ['cloneElement'],
111
        'label'          => [],
112
        'open'           => [],
113
        'openHorizontal' => [],
114
        'close'          => [],
115
    ];
116
117
    /**
118
     * Form constructor.
119
     *
120
     * @param object $form
121
     */
122 42
    public function __construct($form)
123
    {
124 42
        $this->form = $form;
125 28
        $this->config = config('translatable-bootforms');
126 42
    }
127
128
    /**
129
     * Magic __call method.
130
     *
131
     * @param string $method
132
     * @param array  $parameters
133
     * @return \Propaganistas\LaravelTranslatableBootForms\TranslatableBootForm
134
     */
135 30
    public function __call($method, $parameters)
136
    {
137
        // New translatable form element.
138 26
        if (is_null($this->element())) {
139 26
            $this->element($method);
140 26
            $this->arguments($this->mapArguments($parameters));
141
        } // Calling methods on the translatable form element.
142
        else {
143 6
            $this->addMethod($method, $parameters);
144 4
        }
145
146
        // Execute bind or close immediately.
147 26
        if (in_array($method, ['bind', 'close'])) {
148 8
            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...
149
        }
150
151 26
        return $this;
152
    }
153
154
    /**
155
     * Magic __toString method.
156
     *
157
     * @return string
158
     */
159 2
    public function __toString()
160
    {
161 2
        return $this->render();
162
    }
163
164
    /**
165
     * Resets the properties.
166
     *
167
     * @return $this
168
     */
169 30
    protected function reset()
170
    {
171 30
        $this->element = null;
172 30
        $this->arguments = [];
173 30
        $this->methods = [];
174 30
        $this->cloneElement = false;
175 30
        $this->translatableIndicator = false;
176
177 30
        return $this;
178
    }
179
180
    /**
181
     * Get or set the available locales.
182
     *
183
     * @param array|null $locales
184
     * @return array
185
     */
186 35
    public function locales(array $locales = null)
187
    {
188 28
        return is_null($locales)
189 20
            ? $this->locales
190 35
            : ($this->locales = $locales);
191 7
    }
192
193
    /**
194
     * Get or set the current element.
195
     *
196
     * @param string|null $element
197
     * @return string
198
     */
199 26
    protected function element($element = null)
200
    {
201 26
        return is_null($element)
202 26
            ? $this->element
203 26
            : ($this->element = $element);
204
    }
205
206
    /**
207
     * Get or set the arguments.
208
     *
209
     * @param array|null $arguments
210
     * @return array
211
     */
212 30
    protected function arguments(array $arguments = null)
213
    {
214 26
        return is_null($arguments)
215 30
            ? $this->arguments
216 26
            : ($this->arguments = $arguments);
217
    }
218
219
    /**
220
     * Get or set the methods.
221
     *
222
     * @param array|null $methods
223
     * @return array
224
     */
225 29
    protected function methods(array $methods = null)
226
    {
227 24
        return is_null($methods)
228 29
            ? $this->methods
229 29
            : ($this->methods = $methods);
230 5
    }
231
232
    /**
233
     * Get or set the current element.
234
     *
235
     * @param bool|null $clone
236
     * @return bool
237
     */
238 30
    protected function cloneElement($clone = null)
239
    {
240 26
        return is_null($clone)
241 30
            ? $this->cloneElement
242 30
            : ($this->cloneElement = (bool) $clone);
243 4
    }
244
245
    /**
246
     * Get or set the translatable indicator boolean.
247
     *
248
     * @param bool|null $add
249
     * @return bool
250
     */
251 14
    protected function translatableIndicator($add = null)
252
    {
253 14
        return is_null($add)
254 14
            ? $this->translatableIndicator
255 14
            : ($this->translatableIndicator = (bool) $add);
256
    }
257
258
    /**
259
     * Overwrites an argument.
260
     *
261
     * @param string       $argument
262
     * @param string|array $value
263
     */
264 14
    protected function overwriteArgument($argument, $value)
265
    {
266 14
        $arguments = $this->arguments();
267
268 14
        $arguments[$argument] = $value;
269
270 14
        $this->arguments($arguments);
271 14
    }
272
273
    /**
274
     * Adds a method.
275
     *
276
     * @param string       $name
277
     * @param string|array $parameters
278
     */
279 14
    protected function addMethod($name, $parameters)
280
    {
281 14
        $methods = $this->methods();
282
283 14
        $parameters = is_array($parameters) ? $parameters : [$parameters];
284
285 14
        $methods[] = compact('name', 'parameters');
286
287 14
        $this->methods($methods);
288 14
    }
289
290
    /**
291
     * Renders the current translatable form element.
292
     *
293
     * @return string
294
     */
295 32
    public function render()
296
    {
297 26
        $this->applyElementBehavior();
298
299 30
        $elements = [];
300
301 26
        if ($this->cloneElement()) {
302 14
            $originalArguments = $this->arguments();
303 14
            $originalMethods = $this->methods();
304
305 14
            $locales = $this->locales();
306
            // Check if a custom locale set is requested.
307 20
            if ($count = func_num_args()) {
308 2
                $args = ($count == 1 ? head(func_get_args()) : func_get_args());
309 2
                $locales = array_intersect($locales, (array) $args);
310
            }
311
312 14
            foreach ($locales as $locale) {
313 14
                $this->arguments($originalArguments);
314 14
                $this->methods($originalMethods);
315
                
316 14
                $name = str_contains($originalArguments['name'], '%locale') ? $originalArguments['name'] : '%locale[' . $originalArguments['name'] . ']';
317 14
                $this->overwriteArgument('name', str_replace('%locale', $locale, $name));
318
                
319 14
                if ($this->translatableIndicator()) {
320 14
                    $this->setTranslatableLabelIndicator($locale);
321
                }
322 14
                if (!empty($this->config['form-group-class'])) {
323 14
                    $this->addMethod('addGroupClass', str_replace('%locale', $locale, $this->config['form-group-class']));
324
                }
325 14
                if (!empty($this->config['input-locale-attribute'])) {
326 14
                    $this->addMethod('attribute', [$this->config['input-locale-attribute'], $locale]);
327
                }
328 14
                $elements[] = $this->createInput($locale);
329
            }
330
        } else {
331 26
            $elements[] = $this->createInput();
332 6
        }
333
334 26
        $this->reset();
335
336 26
        return implode('', $elements);
337
    }
338
339
    /**
340
     * Shortcut method for locale-specific rendering.
341
     *
342
     * @return string
343
     */
344 2
    public function renderLocale()
345
    {
346 2
        return call_user_func_array([$this, 'render'], func_get_args());
347
    }
348
349
    /**
350
     * Creates an input element using the supplied arguments and methods.
351
     *
352
     * @param string|null $currentLocale
353
     * @return mixed
354
     */
355 36
    protected function createInput($currentLocale = null)
356
    {
357
        // Create element using arguments.
358 26
        $element = call_user_func_array([$this->form, $this->element()], array_values($this->arguments()));
359
360
        // Elements such as 'bind' do not return renderable stuff and do not accept methods.
361 30
        if ($element) {
362
            // Apply requested methods.
363 29
            foreach ($this->methods() as $method) {
364 14
                $methodName = $method['name'];
365 14
                $methodParameters = $method['parameters'];
366
367
                // Check if method is locale-specific.
368 14
                if (ends_with($methodName, 'ForLocale')) {
369 2
                    $methodName = strstr($methodName, 'ForLocale', true);
370 2
                    $locales = array_shift($methodParameters);
371 2
                    $locales = is_array($locales) ? $locales : [$locales];
372 2
                    if (!is_null($currentLocale) && !in_array($currentLocale, $locales)) {
373
                        // Method should not be applied for this locale.
374 2
                        continue;
375
                    }
376
                }
377
378
                // Call method.
379 14
                if (!empty($methodParameters)) {
380 14
                    call_user_func_array([$element, $methodName], $this->replacePlaceholdersRecursively($methodParameters, $currentLocale));
381
                } else {
382 14
                    $element->{$methodName}();
383
                }
384
385 5
            }
386
        }
387
388 30
        return $element;
389 10
    }
390
391
    /**
392
     * Replaces %name recursively with the proper input name.
393
     *
394
     * @param mixed $parameter
395
     * @param string $currentLocale
396
     * @return mixed
397
     */
398 14
    protected function replacePlaceholdersRecursively($parameter, $currentLocale)
399
    {
400 14
        if (is_array($parameter)) {
401 14
            foreach ($parameter as $param) {
402 14
                $this->replacePlaceholdersRecursively($param, $currentLocale);
403
            }
404
        }
405
        
406 14
        $replacements = ['locale' => $currentLocale];
407
        
408 14
        if ($name = array_get($this->arguments(), 'name')) {
409 14
            array_set($replacements, 'name', $name);
410
        }
411
412 14
        return str_replace(array_keys($replacements), array_values($replacements), $parameter);
413
    }
414
415
    /**
416
     * Add specific element behavior to the current translatable form element.
417
     */
418 30
    protected function applyElementBehavior()
419
    {
420 26
        $behaviors = isset($this->elementBehaviors[$this->element()]) ? $this->elementBehaviors[$this->element()] : [];
421
422 26
        foreach ($behaviors as $behavior) {
423 14
            $this->{$behavior}(true);
424 4
        }
425 30
    }
426
427
    /**
428
     * Maps the form element arguments to their name.
429
     *
430
     * @param array $arguments
431
     * @return array
432
     */
433 30
    protected function mapArguments(array $arguments)
434
    {
435 26
        $keys = isset($this->mappableArguments[$this->element()]) ? $this->mappableArguments[$this->element()] : [];
436
437 26
        return array_combine(array_slice($keys, 0, count($arguments)), $arguments);
438 4
    }
439
440
    /**
441
     * Add a locale indicator to the label.
442
     *
443
     * @param string $locale
444
     */
445 14
    protected function setTranslatableLabelIndicator($locale)
446
    {
447 14
        $localizedLabel = str_replace('%label', $this->arguments()['label'], $this->config['label-locale-indicator']);
448 14
        $this->overwriteArgument('label', str_replace('%locale', $locale, $localizedLabel));
449 14
    }
450
451
}
452