Completed
Push — middleware ( 9c572f...78eddc )
by Romain
02:34
created

FormViewHelper::getFormValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
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\Behaviours\BehavioursManager;
20
use Romm\Formz\Core\Core;
21
use Romm\Formz\Exceptions\ClassNotFoundException;
22
use Romm\Formz\Exceptions\EntryNotFoundException;
23
use Romm\Formz\Exceptions\InvalidOptionValueException;
24
use Romm\Formz\Form\FormInterface;
25
use Romm\Formz\Form\FormObject;
26
use Romm\Formz\Form\FormObjectFactory;
27
use Romm\Formz\Service\ContextService;
28
use Romm\Formz\Service\ControllerService;
29
use Romm\Formz\Service\ExtensionService;
30
use Romm\Formz\Service\StringService;
31
use Romm\Formz\Service\TimeTrackerService;
32
use Romm\Formz\Service\ViewHelper\FormViewHelperService;
33
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
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\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 "formz-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->formObjectClassName = $this->getFormClassName();
130
            $this->formObject = $this->getFormObject();
131
            $this->formService->setFormObject($this->formObject);
132
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
133
134
            /*
135
             * If the argument `object` was filled with an instance of Form, it
136
             * is added to the `FormObject`.
137
             */
138
            $objectArgument = $this->arguments['object'];
139
140
            if ($objectArgument instanceof FormInterface
141
                && false === $this->formObject->formWasSubmitted()
142
            ) {
143
                $this->formObject->setForm($objectArgument);
144
            }
145
        }
146
147
        /*
148
         * Important: we need to instantiate the page renderer with this instead
149
         * of Extbase object manager (or with an inject function).
150
         *
151
         * This is due to some TYPO3 low level behaviour which overrides the
152
         * page renderer singleton instance, whenever a new request is used. The
153
         * problem is that the instance is not updated on Extbase side.
154
         *
155
         * Using Extbase injection can lead to old page renderer instance being
156
         * used, resulting in a leak of assets inclusion, and maybe more issues.
157
         */
158
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
159
    }
160
161
    /**
162
     * @inheritdoc
163
     */
164
    public function initializeArguments()
165
    {
166
        parent::initializeArguments();
167
168
        // The name attribute becomes mandatory.
169
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
170
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
171
    }
172
173
    /**
174
     * @return string
175
     */
176
    protected function renderViewHelper()
177
    {
178
        $this->timeTracker = TimeTrackerService::getAndStart();
179
180
        if (false === $this->typoScriptIncluded) {
181
            return (ExtensionService::get()->isInDebugMode())
182
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
183
                : '';
184
        }
185
186
        $formzValidationResult = $this->formObject->getConfigurationValidationResult();
187
        $this->timeTracker->logTime('post-config');
188
189
        $result = ($formzValidationResult->hasErrors())
190
            // If the form configuration is not valid, we display the errors list.
191
            ? $this->getErrorText($formzValidationResult)
192
            // Everything is ok, we render the form.
193
            : $this->renderForm(func_get_args());
194
195
        $this->timeTracker->logTime('final');
196
197
        if (ExtensionService::get()->isInDebugMode()) {
198
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
199
        }
200
201
        $this->formService->resetState();
202
203
        return $result;
204
    }
205
206
    /**
207
     * Will render the whole form and return the HTML result.
208
     *
209
     * @param array $arguments
210
     * @return string
211
     */
212
    final protected function renderForm(array $arguments)
