Completed
Push — master ( 49704d...e9475a )
by Kristijan
12s
created

FormHelper::addCustomField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
ccs 4
cts 4
cp 1
crap 2
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 107
    public function __construct(View $view, Translator $translator, array $config = [])
97
    {
98 107
        $this->view = $view;
99 107
        $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 107
        $this->config = $config;
101 107
        $this->loadCustomTypes();
102 107
    }
103
104
    /**
105
     * @param string $key
106
     * @param string $default
107
     * @return mixed
108
     */
109 107
    public function getConfig($key, $default = null)
110
    {
111 107
        return array_get($this->config, $key, $default);
112
    }
113
114
    /**
115
     * @return View
116
     */
117 36
    public function getView()
118
    {
119 36
        return $this->view;
120
    }
121
122
    /**
123
     * Merge options array.
124
     *
125
     * @param array $first
126
     * @param array $second
127
     * @return array
128
     */
129 107
    public function mergeOptions(array $first, array $second)
130
    {
131 107
        return array_replace_recursive($first, $second);
132
    }
133
134
    /**
135
     * Get proper class for field type.
136
     *
137
     * @param $type
138
     * @return string
139
     */
140 70
    public function getFieldType($type)
141
    {
142 70
        $types = array_keys(static::$availableFieldTypes);
143
144 70
        if (!$type || trim($type) == '') {
145 1
            throw new \InvalidArgumentException('Field type must be provided.');
146
        }
147
148 69
        if (array_key_exists($type, $this->customTypes)) {
149 2
            return $this->customTypes[$type];
150
        }
151
152 67
        if (!in_array($type, $types)) {
153 2
            throw new \InvalidArgumentException(
154
                sprintf(
155 2
                    'Unsupported field type [%s]. Available types are: %s',
156
                    $type,
157 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
158
                )
159
            );
160
        }
161
162 65
        $namespace = __NAMESPACE__.'\\Fields\\';
163
164 65
        return $namespace . static::$availableFieldTypes[$type];
165
    }
166
167
    /**
168
     * Convert array of attributes to html attributes.
169
     *
170
     * @param $options
171
     * @return string
172
     */
173 86
    public function prepareAttributes($options)
174
    {
175 86
        if (!$options) {
176 6
            return null;
177
        }
178
179 86
        $attributes = [];
180
181 86
        foreach ($options as $name => $option) {
182 86
            if ($option !== null) {
183 86
                $name = is_numeric($name) ? $option : $name;
184 86
                $attributes[] = $name.'="'.$option.'" ';
185
            }
186
        }
187
188 86
        return join('', $attributes);
189
    }
190
191
    /**
192
     * Add custom field.
193
     *
194
     * @param $name
195
     * @param $class
196
     */
197 3
    public function addCustomField($name, $class)
198
    {
199 3
        if (!array_key_exists($name, $this->customTypes)) {
200 3
            return $this->customTypes[$name] = $class;
201
        }
202
203 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
204
    }
205
206
    /**
207
     * Load custom field types from config file.
208
     */
209 107
    private function loadCustomTypes()
210
    {
211 107
        $customFields = (array) $this->getConfig('custom_fields');
212
213 107
        if (!empty($customFields)) {
214 1
            foreach ($customFields as $fieldName => $fieldClass) {
215 1
                $this->addCustomField($fieldName, $fieldClass);
216
            }
217
        }
218 107
    }
219
220
    /**
221
     * @param object $model
222
     * @return object|null
223
     */
224 2
    public function convertModelToArray($model)
225
    {
226 2
        if (!$model) {
227 1
            return null;
228
        }
229
230 2
        if ($model instanceof Model) {
231 1
            return $model->toArray();
232
        }
233
234 2
        if ($model instanceof Collection) {
235 2
            return $model->all();
236
        }
237
238 1
        return $model;
239
    }
240
241
    /**
242
     * Format the label to the proper format.
243
     *
244
     * @param $name
245
     * @return string
246
     */
247 84
    public function formatLabel($name)
248
    {
249 84
        if (!$name) {
250 1
            return null;
251
        }
252
253 84
        if ($this->translator->has($name)) {
254 1
            $translatedName = $this->translator->get($name);
255
256 1
            if (is_string($translatedName)) {
257 1
                return $translatedName;
258
            }
259
        }
260
261 83
        return ucfirst(str_replace('_', ' ', $name));
262
    }
263
264
    /**
265
     * @param FormField $field
266
     * @return RulesParser
267
     */
268 85
    public function createRulesParser(FormField $field)
269
    {
270 85
        return new RulesParser($field);
271
    }
272
273
    /**
274
     * @param FormField[] $fields
275
     * @return array
276
     */
