Completed
Push — wip/steps ( 551eb2...b236f9 )
by Romain
03:12
created

FormViewHelper::renderHiddenReferrerFields()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 25
nc 4
nop 0
1
<?php
2
/*
3
 * 2017 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 FormZ project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\Formz\ViewHelpers;
15
16
use Romm\Formz\AssetHandler\AssetHandlerFactory;
17
use Romm\Formz\AssetHandler\Connector\AssetHandlerConnectorManager;
18
use Romm\Formz\AssetHandler\Html\DataAttributesAssetHandler;
19
use Romm\Formz\Core\Core;
20
use Romm\Formz\Exceptions\ClassNotFoundException;
21
use Romm\Formz\Exceptions\InvalidOptionValueException;
22
use Romm\Formz\Form\Definition\Step\Step\Step;
23
use Romm\Formz\Form\Definition\Step\Step\Substep\Substep;
24
use Romm\Formz\Form\FormInterface;
25
use Romm\Formz\Form\FormObject\FormObject;
26
use Romm\Formz\Form\FormObject\FormObjectFactory;
27
use Romm\Formz\Service\ContextService;
28
use Romm\Formz\Service\ControllerService;
29
use Romm\Formz\Service\ExtensionService;
30
use Romm\Formz\Service\FormService;
31
use Romm\Formz\Service\StringService;
32
use Romm\Formz\Service\TimeTrackerService;
33
use Romm\Formz\Service\ViewHelper\Form\FormViewHelperService;
34
use TYPO3\CMS\Core\Page\PageRenderer;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
use TYPO3\CMS\Extbase\Error\Result;
37
use TYPO3\CMS\Extbase\Mvc\Web\Request;
38
use TYPO3\CMS\Fluid\View\StandaloneView;
39
40
/**
41
 * This view helper overrides the default one from Extbase, to include
42
 * everything the extension needs to work properly.
43
 *
44
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
45
 * and must be the exact same name as the form parameter in the controller
46
 * action called when the form is submitted. For instance, if your action looks
47
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
48
 * then the "name" attribute of this view helper must be "exampleForm".
49
 *
50
 * Thanks to the information of the form, the following things are automatically
51
 * handled in this view helper:
52
 *
53
 * - Class
54
 *   A custom class may be added to the form DOM element. If the TypoScript
55
 *   configuration "settings.defaultClass" is set for this form, then the given
56
 *   class will be added to the form element.
57
 *
58
 * - JavaScript
59
 *   A block of JavaScript is built from scratch, which will initialize the
60
 *   form, add validation rules to the fields, and handle activation of the
61
 *   fields validation.
62
 *
63
 * - Data attributes
64
 *   To help integrators customize every aspect they need in CSS, every useful
65
 *   information is put in data attributes in the form DOM element. For example,
66
 *   you can know in real time if the field "email" is valid if the form has the
67
 *   attribute "fz-valid-email"
68
 *
69
 * - CSS
70
 *   A block of CSS is built from scratch, which will handle the fields display,
71
 *   depending on their activation property.
72
 */
