Completed
Push — master ( f3b5cb...518d84 )
by Kristijan
05:42
created

FormHelper::formatLabel()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 2
Bugs 0 Features 2
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 9.2
c 2
b 0
f 2
1
<?php  namespace Kris\LaravelFormBuilder;
2
3
use Illuminate\Support\Collection;
4
use Symfony\Component\Translation\TranslatorInterface;
5
use Illuminate\Database\Eloquent\Model;
6
use Kris\LaravelFormBuilder\Fields\FormField;
7
use Illuminate\Contracts\View\Factory as View;
8
9
class FormHelper
10
{
11
12
    /**
13
     * @var View
14
     */
15
    protected $view;
16
17
    /**
18
     * @var TranslatorInterface
19
     */
20
    protected $translator;
21
22
    /**
23
     * @var array
24
     */
25
    protected $config;
26
27
    /**
28
     * @var FormBuilder
29
     */
30
    protected $formBuilder;
31
32
    /**
33
     * @var array
34
     */
35
    protected static $reservedFieldNames = [
36
        'save'
37
    ];
38
39
    /**
40
     * All available field types
41
     *
42
     * @var array
43
     */
44
    protected static $availableFieldTypes = [
45
        'text'           => 'InputType',
46
        'email'          => 'InputType',
47
        'url'            => 'InputType',
48
        'tel'            => 'InputType',
49
        'search'         => 'InputType',
50
        'password'       => 'InputType',
51
        'hidden'         => 'InputType',
52
        'number'         => 'InputType',
53
        'date'           => 'InputType',
54
        'file'           => 'InputType',
55
        'image'          => 'InputType',
56
        'color'          => 'InputType',
57
        'datetime-local' => 'InputType',
58
        'month'          => 'InputType',
59
        'range'          => 'InputType',
60
        'time'           => 'InputType',
61
        'week'           => 'InputType',
62
        'select'         => 'SelectType',
63
        'textarea'       => 'TextareaType',
64
        'button'         => 'ButtonType',
65
        'submit'         => 'ButtonType',
66
        'reset'          => 'ButtonType',
67
        'radio'          => 'CheckableType',
68
        'checkbox'       => 'CheckableType',
69
        'choice'         => 'ChoiceType',
70
        'form'           => 'ChildFormType',
71
        'entity'         => 'EntityType',
72
        'collection'     => 'CollectionType',
73
        'repeated'       => 'RepeatedType',
74
        'static'         => 'StaticType'
75
    ];
76
77
    /**
78
     * Custom types
79
     *
80
     * @var array
81
     */
82
    private $customTypes = [];
83
84
    /**
85
     * @param View    $view
86
     * @param TranslatorInterface $translator
87
     * @param array   $config
88
     */
89 103
    public function __construct(View $view, TranslatorInterface $translator, array $config = [])
90
    {
91 103
        $this->view = $view;
92 103
        $this->translator = $translator;
93 103
        $this->config = $config;
94 103
        $this->loadCustomTypes();
95 103
    }
96
97
    /**
98
     * @param string $key
99
     * @param string $default
100
     * @return mixed
101
     */
102 103
    public function getConfig($key, $default = null)
103
    {
104 103
        return array_get($this->config, $key, $default);
105
    }
106
107
    /**
108
     * @return View
109
     */
110 34
    public function getView()
111
    {
112 34
        return $this->view;
113
    }
114
115
    /**
116
     * Merge options array
117
     *
118
     * @param array $first
119
     * @param array $second
120
     * @return array
121
     */
122
    public function mergeOptions(array $first, array $second)
123
    {
124 103
        $merge_options = function($first, $second, $concat_classes = FALSE) use(&$merge_options) {
125 103
            $arr = array();
126 103
            foreach (array_unique(array_merge(array_keys($first), array_keys($second))) as $key) {
127 103
                $new_value = NULL;
128
129
                // Element exists in both arrays.
130 103
                if (array_key_exists($key, $first) && array_key_exists($key, $second)) {
131
                    // Recurse.
132 66
                    if (is_array($first[$key]) && is_array($second[$key])) {
133 39
                        $new_value = $merge_options($first[$key], $second[$key], in_array($key, array('wrapper', 'label_attr', 'attr')));
134 39
                    }
135
                    // Merge classes.
136 60
                    elseif ($concat_classes && $key == 'class') {
137 19
                        if (!str_contains($first[$key], $second[$key]) && !str_contains($second[$key], $first[$key])) {
138 11
                            $new_value = trim($first[$key] . ' ' . $second[$key]);
139 11
                        }
140 19
                    }
141 66
                }
142
143
                // Take (in this order) new value, second value, first value.
144 103
                $arr[$key] = $new_value ?: (array_key_exists($key, $second) ? $second[$key] : $first[$key]);
145 103
            }
146 103
            return $arr;
147 103
        };
148
149 103
        return $merge_options($first, $second);
150
    }
151
152
    /**
153
     * Get proper class for field type
154
     *
155
     * @param $type
156
     * @return string
157
     */
158 66
    public function getFieldType($type)
159
    {
160 66
        $types = array_keys(static::$availableFieldTypes);
161
162 66
        if (!$type || trim($type) == '') {
163 1
            throw new \InvalidArgumentException('Field type must be provided.');
164
        }
165
166 65
        if (array_key_exists($type, $this->customTypes)) {
167 2
            return $this->customTypes[$type];
168
        }
169
170 63
        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 2
                )
177 2
            );
