Completed
Push — master ( 54b1af...d6de97 )
by Kristijan
11s
created

FormHelper::convertModelToArray()   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

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 16
ccs 8
cts 8
cp 1
crap 4
rs 9.2
1
<?php  namespace Kris\LaravelFormBuilder;
2
3
use Illuminate\Contracts\Support\MessageBag;
4
use Illuminate\Contracts\View\Factory as View;
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Collection;
8
use Kris\LaravelFormBuilder\Fields\FormField;
9
use Kris\LaravelFormBuilder\Form;
10
use Symfony\Component\Translation\TranslatorInterface;
11
12
class FormHelper
13
{
14
15
    /**
16
     * @var View
17
     */
18
    protected $view;
19
20
    /**
21
     * @var TranslatorInterface
22
     */
23
    protected $translator;
24
25
    /**
26
     * @var array
27
     */
28
    protected $config;
29
30
    /**
31
     * @var FormBuilder
32
     */
33
    protected $formBuilder;
34
35
    /**
36
     * @var array
37
     */
38
    protected static $reservedFieldNames = [
39
        'save'
40
    ];
41
42
    /**
43
     * All available field types
44
     *
45
     * @var array
46
     */
47
    protected static $availableFieldTypes = [
48
        'text'           => 'InputType',
49
        'email'          => 'InputType',
50
        'url'            => 'InputType',
51
        'tel'            => 'InputType',
52
        'search'         => 'InputType',
53
        'password'       => 'InputType',
54
        'hidden'         => 'InputType',
55
        'number'         => 'InputType',
56
        'date'           => 'InputType',
57
        'file'           => 'InputType',
58
        'image'          => 'InputType',
59
        'color'          => 'InputType',
60
        'datetime-local' => 'InputType',
61
        'month'          => 'InputType',
62
        'range'          => 'InputType',
63
        'time'           => 'InputType',
64
        'week'           => 'InputType',
65
        'select'         => 'SelectType',
66
        'textarea'       => 'TextareaType',
67
        'button'         => 'ButtonType',
68
        'submit'         => 'ButtonType',
69
        'reset'          => 'ButtonType',
70
        'radio'          => 'CheckableType',
71
        'checkbox'       => 'CheckableType',
72
        'choice'         => 'ChoiceType',
73
        'form'           => 'ChildFormType',
74
        'entity'         => 'EntityType',
75
        'collection'     => 'CollectionType',
76
        'repeated'       => 'RepeatedType',
77
        'static'         => 'StaticType'
78
    ];
79
80
    /**
81
     * Custom types
82
     *
83
     * @var array
84
     */
85
    private $customTypes = [];
86
87
    /**
88
     * @param View    $view
89
     * @param TranslatorInterface $translator
90
     * @param array   $config
91
     */
92 106
    public function __construct(View $view, TranslatorInterface $translator, array $config = [])
93
    {
94 106
        $this->view = $view;
95 106
        $this->translator = $translator;
96 106
        $this->config = $config;
97 106
        $this->loadCustomTypes();
98 106
    }
99
100
    /**
101
     * @param string $key
102
     * @param string $default
103
     * @return mixed
104
     */
105 106
    public function getConfig($key, $default = null)
106
    {
107 106
        return array_get($this->config, $key, $default);
108
    }
109
110
    /**
111
     * @return View
112
     */
113 35
    public function getView()
114
    {
115 35
        return $this->view;
116
    }
117
118
    /**
119
     * Merge options array
120
     *
121
     * @param array $first
122
     * @param array $second
123
     * @return array
124
     */
125 106
    public function mergeOptions(array $first, array $second)
126
    {
127 106
        return array_replace_recursive($first, $second);
128
    }
129
130
    /**
131
     * Get proper class for field type
132
     *
133
     * @param $type
134
     * @return string
135
     */
136 69
    public function getFieldType($type)
137
    {
138 69
        $types = array_keys(static::$availableFieldTypes);
139
140 69
        if (!$type || trim($type) == '') {
141 1
            throw new \InvalidArgumentException('Field type must be provided.');
142
        }
143
144 68
        if (array_key_exists($type, $this->customTypes)) {
145 2
            return $this->customTypes[$type];
146
        }
147
148 66
        if (!in_array($type, $types)) {
149 2
            throw new \InvalidArgumentException(
150
                sprintf(
151 2
                    'Unsupported field type [%s]. Available types are: %s',
152
                    $type,
153 2
                    join(', ', array_merge($types, array_keys($this->customTypes)))
154
                )
155
            );
156
        }
157
158 64
        $namespace = __NAMESPACE__.'\\Fields\\';
159
160 64
        return $namespace . static::$availableFieldTypes[$type];
161
    }
162
163
    /**
164
     * Convert array of attributes to html attributes
165
     *
166
     * @param $options
167
     * @return string
168
     */
169 85
    public function prepareAttributes($options)
170
    {
171 85
        if (!$options) {
172 6
            return null;
173
        }
174
175 85
        $attributes = [];
176
177 85
        foreach ($options as $name => $option) {
178 85
            if ($option !== null) {
179 85
                $name = is_numeric($name) ? $option : $name;
180 85
                $attributes[] = $name.'="'.$option.'" ';
181
            }
182
        }
183
184 85
        return join('', $attributes);
185
    }
186
187
    /**
188
     * Add custom field
189
     *
190
     * @param $name
191
     * @param $class
192
     */
193 3
    public function addCustomField($name, $class)
194
    {
195 3
        if (!array_key_exists($name, $this->customTypes)) {
196 3
            return $this->customTypes[$name] = $class;
197
        }
198
199 1
        throw new \InvalidArgumentException('Custom field ['.$name.'] already exists on this form object.');
200
    }
201
202
    /**
203
     * Load custom field types from config file
204
     */
205 106
    private function loadCustomTypes()
206
    {
207 106
        $customFields = (array) $this->getConfig('custom_fields');
208
209 106
        if (!empty($customFields)) {
210 1
            foreach ($customFields as $fieldName => $fieldClass) {
211 1
                $this->addCustomField($fieldName, $fieldClass);
212
            }
213
        }
214 106
    }
215
216 5
    public function convertModelToArray($model)
217
    {
218 5
        if (!$model) {
219 1
            return null;
220
        }
221
222 5
        if ($model instanceof Model) {
223 1
            return $model->toArray();
224
        }
225
226 5
        if ($model instanceof Collection) {
227 2
            return $model->all();
228
        }
229
230 5
        return $model;
231
    }
232
233
    /**
234
     * Format the label to the proper format
235
     *
236
     * @param $name
237
     * @return string
238
     */
239 83
    public function formatLabel($name)
240
    {
241 83
        if (!$name) {
242 1
            return null;
243
        }
244
245 83
        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...
246 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...
247
248 1
            if (is_string($translatedName)) {
249 1
                return $translatedName;
250
            }
251
        }
252
253 82
        return ucfirst(str_replace('_', ' ', $name));
254
    }
