Completed
Push — master ( 16e1af...f295bb )
by Propa
02:42
created

TranslatableBootForm::createInput()   D

Complexity

Conditions 9
Paths 2

Size

Total Lines 42
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 24
cts 24
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 21
nc 2
nop 1
crap 9
1
<?php namespace Propaganistas\LaravelTranslatableBootForms;
2
3
use Closure;
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', 'value'],
71
        'textarea'       => ['label', 'name'],
72
        'password'       => ['label', 'name'],
73
        'date'           => ['label', 'name', 'value'],
74
        'email'          => ['label', 'name', 'value'],
75
        'file'           => ['label', 'name', 'value'],
76
        'inputGroup'     => ['label', 'name', 'value'],
77
        'radio'          => ['label', 'name', 'value'],
78
        'inlineRadio'    => ['label', 'name', 'value'],
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 object $form
123
     */
124 45
    public function __construct($form)
125
    {
126 45
        $this->form = $form;
127 45
        $this->config = config('translatable-bootforms');
128 45
    }
129
130
    /**
131
     * Magic __call method.
132
     *
133
     * @param string $method
134
     * @param array  $parameters
135
     * @return \Propaganistas\LaravelTranslatableBootForms\TranslatableBootForm
136
     */
137 42
    public function __call($method, $parameters)
