Completed
Pull Request — master (#272)
by Rudie
52:13
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 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
crap 2
1
<?php  namespace Kris\LaravelFormBuilder;
2
3
use Illuminate\Contracts\View\Factory as View;
4
use Illuminate\Database\Eloquent\Model;
5
use Illuminate\Support\Arr;
6
use Illuminate\Support\Collection;
7
use Kris\LaravelFormBuilder\Fields\FormField;
8
use Kris\LaravelFormBuilder\Form;
9
use Symfony\Component\Translation\TranslatorInterface;
10
11
class FormHelper
12
{
13
14
    /**
15
     * @var View
16
     */
17
    protected $view;
18
19
    /**
20
     * @var TranslatorInterface
21
     */
22
    protected $translator;
23
24
    /**
25
     * @var array
26
     */
27
    protected $config;
28
29
    /**
30
     * @var FormBuilder
31
     */
32
    protected $formBuilder;
33
34
    /**
35
     * @var array
36
     */
37
    protected static $reservedFieldNames = [
38
        'save'
39
    ];
40
41
    /**
42
     * All available field types
43
     *
44
     * @var array
45
     */
46
    protected static $availableFieldTypes = [
47
        'text'           => 'InputType',
48
        'email'          => 'InputType',
49
        'url'            => 'InputType',
50
        'tel'            => 'InputType',
51
        'search'         => 'InputType',
52
        'password'       => 'InputType',
53
        'hidden'         => 'InputType',
54
        'number'         => 'InputType',
55
        'date'           => 'InputType',
56
        'file'           => 'InputType',
57
        'image'          => 'InputType',
58
        'color'          => 'InputType',
59
        'datetime-local' => 'InputType',
60
        'month'          => 'InputType',
61
        'range'          => 'InputType',
62
        'time'           => 'InputType',
63
        'week'           => 'InputType',
64
        'select'         => 'SelectType',
65
        'textarea'       => 'TextareaType',
66
        'button'         => 'ButtonType',
67
        'submit'         => 'ButtonType',
68
        'reset'          => 'ButtonType',
69
        'radio'          => 'CheckableType',
70
        'checkbox'       => 'CheckableType',
71
        'choice'         => 'ChoiceType',
72
        'form'           => 'ChildFormType',
73
        'entity'         => 'EntityType',
74
        'collection'     => 'CollectionType',
75
        'repeated'       => 'RepeatedType',
76
        'static'         => 'StaticType'
77
    ];
78
79
    /**
80
     * Custom types
81
     *
82
     * @var array
83
     */
84
    private $customTypes = [];
85
86
    /**
87
     * @param View    $view
88
     * @param TranslatorInterface $translator
89
     * @param array   $config
90
     */
91 105
    public function __construct(View $view, TranslatorInterface $translator, array $config = [])
92
    {
93 105
        $this->view = $view;
94 105
        $this->translator = $translator;
95 105
        $this->config = $config;
96 105
        $this->loadCustomTypes();
97 105
    }
98
99
    /**
100
     * @param string $key
101
     * @param string $default
102
     * @return mixed
103
     */
104 105
    public function getConfig($key, $default = null)
105
    {
106 105
        return array_get($this->config, $key, $default);
107
    }
108
109
    /**
110
     * @return View
111
     */
112 35
    public function getView()
113
    {
114 35
        return $this->view;
115
    }
116
117
    /**
118
     * Merge options array
119
     *
120
     * @param array $first
121
     * @param array $second
122
     * @return array
123
     */
124 105
    public function mergeOptions(array $first, array $second)
125
    {
126 105
        return array_replace_recursive($first, $second);
127
    }
128
129
    /**
130
     * Get proper class for field type
131
     *
132
     * @param $type
133
     * @return string
134
     */
135 68
    public function getFieldType($type)
136
    {
137 68
        $types = array_keys(static::$availableFieldTypes);
138
139 68
        if (!$type || trim($type) == '') {
140 1
            throw new \InvalidArgumentException('Field type must be provided.');
141
        }
142
143 67
        if (array_key_exists($type, $this->customTypes)) {
144 2
            return $this->customTypes[$type];
145
        }
146
147 65
        if (!in_array($type, $types)) {
148 2
            throw new \InvalidArgumentException(
149
                sprintf(
150 2
                    'Unsupported field type [%s]. Available types are: %s',
151
                    $type,
152 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
153
                )
154
            );
155
        }
156
157 63
        $namespace = __NAMESPACE__.'\\Fields\\';
158
159 63
        return $namespace . static::$availableFieldTypes[$type];
160
    }
161
162
    /**
163
     * Convert array of attributes to html attributes
164
     *
165
     * @param $options
166
     * @return string
167
     */
168 84
    public function prepareAttributes($options)
169
    {
170 84
        if (!$options) {
171 5
            return null;
172
        }
173
174 84
        $attributes = [];
175
176 84
        foreach ($options as $name => $option) {
177 84
            if ($option !== null) {
178 84
                $name = is_numeric($name) ? $option : $name;
179 84
                $attributes[] = $name.'="'.$option.'" ';
180
            }
181
        }
182
183 84
        return join('', $attributes);
184
    }
185
186
    /**
187
     * Add custom field
188
     *
189
     * @param $name
190
     * @param $class
191
     */
192 3
    public function addCustomField($name, $class)
193
    {
194 3
        if (!array_key_exists($name, $this->customTypes)) {
195 3
            return $this->customTypes[$name] = $class;
196
        }
197
198 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
199
    }
200
201
    /**
202
     * Load custom field types from config file
203
     */
204 105
    private function loadCustomTypes()
205
    {
206 105
        $customFields = (array) $this->getConfig('custom_fields');
207
208 105
        if (!empty($customFields)) {
209 1
            foreach ($customFields as $fieldName => $fieldClass) {
210 1
                $this->addCustomField($fieldName, $fieldClass);
211
            }
212
        }
213 105
    }
214
215 5
    public function convertModelToArray($model)
216
    {
217 5
        if (!$model) {
218 1
            return null;
219
        }
220
221 5
        if ($model instanceof Model) {
222 1
            return $model->toArray();
223
        }
224
225 5
        if ($model instanceof Collection) {
226 2
            return $model->all();
227
        }
228
229 5
        return $model;
230
    }
231
232
    /**
233
     * Format the label to the proper format
234
     *
235
     * @param $name
236
     * @return string
237
     */
238 82
    public function formatLabel($name)
239
    {
240 82
        if (!$name) {
241 1
            return null;
242
        }
243
244 82
        if ($this->translator->has($name)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Translation\TranslatorInterface as the method has() does only exist in the following implementations of said interface: Illuminate\Translation\Translator.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
245 1
            $translatedName = $this->translator->get($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Translation\TranslatorInterface as the method get() does only exist in the following implementations of said interface: Illuminate\Translation\Translator.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
246
247 1
            if (is_string($translatedName)) {
248 1
                return $translatedName;
249
            }
250
        }
251
252 81
        return ucfirst(str_replace('_', ' ', $name));
253
    }
254
255
    /**
256
     * @param FormField[] $fields
257
     * @return array
258
     */
259 7
    public function mergeFieldsRules($fields)
260
    {
261 7
        $rules = [];
262 7
        $attributes = [];
263 7
        $messages = [];
264
265 7
        foreach ($fields as $field) {
266 7
            if ($fieldRules = $field->getValidationRules()) {
267 7
                $rules = array_merge($rules, $fieldRules['rules']);
268 7
                $attributes = array_merge($attributes, $fieldRules['attributes']);
269 7
                $messages = array_merge($messages, $fieldRules['error_messages']);
270
            }
271
        }
272
273
        return [
274 7
            'rules' => $rules,
275 7
            'attributes' => $attributes,
276 7
            'error_messages' => $messages
277
        ];
278
    }
279
280
    /**
281
     * @return array
282
     */
283 2
    public function mergeAttributes($fields)
284
    {
285 2
        $attributes = [];
286 2
        foreach ($fields as $field) {
287 2
            $attributes = array_merge($attributes, $field->getAllAttributes());
288
        }
289
290 2
        return $attributes;
291
    }
292
293
    /**
294
     * Alter a form's values recursively according to its fields
295
     *
296
     * @return void
297
     */
298 2
    public function alterFieldValues(Form $form, array &$values)
299
    {
300
        // Alter the form itself
301 2
        $form->alterFieldValues($values);
302
303
        // Alter the form's child forms recursively
304 2
        foreach ($form->getFields() as $name => $field) {
305 2
            if (method_exists($field, 'alterFieldValues')) {
306 2
                $fullName = $this->transformToDotSyntax($name);
307
308 2
                $subValues = Arr::get($values, $fullName);
309 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...
310 2
                Arr::set($values, $fullName, $subValues);
311
            }
312
        }
313 2
    }
314
315
    /**
316
     * @param string $string
317
     * @return string
318
     */
319 83
    public function transformToDotSyntax($string)
320
    {
321 83
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
322
    }
323
324
    /**
325
     * @param string $string
326
     * @return string
327
     */
328 6
    public function transformToBracketSyntax($string)
329
    {
330 6
        $name = explode('.', $string);
331 6
        if (count($name) == 1) {
332
            return $name[0];
333
        }
334
335 6
        $first = array_shift($name);
336 6
        return $first . '[' . implode('][', $name) . ']';
337
    }
338
339
    /**
340
     * @return TranslatorInterface
341
     */
342 3
    public function getTranslator()
343
    {
344 3
        return $this->translator;
345
    }
346
347
    /**
348
     * Check if field name is valid and not reserved
349
     *
350
     * @throws \InvalidArgumentException
351
     * @param string $name
352
     * @param string $className
353
     */
354 54
    public function checkFieldName($name, $className)
355
    {
356 54
        if (!$name || trim($name) == '') {
357 2
            throw new \InvalidArgumentException(
358 2
                "Please provide valid field name for class [{$className}]"
359
            );
360
        }
361
362 52
        if (in_array($name, static::$reservedFieldNames)) {
363 2
            throw new \InvalidArgumentException(
364 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
365 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
366
            );
367
        }
368
369 50
        return true;
370
    }
371
}
372