255
256
    /**
257
     * @param FormField[] $fields
258
     * @return array
259
     */
260 8
    public function mergeFieldsRules($fields)
261
    {
262 8
        $rules = [];
263 8
        $attributes = [];
264 8
        $messages = [];
265
266 8
        foreach ($fields as $field) {
267 8
            if ($fieldRules = $field->getValidationRules()) {
268 8
                $rules = array_merge($rules, $fieldRules['rules']);
269 8
                $attributes = array_merge($attributes, $fieldRules['attributes']);
270 8
                $messages = array_merge($messages, $fieldRules['error_messages']);
271
            }
272
        }
273
274
        return [
275 8
            'rules' => $rules,
276 8
            'attributes' => $attributes,
277 8
            'error_messages' => $messages
278
        ];
279
    }
280
281
    /**
282
     * @return array
283
     */
284 3
    public function mergeAttributes(array $fields)
285
    {
286 3
        $attributes = [];
287 3
        foreach ($fields as $field) {
288 3
            $attributes = array_merge($attributes, $field->getAllAttributes());
289
        }
290
291 3
        return $attributes;
292
    }
293
294
    /**
295
     * Alter a form's values recursively according to its fields
296
     *
297
     * @return void
298
     */
299 3
    public function alterFieldValues(Form $form, array &$values)
