Completed
Pull Request — master (#12)
by Samuel
23:36
created

TranslatableBootForm::cloneElement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2
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
                if (config('app.translator') === 'spatie') {
316 18
                    $this->overwriteArgument('name', $originalArguments['name'] . '[' . $locale . ']');
317 18
                } else {
318 18
                    $this->overwriteArgument('name', $locale . '[' . $originalArguments['name'] . ']');
319 18
                }
320 18
                if ($this->translatableIndicator()) {
321 18
                    $this->setTranslatableLabelIndicator($locale);
322 18
                }
323 18
                if (!empty($this->config['form-group-class'])) {
324 18
                    $this->addMethod('addGroupClass', str_replace('%locale', $locale, 'form-group-translation'));
325 18
                }
326 18
                if (!empty($this->config['input-locale-attribute'])) {
327 18
                    $this->addMethod('attribute', [$this->config['input-locale-attribute'], $locale]);
328 36
                }
329
                $elements[] = $this->createInput($locale);
330
            }
331 36
        } else {
332
            $elements[] = $this->createInput();
333 36
        }
334
335
        $this->reset();
336
337
        return implode('', $elements);
338
    }
339
340
    /**
341 3
     * Shortcut method for locale-specific rendering.
342
     *
343 3
     * @return string
344
     */
345
    public function renderLocale()
346
    {
347
        return call_user_func_array([$this, 'render'], func_get_args());
348
    }
349
350
    /**
351
     * Creates an input element using the supplied arguments and methods.
352 36
     *
353
     * @param string|null $currentLocale
354
     * @return mixed
355 36
     */
356
    protected function createInput($currentLocale = null)
357
    {
358 36
        // Create element using arguments.
359
        $element = call_user_func_array([$this->form, $this->element()], array_values($this->arguments()));
360 33
361 18
        // Elements such as 'bind' do not return renderable stuff and do not accept methods.
362 18
        if ($element) {
363
            // Apply requested methods.
364
            foreach ($this->methods() as $method) {
365 18
                $methodName = $method['name'];
366 3
                $methodParameters = $method['parameters'];
367 3
368 3
                // Check if method is locale-specific.
369 3
                if (ends_with($methodName, 'ForLocale')) {
370
                    $methodName = strstr($methodName, 'ForLocale', true);
371 3
                    $locales = array_shift($methodParameters);
372
                    $locales = is_array($locales) ? $locales : [$locales];
373 3
                    if (!is_null($currentLocale) && !in_array($currentLocale, $locales)) {
374
                        // Method should not be applied for this locale.
375
                        continue;
376 18
                    }
377 18
                }
378 18
379 9
                // Call method.
380
                if (!empty($methodParameters)) {
381
                    call_user_func_array([$element, $methodName], $this->replacePlaceholdersRecursively($methodParameters, $currentLocale));
382 33
                } else {
383 33
                    $element->{$methodName}();
384
                }
385 36
386
            }
387
        }
388
389
        return $element;
390
    }
391
392
    /**
393
     * Replaces %name recursively with the proper input name.
394
     *
395 18
     * @param mixed $parameter
396
     * @param string $currentLocale
397 18
     * @return mixed
398 18
     */
399 18
    protected function replacePlaceholdersRecursively($parameter, $currentLocale)
400 18
    {
401 18
        if (is_array($parameter)) {
402
            foreach ($parameter as $param) {
403 18
                $this->replacePlaceholdersRecursively($param, $currentLocale);
404
            }
405
        }
406
407
        return str_replace(['%name', '%locale'], [$this->arguments()['name'], $currentLocale], $parameter);
408
    }
409 36
410
    /**
411 36
     * Add specific element behavior to the current translatable form element.
412
     */
413 36
    protected function applyElementBehavior()
414 18
    {
415 36
        $behaviors = isset($this->elementBehaviors[$this->element()]) ? $this->elementBehaviors[$this->element()] : [];
416 36
417
        foreach ($behaviors as $behavior) {
418
            $this->{$behavior}(true);
419
        }
420
    }
421
422
    /**
423
     * Maps the form element arguments to their name.
424 36
     *
425
     * @param array $arguments
426 36
     * @return array
427
     */
428 36
    protected function mapArguments(array $arguments)
429
    {
430
        $keys = isset($this->mappableArguments[$this->element()]) ? $this->mappableArguments[$this->element()] : [];
431
432
        return array_combine(array_slice($keys, 0, count($arguments)), $arguments);
433
    }
434
435
    /**
436 18
     * Add a locale indicator to the label.
437
     *
438 18
     * @param string $locale
439 18
     */
440 18
    protected function setTranslatableLabelIndicator($locale)
441
    {
442
        $localizedLabel = str_replace('%label', $this->arguments()['label'], $this->config['label-locale-indicator']);
443
        $this->overwriteArgument('label', str_replace('%locale', $locale, $localizedLabel));
444
    }
445
446
}
447