73
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
74
{
75
    /**
76
     * @var bool
77
     */
78
    protected $escapeOutput = false;
79
80
    /**
81
     * @var PageRenderer
82
     */
83
    protected $pageRenderer;
84
85
    /**
86
     * @var FormObject
87
     */
88
    protected $formObject;
89
90
    /**
91
     * @var FormViewHelperService
92
     */
93
    protected $formService;
94
95
    /**
96
     * @var string
97
     */
98
    protected $formObjectClassName;
99
100
    /**
101
     * @var AssetHandlerFactory
102
     */
103
    protected $assetHandlerFactory;
104
105
    /**
106
     * @var TimeTrackerService
107
     */
108
    protected $timeTracker;
109
110
    /**
111
     * @var bool
112
     */
113
    protected $typoScriptIncluded = false;
114
115
    /**
116
     * @var ControllerService
117
     */
118
    protected $controllerService;
119
120
    /**
121
     * @inheritdoc
122
     */
123
    public function initialize()
124
    {
125
        parent::initialize();
126
127
        $this->typoScriptIncluded = ContextService::get()->isTypoScriptIncluded();
128
129
        if (true === $this->typoScriptIncluded) {
130
            $this->timeTracker = TimeTrackerService::getAndStart();
131
132
            $this->formObjectClassName = $this->getFormClassName();
133
            $this->formObject = $this->getFormObject($this->getFormInstance());
134
            $this->timeTracker->logTime('post-config');
135
136
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
137
138
            /** @var Request $request */
139
            $request = $this->controllerContext->getRequest();
140
141
            $this->formService->setFormObject($this->formObject);
142
            $this->formService->setRequest($request);
143
            $this->formService->injectFormRequestData();
144
        }
145
146
        /*
147
         * Important: we need to instantiate the page renderer with this instead
148
         * of Extbase object manager (or with an inject function).
149
         *
150
         * This is due to some TYPO3 low level behaviour which overrides the
151
         * page renderer singleton instance, whenever a new request is used. The
152
         * problem is that the instance is not updated on Extbase side.
153
         *
154
         * Using Extbase injection can lead to old page renderer instance being
155
         * used, resulting in a leak of assets inclusion, and maybe more issues.
156
         */
157
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
158
    }
159
160
    /**
161
     * @inheritdoc
162
     */
163
    public function initializeArguments()
164
    {
165
        parent::initializeArguments();
166
167
        // The name attribute becomes mandatory.
168
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
169
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    protected function renderViewHelper()
176
    {
177
        if (false === $this->typoScriptIncluded) {
178
            return (ExtensionService::get()->isInDebugMode())
179
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
180
                : '';
181
        }
182
183
        $result = ($this->formObject->getDefinitionValidationResult()->hasErrors())
184
            // If the form configuration is not valid, we display the errors list.
185
            ? $this->getErrorText()
186
            // Everything is ok, we render the form.
187
            : $this->renderForm(func_get_args());
188
189
        $this->timeTracker->logTime('final');
190
191
        if (ExtensionService::get()->isInDebugMode()) {
192
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
193
        }
194
195
        $this->formService->resetState();
196
197
        return $result;
198
    }
199
200
    /**
201
     * Will render the whole form and return the HTML result.
202
     *
203
     * @param array $arguments
204
     * @return string
205
     */
206
    final protected function renderForm(array $arguments)
207
    {
208
        /*
209
         * We begin by setting up the form service: request results and form
210
         * instance are inserted in the service, and are used afterwards.
211
         *
212
         * There are only two ways to be sure the values injected are correct:
213
         * when the form was actually submitted by the user, or when the
214
         * argument `object` of the view helper is filled with a form instance.
215
         */
216
        $this->formService->activateFormContext();
217
218
        /*
219
         * @todo
220
         */
221
        $this->formService->checkStepDefinition();
222
223
        /*
224
         * If the form was submitted, applying custom behaviours on its fields.
225
         */
226
        $this->formService->applyBehavioursOnSubmittedForm();
227
228
        /*
229
         * Adding the default class configured in TypoScript configuration to
230
         * the form HTML tag.
231
         */
232
        $this->addDefaultClass();
233
234
        /*
235
         * Handling data attributes that are added to the form HTML tag,
236
         * depending on several parameters.
237
         */
238
        $this->handleDataAttributes();
239
240
        /*
241
         * @todo
242
         */
243
        $this->handleSubsteps();
244
245
        /*
246
         * Including JavaScript and CSS assets in the page renderer.
247
         */
248
        $this->handleAssets();
249
250
        $this->timeTracker->logTime('pre-render');
251
252
        /*
253
         * Getting the result of the original Fluid `FormViewHelper` rendering.
254
         */
255
        $result = $this->getParentRenderResult($arguments);
256
        $renderingResult = $this->formService->getResult();
257
258
        if ($renderingResult->hasErrors()) {
259
            $result = $this->getErrorText($renderingResult);
260
        }
261
262
        /*
263
         * Language files need to be included at the end, because they depend on
264
         * what was used by previous assets.
265
         */
266
        $this->getAssetHandlerConnectorManager()
267
            ->getJavaScriptAssetHandlerConnector()
268
            ->includeLanguageJavaScriptFiles();
269
270
        return $result;
271
    }
272
273
    /**
274
     * Adds a hidden field to the form rendering, containing the form request
275
     * data as a hashed string (which can be retrieved and used later).
276
     *
277
     * @return string
278
     */
279
    protected function renderHiddenReferrerFields()
280
    {
281
        $result = parent::renderHiddenReferrerFields();
282
283
        $requestData = $this->formObject->getRequestData();
284
        $requestData->setFormHash($this->formObject->getFormHash());
285
        $value = htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($requestData->toArray()))));
286
287
        $result .= '<input type="hidden" name="' . $this->prefixFieldName('formzData') . '" value="' . $value . '" />' . LF;
288
289
        if ($this->formObject->hasSteps()) {
290
            $currentStep = $this->getCurrentStep();
291
292
            if ($currentStep->hasSubsteps()) {
293
                if ($this->formObject->hasForm()) {
294
                    $stepService = FormObjectFactory::get()->getStepService($this->formObject);
295
                    
296
                    $path = array_map(
297
                        function (Substep $substep) {
298
                            return $substep->getIdentifier();
299
                        },
300
                        $stepService->getSubstepsPath()
301
                    );
302
                    
303
                    $substepsPath = [
304
                        'current' => $this->formObject->getCurrentSubstepDefinition()->getSubstep()->getIdentifier(),
305
                        'path'    => $path
306
                    ];
307
                } else {
308
                    $identifier = $currentStep->getSubsteps()->getFirstSubstepDefinition()->getSubstep()->getIdentifier();
309
                    $substepsPath = [
310
                        'current' => $identifier,
311
                        'path'    => [$identifier]
312
                    ];
313
                }
314
315
                $result .= '<input type="hidden" fz-substep-path="1" name="' . $this->prefixFieldName('substepsPath') . '" value="' . htmlspecialchars(json_encode($substepsPath)) . '" />' . LF;
316
            }
317
        }
