Completed
Push — middleware-wip ( f76eb7...e5756f )
by Romain
07:43
created

FormViewHelper::handleSubsteps()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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