Completed
Push — feature/middleware ( f1bcd3...4630a4 )
by Romain
11:52
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
         * If the form was submitted, applying custom behaviours on its fields.
218
         */
219
        $this->formService->applyBehavioursOnSubmittedForm();
220
221
        /*
222
         * Adding the default class configured in TypoScript configuration to
223
         * the form HTML tag.
224
         */
225
        $this->addDefaultClass();
226
227
        /*
228
         * Handling data attributes that are added to the form HTML tag,
229
         * depending on several parameters.
230
         */
231
        $this->handleDataAttributes();
232
233
        /*
234
         * Including JavaScript and CSS assets in the page renderer.
235
         */
236
        $this->handleAssets();
237
238
        $this->timeTracker->logTime('pre-render');
239
240
        /*
241
         * Getting the result of the original Fluid `FormViewHelper` rendering.
242
         */
243
        $result = $this->getParentRenderResult($arguments);
244
        $renderingResult = $this->formService->getResult();
245
246
        if ($renderingResult->hasErrors()) {
247
            $result = $this->getErrorText($renderingResult);
248
        }
249
250
        /*
251
         * Language files need to be included at the end, because they depend on
252
         * what was used by previous assets.
253
         */
254
        $this->getAssetHandlerConnectorManager()
255
            ->getJavaScriptAssetHandlerConnector()
256
            ->includeLanguageJavaScriptFiles();
257
258
        return $result;
259
    }
260
261
    /**
262
     * Adds a hidden field to the form rendering, containing the form request
263
     * data as a hashed string (which can be retrieved and used later).
264
     *
265
     * @return string
266
     */
267
    protected function renderHiddenReferrerFields()
268
    {
269
        $result = parent::renderHiddenReferrerFields();
270
271
        $requestData = $this->formObject->getRequestData();
272
        $requestData->setFormHash($this->formObject->getFormHash());
273
        $value = htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($requestData->toArray()))));
274
275
        $result .= '<input type="hidden" name="' . $this->prefixFieldName('formzData') . '" value="' . $value . '" />' . LF;
276
277
        return $result;
278
    }
279
280
    /**
281
     * Will add a default class to the form element.
282
     *
283
     * To customize the class, take a look at `settings.defaultClass` in the
284
     * form TypoScript configuration.
285
     */
286
    protected function addDefaultClass()
287
    {
288
        $formDefaultClass = $this->formObject
289
            ->getDefinition()
290
            ->getSettings()
291
            ->getDefaultClass();
292
293
        $class = $this->tag->getAttribute('class');
294
295
        if (false === empty($formDefaultClass)) {
296
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
297
            $this->tag->addAttribute('class', $class);
298
        }
299
    }
300
301
    /**
302
     * Adds data attributes to the form element, based on several statements,
303
     * like the submitted form values, the validation result and others.
304
     */
305
    protected function handleDataAttributes()
306
    {
307
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
308
        $dataAttributes = $this->formService->getDataAttributes($dataAttributesAssetHandler);
309
310
        $this->tag->addAttributes($dataAttributes);
311
    }
312
313
    /**
314
     * Will include all JavaScript and CSS assets needed for this form.
315
     */
316
    protected function handleAssets()
317
    {
318
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
319
320
        // Default FormZ assets.
321
        $assetHandlerConnectorManager->includeDefaultAssets();
322
323
        // JavaScript assets.
324
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
325
            ->generateAndIncludeFormzConfigurationJavaScript()
326
            ->generateAndIncludeJavaScript()
327
            ->generateAndIncludeInlineJavaScript()
328
            ->includeJavaScriptValidationAndConditionFiles();
329
330
        // CSS assets.
331
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
332
            ->includeGeneratedCss();
333
    }
334
335
    /**
336
     * Will return an error text from a Fluid view.
337
     *
338
     * @param Result $renderingResult
339
     * @return string
340
     */
341
    protected function getErrorText(Result $renderingResult = null)
342
    {
343
        /** @var $view StandaloneView */
344
        $view = Core::instantiate(StandaloneView::class);
345
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
346
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
347
        $partialRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Partials');
348
        $view->setLayoutRootPaths([$layoutRootPath]);
349
        $view->setPartialRootPaths([$partialRootPath]);
350
        $view->assign('formObject', $this->formObject);
351
        $view->assign('renderingResult', $renderingResult);
352
353
        return $view->render();
354
    }
355
356
    /**
357
     * Checks the type of the argument `object`, and returns it if everything is
358
     * ok.
359
     *
360
     * @return FormInterface|null
361
     * @throws InvalidOptionValueException
362
     */