213
    {
214
        /*
215
         * We begin by setting up the form service: request results and form
216
         * instance are inserted in the service, and are used afterwards.
217
         *
218
         * There are only two ways to be sure the values injected are correct:
219
         * when the form was actually submitted by the user, or when the
220
         * argument `object` of the view helper is filled with a form instance.
221
         */
222
        $this->formService->activateFormContext();
223
224
        /*
225
         * Adding the default class configured in TypoScript configuration to
226
         * the form HTML tag.
227
         */
228
        $this->addDefaultClass();
229
230
        /*
231
         * If the form was submitted, applying custom behaviours on its fields.
232
         */
233
        $this->applyBehavioursOnSubmittedForm();
234
235
        /*
236
         * Handling data attributes that are added to the form HTML tag,
237
         * depending on several parameters.
238
         */
239
        $this->handleDataAttributes();
240
241
        /*
242
         * Including JavaScript and CSS assets in the page renderer.
243
         */
244
        $this->handleAssets();
245
246
        $this->timeTracker->logTime('pre-render');
247
248
        /*
249
         * Getting the result of the original Fluid `FormViewHelper` rendering.
250
         */
251
        $result = $this->getParentRenderResult($arguments);
252
253
        /*
254
         * Language files need to be included at the end, because they depend on
255
         * what was used by previous assets.
256
         */
257
        $this->getAssetHandlerConnectorManager()
258
            ->getJavaScriptAssetHandlerConnector()
259
            ->includeLanguageJavaScriptFiles();
260
261
        return $result;
262
    }
263
264
    /**
265
     * @todo
266
     *
267
     * @return string
268
     */
269
    protected function renderHiddenReferrerFields()
270
    {
271
        $result = parent::renderHiddenReferrerFields();
272
273
        $data = [];
274
275
        $result .= '<input type="hidden" name="' . $this->prefixFieldName('formz') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($data)))) . '" />' . LF;
276
277
        return $result;
278
    }
279
280
    /**
281
     * Will loop on the submitted form fields and apply behaviours if their
282
     * configuration contains.
283
     */
284
    protected function applyBehavioursOnSubmittedForm()
285
    {
286
        if ($this->formObject->formWasSubmitted()) {
287
            /** @var BehavioursManager $behavioursManager */
288
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
289
290
            $originalRequest = $this->controllerContext->getRequest()->getOriginalRequest();
291
            /** @var array $originalForm */
292
            $originalForm = $originalRequest->getArgument($this->getFormObjectName());
293
294
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
295
                $originalForm,
296
                $this->formObject->getConfiguration()
297
            );
298
299
            $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
300
        }
301
    }
302
303
    /**
304
     * Will add a default class to the form element.
305
     *
306
     * To customize the class, take a look at `settings.defaultClass` in the
307
     * form TypoScript configuration.
308
     */
309
    protected function addDefaultClass()
310
    {
311
        $formDefaultClass = $this->formObject
312
            ->getConfiguration()
313
            ->getSettings()
314
            ->getDefaultClass();
315
316
        $class = $this->tag->getAttribute('class');
317
318
        if (false === empty($formDefaultClass)) {
319
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
320
            $this->tag->addAttribute('class', $class);
321
        }
322
    }
323
324
    /**
325
     * Adds custom data attributes to the form element, based on the
326
     * submitted form values and results.
327
     */
328
    protected function handleDataAttributes()
329
    {
330
        $dataAttributes = [];
331
332
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
333
334
        if ($this->formObject->hasForm()) {
335
            if (false === $this->formObject->hasFormResult()) {
336
                $form = $this->formObject->getForm();
337
                $formValidator = $this->getFormValidator($this->getFormObjectName());
338
                $formResult = $formValidator->validateGhost($form);
339
            } else {
340
                $formResult = $this->formObject->getFormResult();
341
            }
342
343
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($formResult);
344
        }
345
346
        if (true === $this->formObject->formWasSubmitted()) {
347
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
348
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes();
349
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes();
350
        }
351
352
        $this->tag->addAttributes($dataAttributes);
353
    }
354
355
    /**
356
     * Will include all JavaScript and CSS assets needed for this form.
357
     */
358
    protected function handleAssets()
359
    {
360
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
361
362
        // Default Formz assets.
363
        $assetHandlerConnectorManager->includeDefaultAssets();
364
365
        // JavaScript assets.
366
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
367
            ->generateAndIncludeFormzConfigurationJavaScript()
368
            ->generateAndIncludeJavaScript()
369
            ->generateAndIncludeInlineJavaScript()
370
            ->includeJavaScriptValidationAndConditionFiles();
371
372
        // CSS assets.
373
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
374
            ->includeGeneratedCss();
375
    }
