Completed
Push — develop ( bd7c3a...4cf41c )
by Peter
18:23
created

Form   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 19

Importance

Changes 0
Metric Value
wmc 64
c 0
b 0
f 0
lcom 2
cbo 19
dl 0
loc 462
rs 2.4812

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
A getFormEvent() 0 7 2
A setFormEvent() 0 5 1
A setTranslatorTextDomain() 0 6 1
A setRenderErrors() 0 5 1
A __invoke() 0 23 3
C createForm() 0 43 7
D resolveRouteFormAction() 0 37 9
C drawNodes() 0 60 10
F matchTemplate() 0 153 29

How to fix   Complexity   

Complex Class

Complex classes like Form 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 Form, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Webino (http://webino.sk)
4
 *
5
 * @link        https://github.com/webino/WebinoDraw for the canonical source repository
6
 * @copyright   Copyright (c) 2012-2017 Webino, s. r. o. (http://webino.sk)
7
 * @author      Peter Bačinský <[email protected]>
8
 * @license     BSD-3-Clause
9
 */
10
11
namespace WebinoDraw\Draw\Helper;
12
13
use WebinoDraw\Event\DrawFormEvent;
14
use WebinoDraw\Dom\NodeList;
15
use WebinoDraw\Exception;
16
use WebinoDraw\Instructions\InstructionsRenderer;
17
use Zend\Form\FormInterface;
18
use Zend\Form\View\Helper\FormCollection;
19
use WebinoDraw\Form\View\Helper\FormElement;
20
use Zend\Form\View\Helper\FormElementErrors;
21
use Zend\Form\View\Helper\FormRow;
22
use Zend\View\Helper\Url;
23
use Zend\ServiceManager\ServiceLocatorInterface;
24
25
/**
26
 * Draw helper used to render the form
27
 *
28
 * @todo redesign
29
 */
30
class Form extends AbstractHelper
31
{
32
    /**
33
     * Draw helper service name
34
     */
35
    const SERVICE = 'webinodrawform';
36
37
    /**
38
     * @var DrawFormEvent
39
     */
40
    private $formEvent;
41
42
    /**
43
     * @var ServiceLocatorInterface
44
     */
45
    protected $forms;
46
47
    /**
48
     * @var FormRow
49
     */
50
    protected $formRow;
51
52
    /**
53
     * @var FormElement
54
     */
55
    protected $formElement;
56
57
    /**
58
     * @var FormElementErrors
59
     */
60
    protected $formElementErrors;
61
62
    /**
63
     * @var FormCollection
64
     */
65
    protected $formCollection;
66
67
    /**
68
     * @var Url
69
     */
70
    protected $url;
71
72
    /**
73
     * @var InstructionsRenderer
74
     */
75
    protected $instructionsRenderer;
76
77
    /**
78
     * @todo redesign
79
     * @param ServiceLocatorInterface $forms
80
     * @param FormRow $formRow
81
     * @param FormElement $formElement
82
     * @param FormElementErrors $formElementErrors
83
     * @param FormCollection $formCollection
84
     * @param Url $url
85
     * @param InstructionsRenderer $instructionsRenderer
86
     */
87
    public function __construct(
88
        ServiceLocatorInterface $forms,
89
        FormRow $formRow,
90
        FormElement $formElement,
91
        FormElementErrors $formElementErrors,
92
        FormCollection $formCollection,
93
        Url $url,
94
        InstructionsRenderer $instructionsRenderer
95
    ) {
96
        $this->forms                = $forms;
97
        $this->formRow              = $formRow;
98
        $this->formElement          = $formElement;
99
        $this->formElementErrors    = $formElementErrors;
100
        $this->formCollection       = $formCollection;
101
        $this->url                  = $url;
102
        $this->instructionsRenderer = $instructionsRenderer;
103
    }
104
105
    /**
106
     * @param NodeList $nodes
107
     * @param array $spec
108
     */
109
    public function __invoke(NodeList $nodes, array $spec)
110
    {
111
        $event = $this->getFormEvent();
112
        $event
113
            ->setHelper($this)
114
            ->clearSpec()
115
            ->setSpec($spec)
116
            ->setNodes($nodes);
117
118
        $cache = $this->getCache();
119
        if ($cache->load($event)) {
120
            return;
121
        }
122
123
        $form = $this->createForm($spec);
124
        $event->setForm($form);
125
126
        array_key_exists('trigger', $spec)
127
            and $this->trigger($spec['trigger'], $event);
128
129
        $this->drawNodes($nodes, $event->getSpec()->getArrayCopy());
130
        $cache->save($event);
131
    }
132
133
    /**
134
     * @return DrawFormEvent
135
     */
136
    public function getFormEvent()
137
    {
138
        if (null === $this->formEvent) {
139
            $this->setFormEvent(new DrawFormEvent);
140
        }
141
        return $this->formEvent;
142
    }
143
144
    /**
145
     * @param DrawFormEvent $event
146
     * @return $this
147
     */
148
    public function setFormEvent(DrawFormEvent $event)
149
    {
150
        $this->formEvent = $event;
151
        return $this;
152
    }
153
154
    /**
155
     * @param string $textDomain
156
     * @return $this
157
     */
158
    public function setTranslatorTextDomain($textDomain = 'default')
159
    {
160
        $this->formRow->setTranslatorTextDomain($textDomain);
161
        $this->formElement->setTranslatorTextDomain($textDomain);
162
        return $this;
163
    }
164
165
    /**
166
     * @param bool $bool
167
     * @return $this
168
     */
169
    public function setRenderErrors($bool = true)
170
    {
171
        $this->formRow->setRenderErrors($bool);
172
        return $this;
173
    }
174
175
    /**
176
     * @param array $spec
177
     * @return FormInterface
178
     * @throws Exception\RuntimeException
179
     */
180
    protected function createForm(array $spec)
181
    {
182
        if (empty($spec['form'])) {
183
            throw new Exception\RuntimeException(
184
                sprintf('Expected form option in: %s', print_r($spec, true))
185
            );
186
        }
187
188
        try {
189
            $form = clone $this->forms->get($spec['form']);
190
        } catch (\Exception $exc) {
191
            throw new Exception\RuntimeException(
192
                sprintf('Expected form in: %s; ' . $exc->getMessage(), print_r($spec, true)),
193
                $exc->getCode(),
194
                $exc
195
            );
196
        }
197
198
        if (!($form instanceof FormInterface)) {
199
            throw new Exception\LogicException('Expected form of type FormInterface');
200
        }
201
202
        if (isset($spec['route'])) {
203
            try {
204
                $routeFormAction = $this->resolveRouteFormAction($spec['route']);
205
            } catch (\Exception $exc) {
206
                throw new Exception\RuntimeException(
207
                    $exc->getMessage() . sprintf(' for %s', print_r($spec, true)),
208
                    $exc->getCode(),
209
                    $exc
210
                );
211
            }
212
213
            $form->setAttribute('action', $routeFormAction);
214
        }
215
216
        if (!empty($spec['populate'])) {
217
            $this->translate($spec['populate']);
218
            $form->populateValues($spec['populate']);
219
        }
220
221
        return $form;
222
    }
223
224
    /**
225
     * @param string|array $spec
226
     * @return string
227
     * @throws Exception\InvalidArgumentException
228
     * @throws Exception\RuntimeException
229
     */
230
    protected function resolveRouteFormAction($spec)
231
    {
232
        if (!is_string($spec) && !is_array($spec)) {
233
            throw new Exception\InvalidArgumentException('Expected string or array');
234
        }
235
236
        $route = is_array($spec) ? $spec : ['name' => $spec];
237
238
        if (empty($route['name'])) {
239
            throw new Exception\RuntimeException('Expected route name option');
240
        }
241
242
        $params  = !empty($route['params']) ? $route['params'] : [];
243
        $options = !empty($route['options']) ? $route['options'] : [];
244
        $reusedMatchedParams = !empty($route['reuseMatchedParams']) ? $route['reuseMatchedParams'] : [];
245
246
        try {
247
            $routeFormAction = $this->url->__invoke(
248
                $route['name'],
249
                $params,
250
                $options,
251
                $reusedMatchedParams
252
            );
253
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
254
        } catch (\Exception $exc) {
255
            throw new Exception\RuntimeException(
256
                sprintf(
257
                    'Expected route `%s`',
258
                    $route['name']
259
                ),
260
                $exc->getCode(),
261
                $exc
262
            );
263
        }
264
265
        return $routeFormAction;
266
    }
267
268
    /**
269
     * @todo refactor
270
     *
271
     * @param NodeList $nodes
272
     * @param array $spec
273
     * @return $this
274
     */
275
    public function drawNodes(NodeList $nodes, array $spec)
276
    {
277
        $form = $this->getFormEvent()->getForm();
278
279
        // set form attributes without class,
280
        // it will be appended later
281
        $formAttribs = (array) $form->getAttributes();
282
        $formClass   = '';
283
        if (isset($formAttribs['class'])) {
284
            $formClass = $formAttribs['class'];
285
            unset($formAttribs['class']);
286
        }
287
        $nodes->setAttribs($formAttribs);
288
289
        isset($spec['wrap'])
290
            or $spec['wrap'] = false;
291
292
        isset($spec['text_domain'])
293
            or $spec['text_domain'] = 'default';
294
295
        $this->setTranslatorTextDomain($spec['text_domain']);
296
297
        // defer sub-instructions
298
        $subInstructions = !empty($spec['instructions']) ? $spec['instructions'] : [];
299
        unset($spec['instructions']);
300
        parent::drawNodes($nodes, $spec);
301
302
        array_key_exists('render_errors', $spec)
303
            and $this->setRenderErrors($spec['render_errors']);
304
305
        $translation = $this->getVarTranslator()->createTranslation($this->getVars());
306
        foreach ($nodes as $node) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
307
308
            // append the form class to the node class
309
            $node->setAttribute('class', trim($node->getAttribute('class') . ' ' . $formClass));
310
311
            $childNodes = $nodes->create([$node]);
312
            if (empty($node->childNodes->length)) {
313
                // easy render
314
                $childNodes->setHtml($this->formCollection->__invoke($form, $spec['wrap']));
0 ignored issues
show
Bug introduced by
It seems like $this->formCollection->_...e($form, $spec['wrap']) targeting Zend\Form\View\Helper\FormCollection::__invoke() can also be of type object<Zend\Form\View\Helper\FormCollection>; however, WebinoDraw\Dom\NodeList::setHtml() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
315
            } else {
316
                $this->matchTemplate($childNodes, $form);
317
            }
318
319
            $this->instructionsRenderer->expandInstructions($spec, $translation);
320
            if (!empty($subInstructions)) {
321
                foreach ($childNodes as $childNode) {
322
                    // sub-instructions
323
                    $this->instructionsRenderer
324
                        ->subInstructions(
325
                            [$childNode],
326
                            $subInstructions,
327
                            $translation
328
                        );
329
                }
330
            }
331
        }
332
333
        return $this;
334
    }
335
336
    // TODO DrawFormRenderer
337
    // todo decouple & redesign
338
    protected function matchTemplate(NodeList $nodes, FormInterface $form)
339
    {
340
        // todo injection
341
        $translator = $this->formRow->getTranslator();
342
343
        $matched  = [];
344
        $toRemove = [];
345
        $elements = $form->getElements();
346
347
        /* @var $node \WebinoDraw\Dom\Element */
348
        foreach ($nodes as $node) {
349
            /** @var \WebinoDraw\Dom\Document $ownerDocument */
350
            $ownerDocument = $node->ownerDocument;
351
352
            $nodePath = $node->getNodePath();
353
            $elementNodes = $ownerDocument->getXpath()->query('.//*[@name]', $node);
354
            /* @var $elementNode \WebinoDraw\Dom\Element */
355
            foreach ($elementNodes as $elementNode) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
356
357
                $elementName = $elementNode->getAttribute('name');
358
                $matched[$elementName] = true;
359
360
                if (!$form->has($elementName)) {
361
                    // remove node of the missing form element
362
                    $parentNode = $elementNode->parentNode;
363
                    if ('label' === $parentNode->nodeName) {
364
                        $parentNode->parentNode->removeChild($parentNode);
365
                    } else {
366
                        $toRemove[$nodePath][] = $elementName;
367
                        $elementNode->parentNode->removeChild($elementNode);
368
                    }
369
                    continue;
370
                }
371
372
                $elementNode->setAttribute('id', $elementName);
373
374
                /* @var $element \Zend\Form\Element */
375
                $element    = $form->get($elementName);
376
                $attributes = $element->getAttributes();
377
378
                // TODO remaining elements
379
                if (isset($attributes['type'])) {
380
                    switch ($attributes['type']) {
381
                        case 'checkbox':
382
                            /* @var $element \Zend\Form\Element\Checkbox */
383
                            $attributes['value'] = $element->getCheckedValue();
384
                            // todo checkbox use hidden element
385
                            break;
386
                        case 'multi_checkbox':
387
                            $multiNode = $ownerDocument->createDocumentFragment();
388
                            $multiNode->appendXml($this->formRow->__invoke($element));
389
                            $elementNode = $elementNode->parentNode->replaceChild($multiNode, $elementNode);
390
                            unset($multiNode);
391
                            break;
392
                        case 'select':
393
                            $selectNode = $ownerDocument->createDocumentFragment();
394
                            $selectNode->appendXml($this->formElement->__invoke($element));
395
                            $newNode = $selectNode->firstChild;
396
                            $elementNode->parentNode->replaceChild($newNode, $elementNode);
397
                            $elementNode = $newNode;
398
                            unset($selectNode);
399
                            unset($newNode);
400
                            break;
401
402
                        case 'text':
403
                        case 'email':
404
                        case 'submit':
405
                        case 'reset':
406
                        case 'button':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
407
408
                            // set element node value if available
409
                            $value = $elementNode->hasAttribute('value')
410
                                ? $elementNode->getAttribute('value')
411
                                : $element->getValue();
412
413
                            $attributes['value'] = !empty($value) ? $translator->translate($value) : '';
414
                            unset($value);
415
                            break;
416
                    }
417
                }
418
419
                // glue form & template element class
420
                empty($attributes['class'])
421
                    or $attributes['class'] = trim($attributes['class'] . ' ' . $elementNode->getAttribute('class'));
422
423
                $subElementNodes = $nodes->create([$elementNode]);
424
                $subElementNodes->setAttribs($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by $element->getAttributes() on line 376 can also be of type object<Traversable>; however, WebinoDraw\Dom\NodeList::setAttribs() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
425
426
                // labels
427
                $subElementNodes->each(
428
                    'xpath=../span[name(..)="label"]|..//label[@for="' . $elementName . '"]',
429
                    function ($nodes) use ($element, $translator) {
430
                        $label = $translator->translate($element->getLabel());
431
432
                        /* @var $subNode \WebinoDraw\Dom\Element */
433
                        foreach ($nodes as $subNode) {
434
                            $subNode->nodeValue = !$subNode->isEmpty()
435
                                ? $translator->translate($subNode->nodeValue)
436
                                : $label;
437
                        }
438
                    }
439
                );
440
441
                // errors
442
                $messages = $element->getMessages();
443
                if (!empty($messages)) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
444
445
                    $errorNode = $ownerDocument->createDocumentFragment();
446
                    $errorNode->appendXml($this->formElementErrors->__invoke($element));
447
448
                    foreach ($subElementNodes as $subElementNode) {
449
                        if (empty($subElementNode->nextSibling)) {
450
                            $subElementNode->parentNode->appendChild($errorNode);
451
                        } else {
452
                            $subElementNode->parentNode->insertBefore($errorNode, $subElementNode);
453
                        }
454
                    }
455
                }
456
            }
457
458
            // auto draw hidden nodes
459
            /** @var \Zend\Form\Element $element */
460
            foreach ($elements as $element) {
461
                $attributes = $element->getAttributes();
462
                if (empty($attributes['type'])
463
                    || 'hidden' !== $attributes['type']
464
                ) {
465
                    continue;
466
                }
467
468
                // skip matched elements
469
                if (isset($matched[$attributes['name']])) {
470
                    continue;
471
                }
472
473
                $hiddenNode = $ownerDocument->createDocumentFragment();
474
                $xhtml      = (string) $this->formRow->__invoke($element);
475
476
                $hiddenNode->appendXml($xhtml);
477
                if (!$hiddenNode->hasChildNodes()) {
478
                    throw new Exception\RuntimeException('Invalid XHTML ' . $xhtml);
479
                }
480
                $node->appendChild($hiddenNode);
481
            }
482
        }
483
484
        // remove labels of missing elements
485
        foreach ($toRemove as $nodePath => $elementNames) {
486
            foreach ($elementNames as $elementName) {
487
                $nodes->remove('xpath=' . $nodePath . '//label[@for="' . $elementName . '"]');
488
            }
489
        }
490
    }
491
}
492