363
    protected function getFormObjectArgument()
364
    {
365
        $objectArgument = $this->arguments['object'];
366
367
        if (null === $objectArgument) {
368
            return null;
369
        }
370
371
        if (false === is_object($objectArgument)) {
372
            throw InvalidOptionValueException::formViewHelperWrongFormValueType($objectArgument);
373
        }
374
375
        if (false === $objectArgument instanceof FormInterface) {
376
            throw InvalidOptionValueException::formViewHelperWrongFormValueObjectType($objectArgument);
377
        }
378
379
        $formClassName = $this->getFormClassName();
380
381
        if (false === $objectArgument instanceof $formClassName) {
382
            throw InvalidOptionValueException::formViewHelperWrongFormValueClassName($formClassName, $objectArgument);
383
        }
384
385
        return $objectArgument;
386
    }
387
388
    /**
389
     * Returns the class name of the form object: it is fetched from the action
390
     * of the controller which will be called when submitting this form. It
391
     * means two things:
392
     * - The action must have a parameter which has the exact same name as the
393
     *   form;
394
     * - The parameter must indicate its type.
395
     *
396
     * @return string
397
     * @throws ClassNotFoundException
398
     * @throws InvalidOptionValueException
399
     */
400
    protected function getFormClassName()
401
    {
402
        $formClassName = ($this->hasArgument('formClassName'))
403
            ? $this->arguments['formClassName']
404
            : $this->getFormClassNameFromControllerAction();
405
406
        if (false === class_exists($formClassName)) {
407
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
408
        }
409
410
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
411
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
412
        }
413
414
        return $formClassName;
415
    }
416
417
    /**
418
     * Will fetch the name of the controller action argument bound to this
419
     * request.
420
     *
421
     * @return string
422
     */
423
    protected function getFormClassNameFromControllerAction()
424
    {
425
        return $this->controllerService->getFormClassNameFromControllerAction(
426
            $this->getControllerName(),
427
            $this->getControllerActionName(),
428
            $this->getFormObjectName()
429
        );
430
    }
431
432
    /**
433
     * Renders the whole Fluid template.
434
     *
435
     * @param array $arguments
436
     * @return string
437
     */
438
    protected function getParentRenderResult(array $arguments)
439
    {
440
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
441
    }
442
443
    /**
444
     * @return string
445
     */
446
    protected function getControllerName()
447
    {
448
        return ($this->arguments['controller'])
449
            ?: $this->controllerContext
450
                ->getRequest()
451
                ->getControllerObjectName();
452
    }
453
454
    /**
455
     * @return string
456
     */
457
    protected function getControllerActionName()
458
    {
459
        return ($this->arguments['action'])
460
            ?: $this->controllerContext
461
                ->getRequest()
462
                ->getControllerActionName();
463
    }
464
465
    /**
466
     * @return AssetHandlerConnectorManager
467
     */
468
    protected function getAssetHandlerConnectorManager()
469
    {
470
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
471
    }
472
473
    /**
474
     * @return DataAttributesAssetHandler
475
     */
476
    protected function getDataAttributesAssetHandler()
477
    {
478
        /** @var DataAttributesAssetHandler $assetHandler */
479
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
480
481
        return $assetHandler;
482
    }
483
484
    /**
485
     * @return FormInterface
486
     */
487
    protected function getFormInstance()
488
    {
489
        /*
490
         * If the argument `object` was filled with an instance of Form, it
491
         * becomes the form instance. Otherwise we try to fetch an instance from
492
         * the form with errors list. If there is still no form, we create an
493
         * empty instance.
494
         */
495
        $objectArgument = $this->getFormObjectArgument();
496
497
        if ($objectArgument) {
498
            $form = $objectArgument;
499
        } else {
500
            $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...
501
502
            $form = $submittedForm ?: Core::get()->getObjectManager()->getEmptyObject($this->getFormClassName());
503
        }
504
505
        return $form;
506
    }
507
508
    /**
509
     * @param FormInterface $form
510
     * @return FormObject
511
     */
512
    protected function getFormObject(FormInterface $form)
513
    {
514
        return FormObjectFactory::get()->registerAndGetFormInstance($form, $this->getFormObjectName());
515
    }
516
517
    /**
518
     * @param FormViewHelperService $service
519
     */
520
    public function injectFormService(FormViewHelperService $service)
521
    {
522
        $this->formService = $service;
523
    }
524
525
    /**
526
     * @param ControllerService $controllerService
527
     */
528
    public function injectControllerService(ControllerService $controllerService)
529
    {
530
        $this->controllerService = $controllerService;
531
    }
532
}
533