376
377
    /**
378
     * Will return an error text from a Fluid view.
379
     *
380
     * @param Result $result
381
     * @return string
382
     */
383
    protected function getErrorText(Result $result)
384
    {
385
        /** @var $view StandaloneView */
386
        $view = Core::instantiate(StandaloneView::class);
387
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
388
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
389
        $view->setLayoutRootPaths([$layoutRootPath]);
390
        $view->assign('result', $result);
391
392
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
393
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
394
395
        return $view->render();
396
    }
397
398
    /**
399
     * Returns the class name of the form object: it is fetched from the action
400
     * of the controller which will be called when submitting this form. It
401
     * means two things:
402
     * - The action must have a parameter which has the exact same name as the
403
     *   form;
404
     * - The parameter must indicate its type.
405
     *
406
     * @return string
407
     * @throws ClassNotFoundException
408
     * @throws InvalidOptionValueException
409
     */
410
    protected function getFormClassName()
411
    {
412
        $formClassName = ($this->hasArgument('formClassName'))
413
            ? $this->arguments['formClassName']
414
            : $this->getFormClassNameFromControllerAction();
415
416
        if (false === class_exists($formClassName)) {
417
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
418
        }
419
420
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
421
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
422
        }
423
424
        return $formClassName;
425
    }
426
427
    /**
428
     * Will fetch the name of the controller action argument bound to this
429
     * request.
430
     *
431
     * @return string
432
     * @throws EntryNotFoundException
433
     */
434
    protected function getFormClassNameFromControllerAction()
435
    {
436
        return $this->controllerService->getFormClassNameFromControllerAction(
437
            $this->getControllerName(),
438
            $this->getControllerActionName(),
439
            $this->getFormObjectName()
440
        );
441
    }
442
443
    /**
444
     * Renders the whole Fluid template.
445
     *
446
     * @param array $arguments
447
     * @return string
448
     */
449
    protected function getParentRenderResult(array $arguments)
450
    {
451
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
452
    }
453
454
    /**
455
     * @return string
456
     */
457
    protected function getControllerName()
458
    {
459
        return ($this->arguments['controller'])
460
            ?: $this->controllerContext
461
                ->getRequest()
462
                ->getControllerObjectName();
463
    }
464
465
    /**
466
     * @return string
467
     */
468
    protected function getControllerActionName()
469
    {
470
        return ($this->arguments['action'])
471
            ?: $this->controllerContext
472
                ->getRequest()
473
                ->getControllerActionName();
474
    }
475
476
    /**
477
     * @param string $formName
478
     * @return DefaultFormValidator
479
     */
480
    protected function getFormValidator($formName)
481
    {
482
        /** @var DefaultFormValidator $validation */
483
        $validation = Core::instantiate(DefaultFormValidator::class, ['name' => $formName]);
484
485
        return $validation;
486
    }
487
488
    /**
489
     * @return AssetHandlerConnectorManager
490
     */
491
    protected function getAssetHandlerConnectorManager()
492
    {
493
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
494
    }
495
496
    /**
497
     * @return DataAttributesAssetHandler
498
     */
499
    protected function getDataAttributesAssetHandler()
500
    {
501
        /** @var DataAttributesAssetHandler $assetHandler */
502
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
503
504
        return $assetHandler;
505
    }
506
507
    /**
508
     * @return FormObject
509
     */
510
    protected function getFormObject()
511
    {
512
        /** @var FormObjectFactory $formObjectFactory */
513
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
514
515
        return $formObjectFactory->getInstanceFromClassName($this->formObjectClassName, $this->getFormObjectName());
516
    }
517
518
    /**
519
     * @param FormViewHelperService $service
520
     */
521
    public function injectFormService(FormViewHelperService $service)
522
    {
523
        $this->formService = $service;
524
    }
525
526
    /**
527
     * @param ControllerService $controllerService
528
     */
529
    public function injectControllerService(ControllerService $controllerService)
530
    {
531
        $this->controllerService = $controllerService;
532
    }
533
}
534