Passed
Push — master ( 9cead0...545cea )
by
unknown
12:43
created

FormTrait   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 497
Duplicated Lines 2.01 %

Coupling/Cohesion

Components 3
Dependencies 2

Importance

Changes 0
Metric Value
wmc 55
lcom 3
cbo 2
dl 10
loc 497
rs 6.8
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
sortItemsByPriority() 0 4 ?
A setFormGroupFactory() 0 6 1
A formGroupFactory() 0 10 2
A setGroupCallback() 0 6 1
A setAction() 0 11 2
A action() 0 4 1
A setMethod() 0 12 2
A method() 0 4 1
A setL10nMode() 0 6 1
A l10nMode() 0 4 1
A setGroups() 0 10 2
A addGroup() 0 16 4
B parseFormGroup() 0 28 6
A createFormGroup() 0 17 3
A updateFormGroup() 0 17 3
A defaultGroupType() 0 4 1
C groups() 0 44 11
A hasGroups() 0 4 1
A hasGroup() 0 10 2
A numGroups() 0 4 1
A setGroupDisplayMode() 0 16 3
A groupDisplayMode() 0 4 1
A isTabbable() 0 4 1
A setFormData() 0 6 1
A addFormData() 10 11 2
A formData() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FormTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FormTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Ui\Form;
4
5
use Charcoal\Ui\ConditionalizableInterface;
6
use Exception;
7
use InvalidArgumentException;
8
9
// From 'charcoal-factory'
10
use Charcoal\Factory\FactoryInterface;
11
12
// From 'charcoal-ui'
13
use Charcoal\Ui\Form\FormInterface;
14
use Charcoal\Ui\FormGroup\FormGroupInterface;
15
use Charcoal\Ui\PrioritizableInterface;
16
17
/**
18
 * Provides an implementation of {@see FormInterface}.
19
 */
