Completed
Pull Request — master (#406)
by
unknown
03:47
created

FormHelper::setConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Kris\LaravelFormBuilder;
4
5
use Illuminate\Contracts\Support\MessageBag;
6
use Illuminate\Contracts\View\Factory as View;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Collection;
10
use Illuminate\Translation\Translator;
11
use Kris\LaravelFormBuilder\Fields\FormField;
12
use Kris\LaravelFormBuilder\Form;
13
use Kris\LaravelFormBuilder\RulesParser;
14
15
class FormHelper
16
{
17
18
    /**
19
     * @var View
20
     */
21
    protected $view;
22
23
    /**
24
     * @var TranslatorInterface
25
     */
26
    protected $translator;
27
28
    /**
29
     * @var array
30
     */
31
    protected $config;
32
33
    /**
34
     * @var FormBuilder
35
     */
36
    protected $formBuilder;
37
38
    /**
39
     * @var array
40
     */
41
    protected static $reservedFieldNames = [
42
        'save'
43
    ];
44
45
    /**
46
     * All available field types
47
     *
48
     * @var array
49
     */
50
    protected static $availableFieldTypes = [
51
        'text'           => 'InputType',
52
        'email'          => 'InputType',
53
        'url'            => 'InputType',
54
        'tel'            => 'InputType',
55
        'search'         => 'InputType',
56
        'password'       => 'InputType',
57
        'hidden'         => 'InputType',
58
        'number'         => 'InputType',
59
        'date'           => 'InputType',
60
        'file'           => 'InputType',
61
        'image'          => 'InputType',
62
        'color'          => 'InputType',
63
        'datetime-local' => 'InputType',
64
        'month'          => 'InputType',
65
        'range'          => 'InputType',
66
        'time'           => 'InputType',
67
        'week'           => 'InputType',
68
        'select'         => 'SelectType',
69
        'textarea'       => 'TextareaType',
70
        'button'         => 'ButtonType',
71
        'buttongroup'    => 'ButtonGroupType',
72
        'submit'         => 'ButtonType',
73
        'reset'          => 'ButtonType',
74
        'radio'          => 'CheckableType',
75
        'checkbox'       => 'CheckableType',
76
        'choice'         => 'ChoiceType',
77
        'form'           => 'ChildFormType',
78
        'entity'         => 'EntityType',
79
        'collection'     => 'CollectionType',
80
        'repeated'       => 'RepeatedType',
81
        'static'         => 'StaticType'
82
    ];
83
84
    /**
85
     * Custom types
86
     *
87
     * @var array
88
     */
89
    private $customTypes = [];
90
91
    /**
92
     * @param View    $view
93
     * @param Translator $translator
94
     * @param array   $config
95
     */
96 130
    public function __construct(View $view, Translator $translator, array $config = [])
97
    {
98 130
        $this->view = $view;
99 130
        $this->translator = $translator;
0 ignored issues
show
Documentation Bug introduced by
It seems like $translator of type object<Illuminate\Translation\Translator> is incompatible with the declared type object<Kris\LaravelFormB...er\TranslatorInterface> of property $translator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
100 130
        $this->config = $config;
101 130
        $this->loadCustomTypes();
102 130
    }
103
104
    /**
105
     * @param string $key
106
     * @param string $default
107
     * @return mixed
108
     */
109 130
    public function getConfig($key = null, $default = null)
110
    {
111 130
        $returnValue = null;
0 ignored issues
show
Unused Code introduced by
$returnValue is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
112 130
        if (!$key)
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
113
        {
114 18
            $returnValue = $this->config;
115
        }
116
        else
117
        {
118 130
            $returnValue = array_get($this->config, $key, $default);
119
        }
120
121 130
        return $returnValue;
122
    }
123
124
    /**
125
     * @param array $config
126
     */
127 18
    public function setConfig($config)
128
    {
129 18
        $this->config = $config;
130 18
    }
131
132
    /**
133
     * @return View
134
     */
135 38
    public function getView()
136
    {
137 38
        return $this->view;
138
    }
139
140
    /**
141
     * Merge options array.
142
     *
143
     * @param array $first
144
     * @param array $second
145
     * @return array
146
     */
147 130
    public function mergeOptions(array $first, array $second)
148
    {
149 130
        return array_replace_recursive($first, $second);
150
    }
151
152
    /**
153
     * Get proper class for field type.
154
     *
155
     * @param $type
156
     * @return string
157
     */
158 87
    public function getFieldType($type)
159
    {
160 87
        $types = array_keys(static::$availableFieldTypes);
161
162 87
        if (!$type || trim($type) == '') {
163 1
            throw new \InvalidArgumentException('Field type must be provided.');
164
        }
165
166 86
        if ($this->hasCustomField($type)) {
167 2
            return $this->customTypes[$type];
168
        }
169
170 84
        if (!in_array($type, $types)) {
171 2
            throw new \InvalidArgumentException(
172 2
                sprintf(
173 2
                    'Unsupported field type [%s]. Available types are: %s',
174 2
                    $type,
175 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
176
                )
177
            );
178
        }
179
180 82
        $namespace = __NAMESPACE__.'\\Fields\\';
181
182 82
        return $namespace . static::$availableFieldTypes[$type];
183
    }
184
185
    /**
186
     * Convert array of attributes to html attributes.
187
     *
188
     * @param $options
189
     * @return string
190
     */
191 103
    public function prepareAttributes($options)
192
    {
193 103
        if (!$options) {
194 13
            return null;
195
        }
196
197 103
        $attributes = [];
198
199 103
        foreach ($options as $name => $option) {
200 103
            if ($option !== null) {
201 103
                $name = is_numeric($name) ? $option : $name;
202 103
                $attributes[] = $name.'="'.$option.'" ';
203
            }
204
        }
205
206 103
        return join('', $attributes);
207
    }
208
209
    /**
210
     * Add custom field.
211
     *
212
     * @param $name
213
     * @param $class
214
     */
215 3
    public function addCustomField($name, $class)
216
    {
217 3
        if (!$this->hasCustomField($name)) {
218 3
            return $this->customTypes[$name] = $class;
219
        }
220
221 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
222
    }
223
224
    /**
225
     * Load custom field types from config file.
226
     */
227 130
    private function loadCustomTypes()
228
    {
229 130
        $customFields = (array) $this->getConfig('custom_fields');
230
231 130
        if (!empty($customFields)) {
232 1
            foreach ($customFields as $fieldName => $fieldClass) {
233 1
                $this->addCustomField($fieldName, $fieldClass);
234
            }
235
        }
236 130
    }
237
238
    /**
239
     * Check if custom field with provided name exists
240
     * @param string $name
241
     * @return boolean
242
     */
243 87
    public function hasCustomField($name)
244
    {
245 87
        return array_key_exists($name, $this->customTypes);
246
    }
247
248
    /**
249
     * @param object $model
250
     * @return object|null
251
     */
252 2
    public function convertModelToArray($model)
253
    {
254 2
        if (!$model) {
255 1
            return null;
256
        }
257
258 2
        if ($model instanceof Model) {
259 1
            return $model->toArray();
260
        }
261
262 2
        if ($model instanceof Collection) {
263 2
            return $model->all();
264
        }
265
266 1
        return $model;
267
    }
268
269
    /**
270
     * Format the label to the proper format.
271
     *
272
     * @param $name
273
     * @return string
274
     */
275 101
    public function formatLabel($name)
276
    {
277 101
        if (!$name) {
278 1
            return null;
279
        }
280
281 101
        if ($this->translator->has($name)) {
282 2
            $translatedName = $this->translator->get($name);
283
284 2
            if (is_string($translatedName)) {
285 2
                return $translatedName;
286
            }
287
        }
288
289 99
        return ucfirst(str_replace('_', ' ', $name));
290
    }
291
292
    /**
293
     * @param FormField $field
294
     * @return RulesParser
295
     */
296 102
    public function createRulesParser(FormField $field)
297
    {
298 102
        return new RulesParser($field);
299
    }
300
301
    /**
302
     * @param FormField[] $fields
303
     * @return array
304
     */
305 9
    public function mergeFieldsRules($fields)
306
    {
307 9
        $rules = [];
308 9
        $attributes = [];
309 9
        $messages = [];
310
311 9
        foreach ($fields as $field) {
312 9
            if ($fieldRules = $field->getValidationRules()) {
313 9
                $rules = array_merge($rules, $fieldRules['rules']);
314 9
                $attributes = array_merge($attributes, $fieldRules['attributes']);
315 9
                $messages = array_merge($messages, $fieldRules['error_messages']);
316
            }
317
        }
318
319
        return [
320 9
            'rules' => $rules,
321 9
            'attributes' => $attributes,
322 9
            'error_messages' => $messages
323
        ];
324
    }
325
326
    /**
327
     * @param array $fields
328
     * @return array
329
     */
330 3
    public function mergeAttributes(array $fields)
331
    {
332 3
        $attributes = [];
333 3
        foreach ($fields as $field) {
334 3
            $attributes = array_merge($attributes, $field->getAllAttributes());
335
        }
336
337 3
        return $attributes;
338
    }
339
340
    /**
341
     * Alter a form's values recursively according to its fields.
342
     *
343
     * @param  Form  $form
344
     * @param  array $values
345
     * @return void
346
     */
347 3
    public function alterFieldValues(Form $form, array &$values)
348
    {
349
        // Alter the form itself
350 3
        $form->alterFieldValues($values);
351
352
        // Alter the form's child forms recursively
353 3
        foreach ($form->getFields() as $name => $field) {
354 3
            if (method_exists($field, 'alterFieldValues')) {
355 2
                $fullName = $this->transformToDotSyntax($name);
356
357 2
                $subValues = Arr::get($values, $fullName);
358 2
                $field->alterFieldValues($subValues);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Kris\LaravelFormBuilder\Fields\FormField as the method alterFieldValues() does only exist in the following sub-classes of Kris\LaravelFormBuilder\Fields\FormField: Kris\LaravelFormBuilder\Fields\ChildFormType. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
359 3
                Arr::set($values, $fullName, $subValues);
360
            }
361
        }
362 3
    }
363
364
    /**
365
     * Alter a form's validity recursively, and add messages with nested form prefix.
366
     *
367
     * @return void
368
     */
369 9
    public function alterValid(Form $form, Form $mainForm, &$isValid)
370
    {
371
        // Alter the form itself
372 9
        $messages = $form->alterValid($mainForm, $isValid);
373
374
        // Add messages to the existing Bag
375 9
        if ($messages) {
376 1
            $messageBag = $mainForm->getValidator()->getMessageBag();
377 1
            $this->appendMessagesWithPrefix($messageBag, $form->getName(), $messages);
378
        }
379
380
        // Alter the form's child forms recursively
381 9
        foreach ($form->getFields() as $name => $field) {
382 9
            if (method_exists($field, 'alterValid')) {
383 9
                $field->alterValid($mainForm, $isValid);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Kris\LaravelFormBuilder\Fields\FormField as the method alterValid() does only exist in the following sub-classes of Kris\LaravelFormBuilder\Fields\FormField: Kris\LaravelFormBuilder\Fields\ChildFormType. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
384
            }
385
        }
386 9
    }
387
388
    /**
389
     * Add unprefixed messages with prefix to a MessageBag.
390
     *
391
     * @return void
392
     */
393 1
    public function appendMessagesWithPrefix(MessageBag $messageBag, $prefix, array $keyedMessages)
394
    {
395 1
        foreach ($keyedMessages as $key => $messages) {
396 1
            if ($prefix) {
397 1
                $key = $this->transformToDotSyntax($prefix . '[' . $key . ']');
398
            }
399
400 1
            foreach ((array) $messages as $message) {
401 1
                $messageBag->add($key, $message);
402
            }
403
        }
404 1
    }
405
406
    /**
407
     * @param string $string
408
     * @return string
409
     */
410 102
    public function transformToDotSyntax($string)
411
    {
412 102
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
413
    }
414
415
    /**
416
     * @param string $string
417
     * @return string
418
     */
419 6
    public function transformToBracketSyntax($string)
420
    {
421 6
        $name = explode('.', $string);
422 6
        if ($name && count($name) == 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
423
            return $name[0];
424
        }
425
426 6
        $first = array_shift($name);
427 6
        return $first . '[' . implode('][', $name) . ']';
428
    }
429
430
    /**
431
     * @return TranslatorInterface
432
     */
433 3
    public function getTranslator()
434
    {
435 3
        return $this->translator;
436
    }
437
438
    /**
439
     * Check if field name is valid and not reserved.
440
     *
441
     * @throws \InvalidArgumentException
442
     * @param string $name
443
     * @param string $className
444
     */
445 70
    public function checkFieldName($name, $className)
446
    {
447 70
        if (!$name || trim($name) == '') {
448 2
            throw new \InvalidArgumentException(
449 2
                "Please provide valid field name for class [{$className}]"
450
            );
451
        }
452
453 68
        if (in_array($name, static::$reservedFieldNames)) {
454 2
            throw new \InvalidArgumentException(
455 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
456 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
457
            );
458
        }
459
460 66
        return true;
461
    }
462
}
463