300
    {
301
        // Alter the form itself
302 3
        $form->alterFieldValues($values);
303
304
        // Alter the form's child forms recursively
305 3
        foreach ($form->getFields() as $name => $field) {
306 3
            if (method_exists($field, 'alterFieldValues')) {
307 2
                $fullName = $this->transformToDotSyntax($name);
308
309 2
                $subValues = Arr::get($values, $fullName);
310 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...
311 3
                Arr::set($values, $fullName, $subValues);
312
            }
313
        }
314 3
    }
315
316
    /**
317
     * Alter a form's validity recursively, and add messages with nested form prefix
318
     *
319
     * @return void
320
     */
321 8
    public function alterValid(Form $form, Form $mainForm, &$isValid)
322
    {
323
        // Alter the form itself
324 8
        $messages = $form->alterValid($mainForm, $isValid);
325
326
        // Add messages to the existing Bag
327 8
        if ($messages) {
328 1
            $messageBag = $mainForm->getValidator()->getMessageBag();
329 1
            $this->appendMessagesWithPrefix($messageBag, $form->getName(), $messages);
330
        }
331
332
        // Alter the form's child forms recursively
333 8
        foreach ($form->getFields() as $name => $field) {
334 8
            if (method_exists($field, 'alterValid')) {
335 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...
336
            }
337
        }
338 8
    }
339
340
    /**
341
     * Add unprefixed messages with prefix to a MessageBag
342
     */
343 1
    public function appendMessagesWithPrefix(MessageBag $messageBag, $prefix, array $keyedMessages)
344
    {
345 1
        foreach ($keyedMessages as $key => $messages) {
346 1
            if ($prefix) {
347 1
                $key = $this->transformToDotSyntax($prefix . '[' . $key . ']');
348
            }
349
350 1
            foreach ((array) $messages as $message) {
351 1
                $messageBag->add($key, $message);
352
            }
353
        }
354 1
    }
355
356
    /**
357
     * @param string $string
358
     * @return string
359
     */
360 84
    public function transformToDotSyntax($string)
361
    {
362 84
        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $string);
363
    }
364
365
    /**
366
     * @param string $string
367
     * @return string
368
     */
369 6
    public function transformToBracketSyntax($string)
370
    {
371 6
        $name = explode('.', $string);
372 6
        if (count($name) == 1) {
373
            return $name[0];
374
        }
375
376 6
        $first = array_shift($name);
377 6
        return $first . '[' . implode('][', $name) . ']';
378
    }
379
380
    /**
381
     * @return TranslatorInterface
382
     */
383 3
    public function getTranslator()
384
    {
385 3
        return $this->translator;
386
    }
387
388
    /**
389
     * Check if field name is valid and not reserved
390
     *
391
     * @throws \InvalidArgumentException
392
     * @param string $name
393
     * @param string $className
394
     */
395 55
    public function checkFieldName($name, $className)
396
    {
397 55
        if (!$name || trim($name) == '') {
398 2
            throw new \InvalidArgumentException(
399 2
                "Please provide valid field name for class [{$className}]"
400
            );
401
        }
402
403 53
        if (in_array($name, static::$reservedFieldNames)) {
404 2
            throw new \InvalidArgumentException(
405 2
                "Field name [{$name}] in form [{$className}] is a reserved word. Please use a different field name." .
406 2
                "\nList of all reserved words: " . join(', ', static::$reservedFieldNames)
407
            );
408
        }
409
410 51
        return true;
411
    }
412
}
413