277 8
    public function mergeFieldsRules($fields)
278
    {
279 8
        $rules = [];
280 8
        $attributes = [];
281 8
        $messages = [];
282
283 8
        foreach ($fields as $field) {
284 8
            if ($fieldRules = $field->getValidationRules()) {
285 8
                $rules = array_merge($rules, $fieldRules['rules']);
286 8
                $attributes = array_merge($attributes, $fieldRules['attributes']);
287 8
                $messages = array_merge($messages, $fieldRules['error_messages']);
288
            }
289
        }
290
291
        return [
292 8
            'rules' => $rules,
293 8
            'attributes' => $attributes,
294 8
            'error_messages' => $messages
295
        ];
296
    }
297
298
    /**
299
     * @param array $fields
300
     * @return array
301
     */
302 3
    public function mergeAttributes(array $fields)
303
    {
304 3
        $attributes = [];
305 3
        foreach ($fields as $field) {
306 3
            $attributes = array_merge($attributes, $field->getAllAttributes());
307
        }
308
309 3
        return $attributes;
310
    }
311
312
    /**
313
     * Alter a form's values recursively according to its fields.
314
     *
315
     * @param  Form  $form
316
     * @param  array $values
317
     * @return void
318
     */
319 3
    public function alterFieldValues(Form $form, array &$values)
320
    {
321
        // Alter the form itself
322 3
        $form->alterFieldValues($values);
323
324
        // Alter the form's child forms recursively
325 3
        foreach ($form->getFields() as $name => $field) {
326 3
            if (method_exists($field, 'alterFieldValues')) {
327 2
                $fullName = $this->transformToDotSyntax($name);
328
329 2
                $subValues = Arr::get($values, $fullName);
330 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...
331 3
                Arr::set($values, $fullName, $subValues);
332
            }
333
        }
334 3
    }
335
336
    /**
337
     * Alter a form's validity recursively, and add messages with nested form prefix.
338
     *
339
     * @return void
340
     */
341 8
    public function alterValid(Form $form, Form $mainForm, &$isValid)
342
    {
343
        // Alter the form itself
344 8
        $messages = $form->alterValid($mainForm, $isValid);
345
346
        // Add messages to the existing Bag
347 8
        if ($messages) {
348 1
            $messageBag = $mainForm->getValidator()->getMessageBag();
349 1
            $this->appendMessagesWithPrefix($messageBag, $form->getName(), $messages);
350
        }
351
352
        // Alter the form's child forms recursively
353 8
        foreach ($form->getFields() as $name => $field) {
354 8
            if (method_exists($field, 'alterValid')) {
355 8
                $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...
356
            }
357
        }
358 8
    }
359
360
    /**
361
     * Add unprefixed messages with prefix to a MessageBag.
362
     *
363
     * @return void
364
     */
365 1
    public function appendMessagesWithPrefix(MessageBag $messageBag, $prefix, array $keyedMessages)
366
    {
367 1
        foreach ($keyedMessages as $key => $messages) {
368 1
            if ($prefix) {
369 1
                $key = $this->transformToDotSyntax($prefix . '[' . $key . ']');
370
            }
371
372 1
            foreach ((array) $messages as $message) {
373 1
                $messageBag->add($key, $message);
374
            }
375
        }
376 1
    }
377
378
    /**
379
     * @param string $string
380
     * @return string
381
     */
382 85
    public function transformToDotSyntax($string)
383
    {
384 85
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
385
    }
386
387
    /**
388
     * @param string $string
389
     * @return string
390
     */
391 6
    public function transformToBracketSyntax($string)
392
    {
393 6
        $name = explode('.', $string);
394 6
        if (count($name) == 1) {
395
            return $name[0];
396
        }
397
398 6
        $first = array_shift($name);
399 6
        return $first . '[' . implode('][', $name) . ']';
400
    }
401
402
    /**
403
     * @return TranslatorInterface
404
     */
405 3
    public function getTranslator()
406
    {
407 3
        return $this->translator;
408
    }
409
410
    /**
411
     * Check if field name is valid and not reserved.
412
     *
413
     * @throws \InvalidArgumentException
414
     * @param string $name
415
     * @param string $className
416
     */
417 55
    public function checkFieldName($name, $className)
418
    {
419 55
        if (!$name || trim($name) == '') {
420 2
            throw new \InvalidArgumentException(
421 2
                "Please provide valid field name for class [{$className}]"
422
            );
423
        }
424
425 53
        if (in_array($name, static::$reservedFieldNames)) {
426 2
            throw new \InvalidArgumentException(
427 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
428 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
429
            );
430
        }
431
432 51
        return true;
433
    }
434
}
435