Completed
Push — master ( 4cd325...0e7ea3 )
by Kristijan
05:44
created

FormHelper::mergeAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 1
b 0
f 0
ccs 6
cts 6
cp 1
crap 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 102
    public function __construct(View $view, TranslatorInterface $translator, array $config = [])
90
    {
91 102
        $this->view = $view;
92 102
        $this->translator = $translator;
93 102
        $this->config = $config;
94 102
        $this->loadCustomTypes();
95 102
    }
96
97
    /**
98
     * @param string $key
99
     * @param string $default
100
     * @return mixed
101
     */
102 102
    public function getConfig($key, $default = null)
103
    {
104 102
        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 102
    public function mergeOptions(array $first, array $second)
123
    {
124 102
        return array_replace_recursive($first, $second);
125
    }
126
127
    /**
128
     * Get proper class for field type
129
     *
130
     * @param $type
131
     * @return string
132
     */
133 65
    public function getFieldType($type)
134
    {
135 65
        $types = array_keys(static::$availableFieldTypes);
136
137 65
        if (!$type || trim($type) == '') {
138 1
            throw new \InvalidArgumentException('Field type must be provided.');
139
        }
140
141 64
        if (array_key_exists($type, $this->customTypes)) {
142 2
            return $this->customTypes[$type];
143
        }
144
145 62
        if (!in_array($type, $types)) {
146 2
            throw new \InvalidArgumentException(
147 2
                sprintf(
148 2
                    'Unsupported field type [%s]. Available types are: %s',
149 2
                    $type,
150 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
151 2
                )
152 2
            );
153
        }
154
155 60
        $namespace = __NAMESPACE__.'\\Fields\\';
156
157 60
        return $namespace . static::$availableFieldTypes[$type];
158
    }
159
160
    /**
161
     * Convert array of attributes to html attributes
162
     *
163
     * @param $options
164
     * @return string
165
     */
166 81
    public function prepareAttributes($options)
167
    {
168 81
        if (!$options) {
169 4
            return null;
170
        }
171
172 81
        $attributes = [];
173
174 81
        foreach ($options as $name => $option) {
175 81
            if ($option !== null) {
176 81
                $name = is_numeric($name) ? $option : $name;
177 81
                $attributes[] = $name.'="'.$option.'" ';
178 81
            }
179 81
        }
180
181 81
        return join('', $attributes);
182
    }
183
184
    /**
185
     * Add custom field
186
     *
187
     * @param $name
188
     * @param $class
189
     */
190 3
    public function addCustomField($name, $class)
191
    {
192 3
        if (!array_key_exists($name, $this->customTypes)) {
193 3
            return $this->customTypes[$name] = $class;
194
        }
195
196 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
197
    }
198
199
    /**
200
     * Load custom field types from config file
201
     */
202 102
    private function loadCustomTypes()
203
    {
204 102
        $customFields = (array) $this->getConfig('custom_fields');
205
206 102
        if (!empty($customFields)) {
207 1
            foreach ($customFields as $fieldName => $fieldClass) {
208 1
                $this->addCustomField($fieldName, $fieldClass);
209 1
            }
210 1
        }
211 102
    }
212
213 5
    public function convertModelToArray($model)
214
    {
215 5
        if (!$model) {
216 1
            return null;
217
        }
218
219 5
        if ($model instanceof Model) {
220 1
            return $model->toArray();
221
        }
222
223 5
        if ($model instanceof Collection) {
224 2
            return $model->all();
225
        }
226
227 5
        return $model;
228
    }
229
230
    /**
231
     * Format the label to the proper format
232
     *
233
     * @param $name
234
     * @return string
235
     */
236 79
    public function formatLabel($name)
237
    {
238 79
        if (!$name) {
239 1
            return null;
240
        }
241
242 79
        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...
243 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...
244
245 1
            if (is_string($translatedName)) {
246 1
                return $translatedName;
247
            }
248
        }
249
250 78
        return ucfirst(str_replace('_', ' ', $name));
251
    }
252
253
    /**
254
     * @param FormField[] $fields
255
     * @return array
256
     */
257 7
    public function mergeFieldsRules($fields)
258
    {
259 7
        $rules = [];
260 7
        $attributes = [];
261 7
        $messages = [];
262
263 7
        foreach ($fields as $field) {
264 7
            if ($fieldRules = $field->getValidationRules()) {
265 7
                $rules = array_merge($rules, $fieldRules['rules']);
266 7
                $attributes = array_merge($attributes, $fieldRules['attributes']);
267 7
                $messages = array_merge($messages, $fieldRules['error_messages']);
268 7
            }
269 7
        }
270
271
        return [
272 7
            'rules' => $rules,
273 7
            'attributes' => $attributes,
274
            'error_messages' => $messages
275 7
        ];
276
    }
277
278
    /**
279
     * @return array
280
     */
281 1
    public function mergeAttributes($fields)
282
    {
283 1
        $attributes = [];
284 1
        foreach ($fields as $field) {
285 1
            $attributes = array_merge($attributes, $field->getAllAttributes());
286 1
        }
287
288 1
        return $attributes;
289
    }
290
291
    /**
292
     * @param string $string
293
     * @return string
294
     */
295 80
    public function transformToDotSyntax($string)
296
    {
297 80
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
298
    }
299
300
    /**
301
     * @return TranslatorInterface
302
     */
303 3
    public function getTranslator()
304
    {
305 3
        return $this->translator;
306
    }
307
308
    /**
309
     * Check if field name is valid and not reserved
310
     *
311
     * @throws \InvalidArgumentException
312
     * @param string $name
313
     * @param string $className
314
     */
315 51
    public function checkFieldName($name, $className)
316
    {
317 51
        if (!$name || trim($name) == '') {
318 2
            throw new \InvalidArgumentException(
319 2
                "Please provide valid field name for class [{$className}]"
320 2
            );
321
        }
322
323 49
        if (in_array($name, static::$reservedFieldNames)) {
324 2
            throw new \InvalidArgumentException(
325 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
326 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
327 2
            );
328
        }
329
330 47
        return true;
331
    }
332
}
333