Completed
Push — master ( ebb14c...4ef20d )
by Propa
06:22
created

replaceInputNameRecursively()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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