178
        }
179
180 61
        $namespace = __NAMESPACE__.'\\Fields\\';
181
182 61
        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 82
    public function prepareAttributes($options)
192
    {
193 82
        if (!$options) {
194 4
            return null;
195
        }
196
197 82
        $attributes = [];
198
199 82
        foreach ($options as $name => $option) {
200 82
            if ($option !== null) {
201 82
                $name = is_numeric($name) ? $option : $name;
202 82
                $attributes[] = $name.'="'.$option.'" ';
203 82
            }
204 82
        }
205
206 82
        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 (!array_key_exists($name, $this->customTypes)) {
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 103
    private function loadCustomTypes()
228
    {
229 103
        $customFields = (array) $this->getConfig('custom_fields');
230
231 103
        if (!empty($customFields)) {
232 1
            foreach ($customFields as $fieldName => $fieldClass) {
233 1
                $this->addCustomField($fieldName, $fieldClass);
234 1
            }
235 1
        }
236 103
    }
237
238 5
    public function convertModelToArray($model)
239
    {
240 5
        if (!$model) {
241 1
            return null;
242
        }
243
244 5
        if ($model instanceof Model) {
245 1
            return $model->toArray();
246
        }
247
248 5
        if ($model instanceof Collection) {
249 2
            return $model->all();
250
        }
251
252 5
        return $model;
253
    }
254
255
    /**
256
     * Format the label to the proper format
257
     *
258
     * @param $name
259
     * @return string
260
     */
261 80
    public function formatLabel($name)
262
    {
263 80
        if (!$name) {
264 1
            return null;
265
        }
266
267 80
        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...
268 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...
269
270 1
            if (is_string($translatedName)) {
271 1
                return $translatedName;
272
            }
273
        }
274
275 79
        return ucfirst(str_replace('_', ' ', $name));
276
    }
277
278
    /**
279
     * @param FormField[] $fields
280
     * @return array
281
     */
282 7
    public function mergeFieldsRules($fields)
283
    {
284 7
        $rules = [];
285 7
        $attributes = [];
286 7
        $messages = [];
287
288 7
        foreach ($fields as $field) {
289 7
            if ($fieldRules = $field->getValidationRules()) {
290 7
                $rules = array_merge($rules, $fieldRules['rules']);
291 7
                $attributes = array_merge($attributes, $fieldRules['attributes']);
292 7
                $messages = array_merge($messages, $fieldRules['error_messages']);
293 7
            }
294 7
        }
295
296
        return [
297 7
            'rules' => $rules,
298 7
            'attributes' => $attributes,
299
            'error_messages' => $messages
300 7
        ];
301
    }
302
303
    /**
304
     * @return array
305
     */
306 1
    public function mergeAttributes($fields)
307
    {
308 1
        $attributes = [];
309 1
        foreach ($fields as $field) {
310 1
            $attributes = array_merge($attributes, $field->getAllAttributes());
311 1
        }
312
313 1
        return $attributes;
314
    }
315
316
    /**
317
     * @param string $string
318
     * @return string
319
     */
320 81
    public function transformToDotSyntax($string)
321
    {
322 81
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
323
    }
324
325
    /**
326
     * @param string $string
327
     * @return string
328
     */
329 6
    public function transformToBracketSyntax($string)
330
    {
331 6
        $name = explode('.', $string);
332 6
        if (count($name) == 1) {
333
            return $name[0];
334
        }
335
336 6
        $first = array_shift($name);
337 6
        return $first . '[' . implode('][', $name) . ']';
338
    }
339
340
    /**
341
     * @return TranslatorInterface
342
     */
343 3
    public function getTranslator()
344
    {
345 3
        return $this->translator;
346
    }
347
348
    /**
349
     * Check if field name is valid and not reserved
350
     *
351
     * @throws \InvalidArgumentException
352
     * @param string $name
353
     * @param string $className
354
     */
355 52
    public function checkFieldName($name, $className)
356
    {
357 52
        if (!$name || trim($name) == '') {
358 2
            throw new \InvalidArgumentException(
359 2
                "Please provide valid field name for class [{$className}]"
360 2
            );
361
        }
362
363 50
        if (in_array($name, static::$reservedFieldNames)) {
364 2
            throw new \InvalidArgumentException(
365 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
366 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
367 2
            );
368
        }
369
370 48
        return true;
371
    }
372
}
373