Completed
Push — master ( 03abae...843902 )
by Propa
03:39
created

TranslatableBootForm::render()   C

Complexity

Conditions 8
Paths 4

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 8

Importance

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