20
trait FormTrait
21
{
22
    /**
23
     * The URI of a program that processes the form information.
24
     *
25
     * @var string
26
     */
27
    private $action = '';
28
29
    /**
30
     * The HTTP method that the browser uses to submit the form.
31
     *
32
     * @var string
33
     */
34
    private $method = 'post';
35
36
    /**
37
     * The form's display mode for multilingual fields.
38
     *
39
     * @var string
40
     */
41
    private $l10nMode = 'loop_inputs';
42
43
    /**
44
     * The form's display mode for groups.
45
     *
46
     * @var string
47
     */
48
    protected $groupDisplayMode;
49
50
    /**
51
     * The form's field groups.
52
     *
53
     * @var FormGroupInterface[]
54
     */
55
    protected $groups = [];
56
57
    /**
58
     * The form's predefined data.
59
     *
60
     * @var array $formData
61
     */
62
    private $formData = [];
63
64
    /**
65
     * Store the form's metadata instance.
66
     *
67
     * @var MetadataInterface
68
     */
69
    private $metadata;
70
71
    /**
72
     * Store the form's group factory instance.
73
     *
74
     * @var FactoryInterface
75
     */
76
    protected $formGroupFactory;
77
78
    /**
79
     * Store the form's group callback.
80
     *
81
     * @var callable
82
     */
83
    private $groupCallback;
84
85
    /**
86
     * Comparison function used by {@see uasort()}.
87
     *
88
     * @param  PrioritizableInterface $a Sortable entity A.
89
     * @param  PrioritizableInterface $b Sortable entity B.
90
     * @return integer Sorting value: -1 or 1.
91
     */
92
    abstract protected function sortItemsByPriority(
93
        PrioritizableInterface $a,
94
        PrioritizableInterface $b
95
    );
96
97
    /**
98
     * @param FactoryInterface $factory A factory, to create customized form gorup objects.
99
     * @return FormInterface Chainable
100
     */
101
    public function setFormGroupFactory(FactoryInterface $factory)
102
    {
103
        $this->formGroupFactory = $factory;
104
105
        return $this;
106
    }
107
108
    /**
109
     * @throws Exception If the form group factory object was not set / injected.
110
     * @return FormInterface Chainable
111
     */
112
    protected function formGroupFactory()
113
    {
114
        if ($this->formGroupFactory === null) {
115
            throw new Exception(
116
                'Form group factory was not set.'
117
            );
118
        }
119
120
        return $this->formGroupFactory;
121
    }
122
123
    /**
124
     * @param callable $cb The group callback.
125
     * @return FormInterface Chainable
126
     */
127
    public function setGroupCallback(callable $cb)
128
    {
129
        $this->groupCallback = $cb;
130
131
        return $this;
132
    }
133
134
    /**
135
     * @param string $action The "action" value, typically a URL.
136
     * @throws InvalidArgumentException If the action argument is not a string.
137
     * @return FormInterface Chainable
138
     */
139
    public function setAction($action)
140
    {
141
        if (!is_string($action)) {
142
            throw new InvalidArgumentException(
143
                'Action must be a string'
144
            );
145
        }
146
        $this->action = $action;
147
148
        return $this;
149
    }
150
151
    /**
152
     * @return string
153
     */
154
    public function action()
155
    {
156
        return $this->action;
157
    }
158
159
    /**
160
     * Set the method (forcing lowercase) to "post" or "get".
161
     *
162
     * @param string $method Either "post" or "get".
163
     * @throws InvalidArgumentException If the method is not post or get.
164
     * @return FormInterface Chainable
165
     */
166
    public function setMethod($method)
167
    {
168
        $method = strtolower($method);
169
        if (!in_array($method, ['post', 'get'])) {
170
            throw new InvalidArgumentException(
171
                'Method must be "post" or "get"'
172
            );
173
        }
174
        $this->method = $method;
175
176
        return $this;
177
    }
178
179
    /**
180
     * @return string Either "post" or "get".
181
     */
182
    public function method()
183
    {
184
        return $this->method;
185
    }
186
187
    /**
188
     * @param string $mode The l10n mode.
189
     * @return FormInterface Chainable
190
     */
191
    public function setL10nMode($mode)
192
    {
193
        $this->l10nMode = $mode;
194
195
        return $this;
196
    }
197
198
    /**
199
     * @return string
200
     */
201
    public function l10nMode()
202
    {
203
        return $this->l10nMode;
204
    }
205
206
    /**
207
     * Set the object's form groups.
208
     *
209
     * @param array $groups A collection of group structures.
210
     * @return FormInterface Chainable
211
     */
212
    public function setGroups(array $groups)
213
    {
214
        $this->groups = [];
215
216
        foreach ($groups as $groupIdent => $group) {
217
            $this->addGroup($groupIdent, $group);
218
        }
219
220
        return $this;
221
    }
222
223
    /**
224
     * Add a form group.
225
     *
226
     * @param  string                   $groupIdent The group identifier.
227
     * @param  array|FormGroupInterface $group      The group object or structure.
228
     * @throws InvalidArgumentException If the identifier is not a string or the group is invalid.
229
     * @return FormInterface Chainable
230
     */
231
    public function addGroup($groupIdent, $group)
232
    {
233
        if ($group === false || $group === null) {
234
            return $this;
235
        }
236
237
        $group = $this->parseFormGroup($groupIdent, $group);
238
239
        if (isset($group['ident'])) {
240
            $groupIdent = $group['ident'];
241
        }
242
243
        $this->groups[$groupIdent] = $group;
244
245
        return $this;
246
    }
247
248
    /**
249
     * Parse a form group.
250
     *
251
     * @param  string                   $groupIdent The group identifier.
252
     * @param  array|FormGroupInterface $group      The group object or structure.
253
     * @throws InvalidArgumentException If the identifier is not a string or the group is invalid.
254
     * @return FormGroupInterface
255
     */
256
    protected function parseFormGroup($groupIdent, $group)
257
    {
258
        if (!is_string($groupIdent)) {
259
            throw new InvalidArgumentException(
260
                'Group identifier must be a string'
261
            );
262
        }
263
264
        if ($group instanceof FormGroupInterface) {
265
            $group = $this->updateFormGroup($group, null, $groupIdent);
266
        } elseif (is_array($group)) {
267
            $data = $group;
268
269
            if (!isset($data['ident'])) {
270
                $data['ident'] = $groupIdent;
271
            }
272
273
            $group = $this->createFormGroup($data);
274
        } else {
275
            throw new InvalidArgumentException(sprintf(
276
                'Group must be an instance of %s or an array of form group options, received %s',
277
                'FormGroupInterface',
278
                (is_object($group) ? get_class($group) : gettype($group))
279
            ));
280
        }
281
282
        return $group;
283
    }
284
285
    /**
286
     * Create a new form group widget.
287
     *
288
     * @param  array|null $data Optional. The form group data to set.
289
     * @return FormGroupInterface
290
     */
291
    protected function createFormGroup(array $data = null)
292
    {
293
        if (isset($data['type'])) {
294
            $type = $data['type'];
295
        } else {
296
            $type = $this->defaultGroupType();
297
        }
298
299
        $group = $this->formGroupFactory()->create($type);
0 ignored issues
show
Bug introduced by
The method create() does not seem to exist on object<Charcoal\Ui\Form\FormInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
300
        $group->setForm($this);
301
302
        if ($data !== null) {
303
            $group->setData($data);
304
        }
305
306
        return $group;
307
    }
308
309
    /**
310
     * Update the given form group widget.
311
     *
312
     * @param  FormGroupInterface $group      The form group to update.
313
     * @param  array|null         $groupData  Optional. The new group data to apply.
314
     * @param  string|null        $groupIdent Optional. The new group identifier.
315
     * @return FormGroupInterface
316
     */
317
    protected function updateFormGroup(
318
        FormGroupInterface $group,
319
        array $groupData = null,
320
        $groupIdent = null
321
    ) {
322
        $group->setForm($this);
323
324
        if ($groupData !== null) {
325
            $group->setData($groupData);
326
        }
327
328
        if ($groupIdent !== null) {
329
            $group->setIdent($groupIdent);
330
        }
331
332
        return $group;
333
    }
334
335
    /**
336
     * Retrieve the default form group class name.
337
     *
338
     * @return string
339
     */
340
    public function defaultGroupType()
341
    {
342
        return 'charcoal/ui/form-group/generic';
343
    }
344
345
    /**
346
     * Retrieve the form groups.
347
     *
348
     * @param callable $groupCallback Optional callback applied to each form group.
349
     * @return FormGroupInterface[]|Generator
350
     */
351
    public function groups(callable $groupCallback = null)
0 ignored issues
show
Coding Style introduced by
groups uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
352
    {
353
        $groups = $this->groups;
354
        uasort($groups, [ $this, 'sortItemsByPriority' ]);
355
356
        $groupCallback = (isset($groupCallback) ? $groupCallback : $this->groupCallback);
357
358
        $i = 1;
359
        foreach ($groups as $group) {
360
            if (!$group->active()) {
361
                continue;
362
            }
363
364
            if ($group instanceof ConditionalizableInterface) {
365
                if (!$group->resolvedCondition()) {
366
                    continue;
367
                }
368
            }
369
370
            // Test formGroup vs. ACL roles
371
            if ($group->isAuthorized() === false) {
372
                continue;
373
            }
374
375
            if (!$group->l10nMode()) {
376
                $group->setL10nMode($this->l10nMode());
377
            }
378
379
            if ($groupCallback) {
380
                $groupCallback($group);
381
            }
382
383
            $GLOBALS['widget_template'] = $group->template();
384
385
            if ($this->isTabbable() && $i > 1) {
386
                $group->isHidden = true;
387
            }
388
            $i++;
389
390
            yield $group;
391
392
            $GLOBALS['widget_template'] = '';
393
        }
394
    }
395
396
    /**
397
     * Determine if the form has any groups.
398
     *
399
     * @return boolean
400
     */
401
    public function hasGroups()
402
    {
403
        return (count($this->groups) > 0);
404
    }
405
406
    /**
407
     * Determine if the form has a given group.
408
     *
409
     * @param string $groupIdent The group identifier to look up.
410
     * @throws InvalidArgumentException If the group identifier is invalid.
411
     * @return boolean
412
     */
413
    public function hasGroup($groupIdent)
414
    {
415
        if (!is_string($groupIdent)) {
416
            throw new InvalidArgumentException(
417
                'Group identifier must be a string'
418
            );
419
        }
420
421
        return isset($this->groups[$groupIdent]);
422
    }
423
424
    /**
425
     * Count the number of form groups.
426
     *
427
     * @return integer
428
     */
429
    public function numGroups()
430
    {
431
        return count($this->groups);
432
    }
433
434
    /**
435
     * Set the widget's content group display mode.
436
     *
437
     * Currently only supports "tab".
438
     *
439
     * @param string $mode Group display mode.
440
     * @throws InvalidArgumentException If the display mode is not a string.
441
     * @return ObjectFormWidget Chainable.
442
     */
443
    public function setGroupDisplayMode($mode)
444
    {
445
        if (!is_string($mode)) {
446
            throw new InvalidArgumentException(
447
                'Display mode must be a string'
448
            );
449
        }
450
451
        if ($mode === 'tabs') {
452
            $mode = 'tab';
453
        }
454
455
        $this->groupDisplayMode = $mode;
456
457
        return $this;
458
    }
459
460
    /**
461
     * Retrieve the widget's content group display mode.
462
     *
463
     * @return string Group display mode.
464
     */
465
    public function groupDisplayMode()
466
    {
467
        return $this->groupDisplayMode;
468
    }
469
470
    /**
471
     * Determine if content groups are to be displayed as tabbable panes.
472
     *
473
     * @return boolean
474
     */
475
    public function isTabbable()
476
    {
477
        return ($this->groupDisplayMode() === 'tab');
478
    }
479
480
    /**
481
     * @param array $formData The (pre-populated) form data, as [$key=>$val] array.
482
     * @return FormInterface Chainable
483
     */
484
    public function setFormData(array $formData)
485
    {
486
        $this->formData = $formData;
487
488
        return $this;
489
    }
490
491
    /**
492
     * @param string $key The form data key, or poperty identifier.
493
     * @param mixed  $val The form data value, for a given key.
494
     * @throws InvalidArgumentException If the key argument is not a string.
495
     * @return FormInterface Chainable
496
     */
497 View Code Duplication
    public function addFormData($key, $val)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
498
    {
499
        if (!is_string($key)) {
500
            throw new InvalidArgumentException(
501
                'Can not add form data: Data key must be a string'
502
            );
503
        }
504
        $this->formData[$key] = $val;
505
506
        return $this;
507
    }
508
509
    /**
510
     * @return array
511
     */
512
    public function formData()
513
    {
514
        return $this->formData;
515
    }
516
}
517