Completed
Push — wip/steps ( 107e99...47b00f )
by Romain
02:27
created

FormViewHelper::renderHiddenIdentityField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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