Completed
Push — develop ( 7cf369...ca1c47 )
by Peter
02:50
created

Form::createForm()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 26
nc 8
nop 1
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-2016 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
            or $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 self
147
     */
148
    public function setFormEvent(DrawFormEvent $event)
149
    {
150
        $this->formEvent = $event;
151
        return $this;
152
    }
153
154
    /**
155
     * @param string $textDomain
156
     * @return self
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 self
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 \InvalidArgumentException
228
     * @throws Exception\RuntimeException
229
     */
230
    protected function resolveRouteFormAction($spec)
231
    {
232
        if (!is_string($spec) && !is_array($spec)) {
233
            throw new \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 self
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 (!empty($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
        !array_key_exists('render_errors', $spec)
298
            or $this->setRenderErrors($spec['render_errors']);
299
300
        $translation = $this->getVarTranslator()->createTranslation($this->getVars());
301
        foreach ($nodes as $node) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
302
303
            // append the form class to the node class
304
            $node->setAttribute('class', trim($node->getAttribute('class') . ' ' . $formClass));
305
306
            $childNodes = $nodes->create([$node]);
307
            if (empty($node->childNodes->length)) {
308
                // easy render
309
                $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...
310
            } else {
311
                $this->matchTemplate($childNodes, $form);
312
            }
313
314
            $this->instructionsRenderer->expandInstructions($spec, $translation);
315
            if (!empty($spec['instructions'])) {
316
                foreach ($childNodes as $childNode) {
317
                    // subinstructions
318
                    $this->instructionsRenderer
319
                        ->subInstructions(
320
                            [$childNode],
321
                            $spec['instructions'],
322
                            $translation
323
                        );
324
                }
325
            }
326
        }
327
328
        return $this;
329
    }
330
331
    // TODO DrawFormRenderer
332
    // todo decouple & redesign
333
    protected function matchTemplate(NodeList $nodes, FormInterface $form)
334
    {
335
        // todo injection
336
        $translator = $this->formRow->getTranslator();
337
338
        $matched  = [];
339
        $toRemove = [];
340
        $elements = $form->getElements();
341
342
        /* @var $node \WebinoDraw\Dom\Element */
343
        foreach ($nodes as $node) {
344
            /** @var \WebinoDraw\Dom\Document $ownerDocument */
345
            $ownerDocument = $node->ownerDocument;
346
347
            $nodePath = $node->getNodePath();
348
            $elementNodes = $ownerDocument->getXpath()->query('.//*[@name]', $node);
349
            /* @var $elementNode \WebinoDraw\Dom\Element */
350
            foreach ($elementNodes as $elementNode) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
351
352
                $elementName = $elementNode->getAttribute('name');
353
                $matched[$elementName] = true;
354
355
                if (!$form->has($elementName)) {
356
                    // remove node of the missing form element
357
                    $parentNode = $elementNode->parentNode;
358
                    if ('label' === $parentNode->nodeName) {
359
                        $parentNode->parentNode->removeChild($parentNode);
360
                    } else {
361
                        $toRemove[$nodePath][] = $elementName;
362
                        $elementNode->parentNode->removeChild($elementNode);
363
                    }
364
                    continue;
365
                }
366
367
                $elementNode->setAttribute('id', $elementName);
368
369
                /* @var $element \Zend\Form\Element */
370
                $element    = $form->get($elementName);
371
                $attributes = $element->getAttributes();
372
373
                // TODO remaining elements
374
                if (isset($attributes['type'])) {
375
                    switch ($attributes['type']) {
376
                        case 'checkbox':
377
                            /* @var $element \Zend\Form\Element\Checkbox */
378
                            $attributes['value'] = $element->getCheckedValue();
379
                            // todo checkbox use hidden element
380
                            break;
381
                        case 'multi_checkbox':
382
                        case 'select':
383
                            $selectNode = $ownerDocument->createDocumentFragment();
384
                            $selectNode->appendXml($this->formRow->__invoke($element));
385
                            $elementNode = $elementNode->parentNode->replaceChild($selectNode, $elementNode);
386
                            unset($selectNode);
387
                            break;
388
389
                        case 'text':
390
                        case 'email':
391
                        case 'submit':
392
                        case 'reset':
393
                        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...
394
395
                            // set element node value if available
396
                            $value = $elementNode->hasAttribute('value')
397
                                ? $elementNode->getAttribute('value')
398
                                : $element->getValue();
399
400
                            $attributes['value'] = !empty($value) ? $translator->translate($value) : '';
401
                            unset($value);
402
                            break;
403
                    }
404
                }
405
406
                $subElementNodes = $nodes->create([$elementNode]);
407
                $subElementNodes->setAttribs($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by $element->getAttributes() on line 371 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...
408
409
                // labels
410
                $subElementNodes->each(
411
                    'xpath=../span[name(..)="label"]|..//label[@for="' . $elementName . '"]',
412
                    function ($nodes) use ($element, $translator) {
413
                        $label = $translator->translate($element->getLabel());
414
415
                        /* @var $subNode \WebinoDraw\Dom\Element */
416
                        foreach ($nodes as $subNode) {
417
                            $subNode->nodeValue = !$subNode->isEmpty()
418
                                ? $translator->translate($subNode->nodeValue)
419
                                : $label;
420
                        }
421
                    }
422
                );
423
424
                // errors
425
                $messages = $element->getMessages();
426
                if (!empty($messages)) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
427
428
                    $errorNode = $ownerDocument->createDocumentFragment();
429
                    $errorNode->appendXml($this->formElementErrors->__invoke($element));
430
431
                    foreach ($subElementNodes as $subElementNode) {
432
                        if (empty($subElementNode->nextSibling)) {
433
                            $subElementNode->parentNode->appendChild($errorNode);
434
                        } else {
435
                            $subElementNode->parentNode->insertBefore($errorNode, $subElementNode);
436
                        }
437
                    }
438
                }
439
            }
440
441
            // auto draw hidden nodes
442
            /** @var \Zend\Form\Element $element */
443
            foreach ($elements as $element) {
444
                $attributes = $element->getAttributes();
445
                if (empty($attributes['type'])
446
                    || 'hidden' !== $attributes['type']
447
                ) {
448
                    continue;
449
                }
450
451
                // skip matched elements
452
                if (isset($matched[$attributes['name']])) {
453
                    continue;
454
                }
455
456
                $hiddenNode = $ownerDocument->createDocumentFragment();
457
                $xhtml      = (string) $this->formRow->__invoke($element);
458
459
                $hiddenNode->appendXml($xhtml);
460
                if (!$hiddenNode->hasChildNodes()) {
461
                    throw new Exception\RuntimeException('Invalid XHTML ' . $xhtml);
462
                }
463
                $node->appendChild($hiddenNode);
464
            }
465
        }
466
467
        // remove labels of missing elements
468
        foreach ($toRemove as $nodePath => $elementNames) {
469
            foreach ($elementNames as $elementName) {
470
                $nodes->remove('xpath=' . $nodePath . '//label[@for="' . $elementName . '"]');
471
            }
472
        }
473
    }
474
}
475