Completed
Push — master ( 322315...b6a4c4 )
by Kristijan
06:52 queued 10s
created

FormHelper::mergeFieldsRules()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 2
rs 9.9332
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\Events\AfterCollectingFieldRules;
12
use Kris\LaravelFormBuilder\Fields\FormField;
13
use Kris\LaravelFormBuilder\Form;
14
use Kris\LaravelFormBuilder\RulesParser;
15
16
class FormHelper
17
{
18
19
    /**
20
     * @var View
21
     */
22
    protected $view;
23
24
    /**
25
     * @var TranslatorInterface
26
     */
27
    protected $translator;
28
29
    /**
30
     * @var array
31
     */
32
    protected $config;
33
34
    /**
35
     * @var FormBuilder
36
     */
37
    protected $formBuilder;
38
39
    /**
40
     * @var array
41
     */
42
    protected static $reservedFieldNames = [
43
        'save'
44
    ];
45
46
    /**
47
     * All available field types
48
     *
49
     * @var array
50
     */
51
    protected static $availableFieldTypes = [
52
        'text'           => 'InputType',
53
        'email'          => 'InputType',
54
        'url'            => 'InputType',
55
        'tel'            => 'InputType',
56
        'search'         => 'InputType',
57
        'password'       => 'InputType',
58
        'hidden'         => 'InputType',
59
        'number'         => 'InputType',
60
        'date'           => 'InputType',
61
        'file'           => 'InputType',
62
        'image'          => 'InputType',
63
        'color'          => 'InputType',
64
        'datetime-local' => 'InputType',
65
        'month'          => 'InputType',
66
        'range'          => 'InputType',
67
        'time'           => 'InputType',
68
        'week'           => 'InputType',
69
        'select'         => 'SelectType',
70
        'textarea'       => 'TextareaType',
71
        'button'         => 'ButtonType',
72
        'buttongroup'    => 'ButtonGroupType',
73
        'submit'         => 'ButtonType',
74
        'reset'          => 'ButtonType',
75
        'radio'          => 'CheckableType',
76
        'checkbox'       => 'CheckableType',
77
        'choice'         => 'ChoiceType',
78
        'form'           => 'ChildFormType',
79
        'entity'         => 'EntityType',
80
        'collection'     => 'CollectionType',
81
        'repeated'       => 'RepeatedType',
82
        'static'         => 'StaticType'
83
    ];
84
85
    /**
86
     * Custom types
87
     *
88
     * @var array
89
     */
90
    private $customTypes = [];
91
92
    /**
93
     * @param View    $view
94
     * @param Translator $translator
95
     * @param array   $config
96
     */
97 131
    public function __construct(View $view, Translator $translator, array $config = [])
98
    {
99 131
        $this->view = $view;
100 131
        $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...
101 131
        $this->config = $config;
102 131
        $this->loadCustomTypes();
103 131
    }
104
105
    /**
106
     * @param string $key
107
     * @param string $default
108
     * @param array $customConfig
109
     * @return mixed
110
     */
111 131
    public function getConfig($key = null, $default = null, $customConfig = [])
112
    {
113 131
        $config = array_replace_recursive($this->config, $customConfig);
114
115 131
        if ($key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to true; 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...
116 131
            return Arr::get($config, $key, $default);
117
        }
118
119
        return $config;
120
    }
121
122
    /**
123
     * @return View
124
     */
125 38
    public function getView()
126
    {
127 38
        return $this->view;
128
    }
129
130
    /**
131
     * Merge options array.
132
     *
133
     * @param array $targetOptions
134
     * @param array $sourceOptions
135
     * @return array
136
     */
137 131
    public function mergeOptions(array $targetOptions, array $sourceOptions)
138
    {
139 131
        return array_replace_recursive($targetOptions, $sourceOptions);
140
    }
141
142
143
    /**
144
     * Get proper class for field type.
145
     *
146
     * @param $type
147
     * @return string
148
     */
149 88
    public function getFieldType($type)
150
    {
151 88
        $types = array_keys(static::$availableFieldTypes);
152
153 88
        if (!$type || trim($type) == '') {
154 1
            throw new \InvalidArgumentException('Field type must be provided.');
155
        }
156
157 87
        if ($this->hasCustomField($type)) {
158 3
            return $this->customTypes[$type];
159
        }
160
161 84
        if (!in_array($type, $types)) {
162 2
            throw new \InvalidArgumentException(
163 2
                sprintf(
164 2
                    'Unsupported field type [%s]. Available types are: %s',
165 2
                    $type,
166 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
167
                )
168
            );
169
        }
170
171 82
        $namespace = __NAMESPACE__.'\\Fields\\';
172
173 82
        return $namespace . static::$availableFieldTypes[$type];
174
    }
175
176
    /**
177
     * Convert array of attributes to html attributes.
178
     *
179
     * @param $options
180
     * @return string
181
     */
182 104
    public function prepareAttributes($options)
183
    {
184 104
        if (!$options) {
185 13
            return null;
186
        }
187
188 104
        $attributes = [];
189
190 104
        foreach ($options as $name => $option) {
191 104
            if ($option !== null) {
192 104
                $name = is_numeric($name) ? $option : $name;
193 104
                $attributes[] = $name.'="'.$option.'" ';
194
            }
195
        }
196
197 104
        return join('', $attributes);
198
    }
199
200
    /**
201
     * Add custom field.
202
     *
203
     * @param $name
204
     * @param $class
205
     */
206 4
    public function addCustomField($name, $class)
207
    {
208 4
        if (!$this->hasCustomField($name)) {
209 4
            return $this->customTypes[$name] = $class;
210
        }
211
212 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
213
    }
214
215
    /**
216
     * Load custom field types from config file.
217
     */
218 131
    private function loadCustomTypes()
219
    {
220 131
        $customFields = (array) $this->getConfig('custom_fields');
221
222 131
        if (!empty($customFields)) {
223 1
            foreach ($customFields as $fieldName => $fieldClass) {
224 1
                $this->addCustomField($fieldName, $fieldClass);
225
            }
226
        }
227 131
    }
228
229
    /**
230
     * Check if custom field with provided name exists
231
     * @param string $name
232
     * @return boolean
233
     */
234 88
    public function hasCustomField($name)
235
    {
236 88
        return array_key_exists($name, $this->customTypes);
237
    }
238
239
    /**
240
     * @param object $model
241
     * @return object|null
242
     */
243 4
    public function convertModelToArray($model)
244
    {
245 4
        if (!$model) {
246 1
            return null;
247
        }
248
249 4
        if ($model instanceof Model) {
250 2
            return $model->toArray();
251
        }
252
253 3
        if ($model instanceof Collection) {
254 2
            return $model->all();
255
        }
256
257 2
        return $model;
258
    }
259
260
    /**
261
     * Format the label to the proper format.
262
     *
263
     * @param $name
264
     * @return string
265
     */
266 102
    public function formatLabel($name)
267
    {
268 102
        if (!$name) {
269 1
            return null;
270
        }
271
272 102
        if ($this->translator->has($name)) {
273 2
            $translatedName = $this->translator->get($name);
274
275 2
            if (is_string($translatedName)) {
276 2
                return $translatedName;
277
            }
278
        }
279
280 100
        return ucfirst(str_replace('_', ' ', $name));
281
    }
282
283
    /**
284
     * @param FormField $field
285
     * @return RulesParser
286
     */
287 103
    public function createRulesParser(FormField $field)
288
    {
289 103
        return new RulesParser($field);
290
    }
291
292
    /**
293
     * @param FormField $field
294
     * @return array
295
     */
296 10
    public function getFieldValidationRules(FormField $field)
297
    {
298 10
        $fieldRules = $field->getValidationRules();
299
300 10
        if (is_array($fieldRules)) {
301
          $fieldRules = Rules::fromArray($fieldRules)->setFieldName($field->getNameKey());
302
        }
303
304 10
        $formBuilder = $field->getParent()->getFormBuilder();
305 10
        $formBuilder->fireEvent(new AfterCollectingFieldRules($field, $fieldRules));
306
307 10
        return $fieldRules;
308
    }
309
310
    /**
311
     * @param FormField[] $fields
312
     * @return array
313
     */
314 10
    public function mergeFieldsRules($fields)
315
    {
316 10
        $rules = new Rules([]);
317
318 10
        foreach ($fields as $field) {
319 10
            $rules->append($this->getFieldValidationRules($field));
0 ignored issues
show
Documentation introduced by
$this->getFieldValidationRules($field) is of type object<Kris\LaravelFormBuilder\Rules>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
320
        }
321
322 10
        return $rules;
323
    }
324
325
    /**
326
     * @param array $fields
327
     * @return array
328
     */
329 3
    public function mergeAttributes(array $fields)
330
    {
331 3
        $attributes = [];
332 3
        foreach ($fields as $field) {
333 3
            $attributes = array_merge($attributes, $field->getAllAttributes());
334
        }
335
336 3
        return $attributes;
337
    }
338
339
    /**
340
     * Alter a form's values recursively according to its fields.
341
     *
342
     * @param  Form  $form
343
     * @param  array $values
344
     * @return void
345
     */
346 3
    public function alterFieldValues(Form $form, array &$values)
347
    {
348
        // Alter the form's child forms recursively
349 3
        foreach ($form->getFields() as $name => $field) {
350 3
            if (method_exists($field, 'alterFieldValues')) {
351 2
                $fullName = $this->transformToDotSyntax($name);
352
353 2
                $subValues = (array) Arr::get($values, $fullName);
354 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...
355 3
                Arr::set($values, $fullName, $subValues);
356
            }
357
        }
358
359
        // Alter the form itself
360 3
        $form->alterFieldValues($values);
361 3
    }
362
363
    /**
364
     * Alter a form's validity recursively, and add messages with nested form prefix.
365
     *
366
     * @return void
367
     */
368 10
    public function alterValid(Form $form, Form $mainForm, &$isValid)
369
    {
370
        // Alter the form itself
371 10
        $messages = $form->alterValid($mainForm, $isValid);
372
373
        // Add messages to the existing Bag
374 10
        if ($messages) {
375 1
            $messageBag = $mainForm->getValidator()->getMessageBag();
376 1
            $this->appendMessagesWithPrefix($messageBag, $form->getName(), $messages);
377
        }
378
379
        // Alter the form's child forms recursively
380 10
        foreach ($form->getFields() as $name => $field) {
381 10
            if (method_exists($field, 'alterValid')) {
382 10
                $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...
383
            }
384
        }
385 10
    }
386
387
    /**
388
     * Add unprefixed messages with prefix to a MessageBag.
389
     *
390
     * @return void
391
     */
392 1
    public function appendMessagesWithPrefix(MessageBag $messageBag, $prefix, array $keyedMessages)
393
    {
394 1
        foreach ($keyedMessages as $key => $messages) {
395 1
            if ($prefix) {
396 1
                $key = $this->transformToDotSyntax($prefix . '[' . $key . ']');
397
            }
398
399 1
            foreach ((array) $messages as $message) {
400 1
                $messageBag->add($key, $message);
401
            }
402
        }
403 1
    }
404
405
    /**
406
     * @param string $string
407
     * @return string
408
     */
409 103
    public function transformToDotSyntax($string)
410
    {
411 103
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
412
    }
413
414
    /**
415
     * @param string $string
416
     * @return string
417
     */
418 7
    public function transformToBracketSyntax($string)
419
    {
420 7
        $name = explode('.', $string);
421 7
        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...
422 1
            return $name[0];
423
        }
424
425 6
        $first = array_shift($name);
426 6
        return $first . '[' . implode('][', $name) . ']';
427
    }
428
429
    /**
430
     * @return TranslatorInterface
431
     */
432 3
    public function getTranslator()
433
    {
434 3
        return $this->translator;
435
    }
436
437
    /**
438
     * Check if field name is valid and not reserved.
439
     *
440
     * @throws \InvalidArgumentException
441
     * @param string $name
442
     * @param string $className
443
     */
444 71
    public function checkFieldName($name, $className)
445
    {
446 71
        if (!$name || trim($name) == '') {
447 2
            throw new \InvalidArgumentException(
448 2
                "Please provide valid field name for class [{$className}]"
449
            );
450
        }
451
452 69
        if (in_array($name, static::$reservedFieldNames)) {
453 2
            throw new \InvalidArgumentException(
454 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
455 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
456
            );
457
        }
458
459 67
        return true;
460
    }
461
}
462