318
319
        return $result;
320
    }
321
322
    /**
323
     * Will add a default class to the form element.
324
     *
325
     * To customize the class, take a look at `settings.defaultClass` in the
326
     * form TypoScript configuration.
327
     */
328
    protected function addDefaultClass()
329
    {
330
        $formDefaultClass = $this->formObject
331
            ->getDefinition()
332
            ->getSettings()
333
            ->getDefaultClass();
334
335
        $class = $this->tag->getAttribute('class');
336
337
        if (false === empty($formDefaultClass)) {
338
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
339
            $this->tag->addAttribute('class', $class);
340
        }
341
    }
342
343
    /**
344
     * Adds data attributes to the form element, based on several statements,
345
     * like the submitted form values, the validation result and others.
346
     */
347
    protected function handleDataAttributes()
348
    {
349
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
350
        $dataAttributes = $this->formService->getDataAttributes($dataAttributesAssetHandler);
351
352
        $this->tag->addAttributes($dataAttributes);
353
    }
354
355
    /**
356
     * @todo
357
     */
358
    protected function handleSubsteps()
359
    {
360
        $currentStep = $this->getCurrentStep();
361
362
        if ($currentStep
363
            && $currentStep->hasSubsteps()
364
        ) {
365
            $substepDefinition = $this->formObject->getCurrentSubstepDefinition();
366
            $this->tag->addAttribute('fz-substep', $substepDefinition->getSubstep()->getIdentifier());
367
        }
368
    }
369
370
    /**
371
     * Will include all JavaScript and CSS assets needed for this form.
372
     */
373
    protected function handleAssets()
374
    {
375
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
376
377
        // Default FormZ assets.
378
        $assetHandlerConnectorManager->includeDefaultAssets();
379
380
        // JavaScript assets.
381
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
382
            ->generateAndIncludeFormzConfigurationJavaScript()
383
            ->generateAndIncludeJavaScript()
384
            ->generateAndIncludeInlineJavaScript()
385
            ->includeJavaScriptValidationAndConditionFiles();
386
387
        // CSS assets.
388
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
389
            ->includeGeneratedCss();
390
    }
391
392
    /**
393
     * Will return an error text from a Fluid view.
394
     *
395
     * @param Result $renderingResult
396
     * @return string
397
     */
398
    protected function getErrorText(Result $renderingResult = null)
399
    {
400
        /** @var $view StandaloneView */
401
        $view = Core::instantiate(StandaloneView::class);
402
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
403
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
404
        $partialRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Partials');
405
        $view->setLayoutRootPaths([$layoutRootPath]);
406
        $view->setPartialRootPaths([$partialRootPath]);
407
        $view->assign('formObject', $this->formObject);
408
        $view->assign('renderingResult', $renderingResult);
409
410
        return $view->render();
411
    }
412
413
    /**
414
     * Checks the type of the argument `object`, and returns it if everything is
415
     * ok.
416
     *
417
     * @return FormInterface|null
418
     * @throws InvalidOptionValueException
419
     */
420
    protected function getFormObjectArgument()
421
    {
422
        $objectArgument = $this->arguments['object'];
423
424
        if (null === $objectArgument) {
425
            return null;
426
        }
427
428
        if (false === is_object($objectArgument)) {
429
            throw InvalidOptionValueException::formViewHelperWrongFormValueType($objectArgument);
430
        }
431
432
        if (false === $objectArgument instanceof FormInterface) {
433
            throw InvalidOptionValueException::formViewHelperWrongFormValueObjectType($objectArgument);
434
        }
435
436
        $formClassName = $this->getFormClassName();
437
438
        if (false === $objectArgument instanceof $formClassName) {
439
            throw InvalidOptionValueException::formViewHelperWrongFormValueClassName($formClassName, $objectArgument);
440
        }
441
442
        return $objectArgument;
443
    }
444
445
    /**
446
     * Returns the class name of the form object: it is fetched from the action
447
     * of the controller which will be called when submitting this form. It
448
     * means two things:
449
     * - The action must have a parameter which has the exact same name as the
450
     *   form;
451
     * - The parameter must indicate its type.
452
     *
453
     * @return string
454
     * @throws ClassNotFoundException
455
     * @throws InvalidOptionValueException
456
     */