138
    {
139
        // New translatable form element.
140 42
        if (is_null($this->element())) {
141 42
            $this->element($method);
142 42
            $this->arguments($this->mapArguments($parameters));
143 28
        } // Calling methods on the translatable form element.
144
        else {
145 12
            $this->addMethod($method, $parameters);
146
        }
147
148
        // Execute bind or close immediately.
149 42
        if (in_array($method, ['bind', 'close'])) {
150 15
            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 36
        return $this;
154
    }
155
156
    /**
157
     * Magic __toString method.
158
     *
159
     * @return string
160
     */
161 3
    public function __toString()
162
    {
163 3
        return $this->render();
164
    }
165
166
    /**
167
     * Resets the properties.
168
     *
169
     * @return $this
170
     */
171 42
    protected function reset()
172
    {
173 42
        $this->element = null;
174 42
        $this->arguments = [];
175 42
        $this->methods = [];
176 42
        $this->cloneElement = false;
177 42
        $this->translatableIndicator = false;
178
179 42
        return $this;
180
    }
181
182
    /**
183
     * Get or set the available locales.
184
     *
185
     * @param array|null $locales
186
     * @return array
187
     */
188 45
    public function locales(array $locales = null)
189
    {
190 45
        return is_null($locales)
191 38
            ? $this->locales
192 45
            : ($this->locales = $locales);
193
    }
194
195
    /**
196
     * Get or set the current element.
197
     *
198
     * @param string|null $element
199
     * @return string
200
     */
201 42
    protected function element($element = null)
202
    {
203 42
        return is_null($element)
204 42
            ? $this->element
205 42
            : ($this->element = $element);
206
    }
207
208
    /**
209
     * Get or set the arguments.
210
     *
211
     * @param array|null $arguments
212
     * @return array
213
     */
214 42
    protected function arguments(array $arguments = null)
215
    {
216 42
        return is_null($arguments)
217 42
            ? $this->arguments
218 42
            : ($this->arguments = $arguments);
219
    }
220
221
    /**
222
     * Get or set the methods.
223
     *
224
     * @param array|null $methods
225
     * @return array
226
     */
227 39
    protected function methods(array $methods = null)
228
    {
229 39
        return is_null($methods)
230 39
            ? $this->methods
231 39
            : ($this->methods = $methods);
232
    }
233
234
    /**
235
     * Get or set the current element.
236
     *
237
     * @param bool|null $clone
238
     * @return bool
239
     */
240 42
    protected function cloneElement($clone = null)
241
    {
242 42
        return is_null($clone)
243 42
            ? $this->cloneElement
244 42
            : ($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 24
    protected function translatableIndicator($add = null)
254
    {
255 24
        return is_null($add)
256 24
            ? $this->translatableIndicator
257 24
            : ($this->translatableIndicator = (bool) $add);
258
    }
259
260
    /**
261
     * Overwrites an argument.
262
     *
263
     * @param string       $argument
264
     * @param string|array $value
265
     */
266 24
    protected function overwriteArgument($argument, $value)
267
    {
268 24
        $arguments = $this->arguments();
269
270 24
        $arguments[$argument] = $value;
271
272 24
        $this->arguments($arguments);
273 24
    }
274
275
    /**
276
     * Adds a method.
277
     *
278
     * @param string       $name
279
     * @param string|array $parameters
280
     */
281 24
    protected function addMethod($name, $parameters)
282
    {
283 24
        $methods = $this->methods();
284
        
285 24
        $parameters = is_array($parameters) ? $parameters : [$parameters];
286
287 24
        $methods[] = compact('name', 'parameters');
288
289 24
        $this->methods($methods);
290 24
    }
291
292
    /**
293
     * Renders the current translatable form element.
294
     *
295
     * @return string
296
     */
297 42
    public function render()
298
    {
299 42
        $this->applyElementBehavior();
300
301 42
        $elements = [];
302
303 42
        if ($this->cloneElement()) {
304 24
            $originalArguments = $this->arguments();
305 24
            $originalMethods = $this->methods();
306
307 24
            $locales = $this->locales();
308
            // Check if a custom locale set is requested.
309 24
            if ($count = func_num_args()) {
310 3
                $args = ($count == 1 ? head(func_get_args()) : func_get_args());
311 3
                $locales = array_intersect($locales, (array) $args);
312 2
            }
313
314 24
            foreach ($locales as $locale) {
315 24
                $this->arguments($originalArguments);
316 24
                $this->methods($originalMethods);
317
                
318 24
                $name = str_contains($originalArguments['name'], '%locale') ? $originalArguments['name'] : '%locale[' . $originalArguments['name'] . ']';
319 24
                $this->overwriteArgument('name', str_replace('%locale', $locale, $name));
320
                
321 24
                if ($this->translatableIndicator()) {
322 24
                    $this->setTranslatableLabelIndicator($locale);
323 16
                }
324 24
                if (!empty($this->config['form-group-class'])) {
325 24
                    $this->addMethod('addGroupClass', str_replace('%locale', $locale, $this->config['form-group-class']));
326 16
                }
327 24
                if (!empty($this->config['input-locale-attribute'])) {
328 24
                    $this->addMethod('attribute', [$this->config['input-locale-attribute'], $locale]);
329 16
                }
330 24
                $elements[] = $this->createInput($locale);
331 16
            }
332 16
        } else {
333 42
            $elements[] = $this->createInput();
334
        }
335
336 42
        $this->reset();
337
338 42
        return implode('', $elements);
339
    }
340
341
    /**
342
     * Shortcut method for locale-specific rendering.
343
     *
344
     * @return string
345
     */
346 3
    public function renderLocale()
347
    {
348 3
        return call_user_func_array([$this, 'render'], func_get_args());
349
    }
350
351
    /**
352
     * Creates an input element using the supplied arguments and methods.
353
     *
354
     * @param string|null $currentLocale
355
     * @return mixed
356
     */
357 42
    protected function createInput($currentLocale = null)
358
    {
359
        // Create element using arguments.
360 42
        $element = call_user_func_array([$this->form, $this->element()], array_values($this->arguments()));
361
362
        // Elements such as 'bind' do not return renderable stuff and do not accept methods.
363 42
        if ($element) {
364
            // Apply requested methods.
365 39
            foreach ($this->methods() as $method) {
366 24
                $methodName = $method['name'];
367 24
                $methodParameters = $method['parameters'];
368
369
                // Check if method is locale-specific.
370 24
                if (ends_with($methodName, 'ForLocale')) {
371 3
                    $methodName = strstr($methodName, 'ForLocale', true);
372 3
                    $locales = array_shift($methodParameters);
373 3
                    $locales = is_array($locales) ? $locales : [$locales];
374 3
                    if (!is_null($currentLocale) && !in_array($currentLocale, $locales)) {
375
                        // Method should not be applied for this locale.
376 3
                        continue;
377
                    }
378 2
                }
379
380
                // Call method.
381 24
                if (!empty($methodParameters)) {
382
                    // Convert closures to values.
383 24
                    $methodParameters = array_map(function($parameter) use ($currentLocale) {
384 24
                        return ($parameter instanceof Closure) ? $parameter($currentLocale) : $parameter;
385 24
                    }, $methodParameters);
386
                    
387 24
                    $methodParameters = $this->replacePlaceholdersRecursively($methodParameters, $currentLocale);
388
                    
389 24
                    call_user_func_array([$element, $methodName], $methodParameters);
390 16
                } else {
391 14
                    $element->{$methodName}();
392
                }
393
394 26
            }
395 26
        }
396
397 42
        return $element;
398
    }
399
400
    /**
401
     * Replaces %name recursively with the proper input name.
402
     *
403
     * @param mixed $parameter
404
     * @param string $currentLocale
405
     * @return mixed
406
     */
407 24
    protected function replacePlaceholdersRecursively($parameter, $currentLocale)
408
    {
409 24
        if (is_array($parameter)) {
410 24
            foreach ($parameter as $param) {
411 24
                $this->replacePlaceholdersRecursively($param, $currentLocale);
412 16
            }
413 16
        }
414
        
415 24
        $replacements = ['locale' => $currentLocale];
416
        
417 24
        if ($name = array_get($this->arguments(), 'name')) {
418 24
            array_set($replacements, 'name', $name);
419 16
        }
420
421 24
        return str_replace(array_keys($replacements), array_values($replacements), $parameter);
422
    }
423
424
    /**
425
     * Add specific element behavior to the current translatable form element.
426
     */
427 42
    protected function applyElementBehavior()
428
    {
429 42
        $behaviors = isset($this->elementBehaviors[$this->element()]) ? $this->elementBehaviors[$this->element()] : [];
430
431 42
        foreach ($behaviors as $behavior) {
432 24
            $this->{$behavior}(true);
433 28
        }
434 42
    }
435
436
    /**
437
     * Maps the form element arguments to their name.
438
     *
439
     * @param array $arguments
440
     * @return array
441
     */
442 42
    protected function mapArguments(array $arguments)
443
    {
444 42
        $keys = isset($this->mappableArguments[$this->element()]) ? $this->mappableArguments[$this->element()] : [];
445
446 42
        return array_combine(array_slice($keys, 0, count($arguments)), $arguments);
447
    }
448
449
    /**
450
     * Add a locale indicator to the label.
451
     *
452
     * @param string $locale
453
     */
454 24
    protected function setTranslatableLabelIndicator($locale)
455
    {
456 24
        $localizedLabel = str_replace('%label', $this->arguments()['label'], $this->config['label-locale-indicator']);
457 24
        $this->overwriteArgument('label', str_replace('%locale', $locale, $localizedLabel));
458 24
    }
459
460
}
461