457
    protected function getFormClassName()
458
    {
459
        $formClassName = ($this->hasArgument('formClassName'))
460
            ? $this->arguments['formClassName']
461
            : $this->getFormClassNameFromControllerAction();
462
463
        if (false === class_exists($formClassName)) {
464
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
465
        }
466
467
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
468
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
469
        }
470
471
        return $formClassName;
472
    }
473
474
    /**
475
     * Will fetch the name of the controller action argument bound to this
476
     * request.
477
     *
478
     * @return string
479
     */
480
    protected function getFormClassNameFromControllerAction()
481
    {
482
        return $this->controllerService->getFormClassNameFromControllerAction(
483
            $this->getControllerName(),
484
            $this->getControllerActionName(),
485
            $this->getFormObjectName()
486
        );
487
    }
488
489
    /**
490
     * @todo
491
     */
492
    protected function removeFormFieldNamesFromViewHelperVariableContainer()
493
    {
494
        $this->formService->checkStepFields($this->viewHelperVariableContainer);
0 ignored issues
show
Compatibility introduced by
$this->viewHelperVariableContainer of type object<TYPO3Fluid\Fluid\...elperVariableContainer> is not a sub-type of object<TYPO3\CMS\Fluid\C...elperVariableContainer>. It seems like you assume a child class of the class TYPO3Fluid\Fluid\Core\Vi...HelperVariableContainer to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
495
496
        parent::removeFormFieldNamesFromViewHelperVariableContainer();
497
    }
498
499
    /**
500
     * Renders the whole Fluid template.
501
     *
502
     * @param array $arguments
503
     * @return string
504
     */
505
    protected function getParentRenderResult(array $arguments)
506
    {
507
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
508
    }
509
510
    /**
511
     * @return string
512
     */
513
    protected function getControllerName()
514
    {
515
        return ($this->arguments['controller'])
516
            ?: $this->controllerContext
517
                ->getRequest()
518
                ->getControllerObjectName();
519
    }
520
521
    /**
522
     * @return string
523
     */
524
    protected function getControllerActionName()
525
    {
526
        return ($this->arguments['action'])
527
            ?: $this->controllerContext
528
                ->getRequest()
529
                ->getControllerActionName();
530
    }
531
532
    /**
533
     * @return AssetHandlerConnectorManager
534
     */
535
    protected function getAssetHandlerConnectorManager()
536
    {
537
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
538
    }
539
540
    /**
541
     * @return DataAttributesAssetHandler
542
     */
543
    protected function getDataAttributesAssetHandler()
544
    {
545
        /** @var DataAttributesAssetHandler $assetHandler */
546
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
547
548
        return $assetHandler;
549
    }
550
551
    /**
552
     * @return FormInterface
553
     */
554
    protected function getFormInstance()
555
    {
556
        /*
557
         * If the argument `object` was filled with an instance of Form, it
558
         * becomes the form instance. Otherwise we try to fetch an instance from
559
         * the form with errors list. If there is still no form, we create an
560
         * empty instance.
561
         */
562
        $objectArgument = $this->getFormObjectArgument();
563
564
        if ($objectArgument) {
565
            $form = $objectArgument;
566
        } else {
567
            $submittedForm = FormService::getFormWithErrors($this->getFormClassName());
0 ignored issues
show
Deprecated Code introduced by
The method Romm\Formz\Service\FormS...ce::getFormWithErrors() has been deprecated with message: This method is deprecated, please try not to use it if you can. It will be removed in FormZ v2, where you will have a whole new way to get a validated form.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
568
569
            $form = $submittedForm ?: Core::get()->getObjectManager()->getEmptyObject($this->getFormClassName());
570
        }
571
572
        return $form;
573
    }
574
575
    /**
576
     * @return Step|null
577
     */
578
    protected function getCurrentStep()
579
    {
580
        /** @var Request $request */
581
        $request = $this->controllerContext->getRequest();
582
583
        return $this->formService->getFormObject()->fetchCurrentStep($request)->getCurrentStep();
584
    }
585
586
    /**
587
     * @param FormInterface $form
588
     * @return FormObject
589
     */
590
    protected function getFormObject(FormInterface $form)
591
    {
592
        return FormObjectFactory::get()->registerAndGetFormInstance($form, $this->getFormObjectName());
593
    }
594
595
    /**
596
     * @param FormViewHelperService $service
597
     */
598
    public function injectFormService(FormViewHelperService $service)
599
    {
600
        $this->formService = $service;
601
    }
602
603
    /**
604
     * @param ControllerService $controllerService
605
     */
606
    public function injectControllerService(ControllerService $controllerService)
607
    {
608
        $this->controllerService = $controllerService;
609
    }
610
}
611