Completed
Push — master ( 5be95e...69d0c2 )
by Romain
01:38 queued 01:34
created

FormViewHelper::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
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\Behaviours\BehavioursManager;
20
use Romm\Formz\Configuration\Configuration;
21
use Romm\Formz\Core\Core;
22
use Romm\Formz\Form\FormInterface;
23
use Romm\Formz\Form\FormObject;
24
use Romm\Formz\Utility\TimeTracker;
25
use Romm\Formz\Validation\Validator\Form\AbstractFormValidator;
26
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
27
use TYPO3\CMS\Core\Page\PageRenderer;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Extbase\Error\Result;
30
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
31
use TYPO3\CMS\Fluid\View\StandaloneView;
32
33
/**
34
 * This view helper overrides the default one from Extbase, to include
35
 * everything the extension needs to work properly.
36
 *
37
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
38
 * and must be the exact same name as the form parameter in the controller
39
 * action called when the form is submitted. For instance, if your action looks
40
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
41
 * then the "name" attribute of this view helper must be "exampleForm".
42
 *
43
 * Thanks to the information of the form, the following things are automatically
44
 * handled in this view helper:
45
 *
46
 * - Class
47
 *   A custom class may be added to the form DOM element. If the TypoScript
48
 *   configuration "settings.defaultClass" is set for this form, then the given
49
 *   class will be added to the form element.
50
 *
51
 * - JavaScript
52
 *   A block of JavaScript is built from scratch, which will initialize the
53
 *   form, add validation rules to the fields, and handle activation of the
54
 *   fields validation.
55
 *
56
 * - Data attributes
57
 *   To help integrators customize every aspect they need in CSS, every useful
58
 *   information is put in data attributes in the form DOM element. For example,
59
 *   you can know in real time if the field "email" is valid if the form has the
60
 *   attribute "formz-valid-email"
61
 *
62
 * - CSS
63
 *   A block of CSS is built from scratch, which will handle the fields display,
64
 *   depending on their activation property.
65
 */
66
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
67
{
68
    const FORM_VIEW_HELPER = 'FormViewHelper';
69
    const FORM_INSTANCE = 'FormInstance';
70
    const FORM_RESULT = 'FormResult';
71
    const FORM_WAS_SUBMITTED = 'FormWasSubmitted';
72
73
    /**
74
     * @var PageRenderer
75
     */
76
    protected $pageRenderer;
77
78
    /**
79
     * @var string
80
     */
81
    protected $formObjectClassName;
82
83
    /**
84
     * @var Configuration
85
     */
86
    protected $formzConfiguration;
87
88
    /**
89
     * @var FormObject
90
     */
91
    protected $formObject;
92
93
    /**
94
     * @var AssetHandlerFactory
95
     */
96
    protected $assetHandlerFactory;
97
98
    /**
99
     * @var TimeTracker
100
     */
101
    protected $timeTracker;
102
103
    /**
104
     * @var array
105
     */
106
    protected static $staticVariables = [];
107
108
    /**
109
     * @inheritdoc
110
     */
111
    public function initialize()
112
    {
113
        parent::initialize();
114
115
        /*
116
         * Important: we need to instantiate the page renderer with this instead
117
         * of Extbase object manager (or with an inject function).
118
         *
119
         * This is due to some TYPO3 low level behaviour which overrides the
120
         * page renderer singleton instance, whenever a new request is used. The
121
         * problem is that the instance is not updated on Extbase side.
122
         *
123
         * Using Extbase injection can lead to old page renderer instance being
124
         * used, resulting in a leak of assets inclusion, and maybe more issues.
125
         */
126
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
127
    }
128
129
    /**
130
     * @inheritdoc
131
     */
132
    public function initializeArguments()
133
    {
134
        parent::initializeArguments();
135
136
        // The name attribute becomes mandatory.
137
        $this->overrideArgument('name', 'string', 'Name of the form', true);
138
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
139
    }
140
141
    /**
142
     * Render the form.
143
     *
144
     * @param string  $action                               Target action
145
     * @param array   $arguments                            Arguments
146
     * @param string  $controller                           Target controller
147
     * @param string  $extensionName                        Target Extension Name (without "tx_" prefix and no underscores). If NULL the current extension name is used
148
     * @param string  $pluginName                           Target plugin. If empty, the current plugin name is used
149
     * @param int $pageUid                              Target page uid
150
     * @param mixed   $object                               Object to use for the form. Use in conjunction with the "property" attribute on the sub tags
151
     * @param int $pageType                             Target page type
152
     * @param bool $noCache                              set this to disable caching for the target page. You should not need this.
153
     * @param bool $noCacheHash                          set this to suppress the cHash query parameter created by TypoLink. You should not need this.
154
     * @param string  $section                              The anchor to be added to the action URI (only active if $actionUri is not set)
155
     * @param string  $format                               The requested format (e.g. ".html") of the target page (only active if $actionUri is not set)
156
     * @param array   $additionalParams                     additional action URI query parameters that won't be prefixed like $arguments (overrule $arguments) (only active if $actionUri is not set)
157
     * @param bool $absolute                             If set, an absolute action URI is rendered (only active if $actionUri is not set)
158
     * @param bool $addQueryString                       If set, the current query parameters will be kept in the action URI (only active if $actionUri is not set)
159
     * @param array   $argumentsToBeExcludedFromQueryString arguments to be removed from the action URI. Only active if $addQueryString = TRUE and $actionUri is not set
160
     * @param string  $fieldNamePrefix                      Prefix that will be added to all field names within this form. If not set the prefix will be tx_yourExtension_plugin
161
     * @param string  $actionUri                            can be used to overwrite the "action" attribute of the form tag
162
     * @param string  $objectName                           name of the object that is bound to this form. If this argument is not specified, the name attribute of this form is used to determine the FormObjectName
163
     * @param string  $hiddenFieldClassName
164
     * @return string rendered form
165
     * @throws \Exception
166
     */
167
    public function render($action = null, array $arguments = [], $controller = null, $extensionName = null, $pluginName = null, $pageUid = null, $object = null, $pageType = 0, $noCache = false, $noCacheHash = false, $section = '', $format = '', array $additionalParams = [], $absolute = false, $addQueryString = false, array $argumentsToBeExcludedFromQueryString = [], $fieldNamePrefix = null, $actionUri = null, $objectName = null, $hiddenFieldClassName = null)
168
    {
169
        $this->timeTracker = TimeTracker::getAndStart();
170
        $result = '';
171
172
        if (false === Core::get()->isTypoScriptIncluded()) {
173
            if (Core::get()->isInDebugMode()) {
174
                $result = Core::get()->translate('form.typoscript_not_included.error_message');
175
            }
176
        } else {
177
            $this->formObject = Core::get()->getFormObjectFactory()
178
                ->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
179
180
            $formzValidationResult = $this->formObject->getConfigurationValidationResult();
181
182
            if ($formzValidationResult->hasErrors()) {
183
                // If the form configuration is not valid, we display the errors list.
184
                $result = $this->getErrorText($formzValidationResult);
185
            } else {
186
                // Everything is ok, we render the form.
187
                $result = $this->renderForm();
188
            }
189
190
            unset($formzValidationResult);
191
        }
192
193
        $this->timeTracker->logTime('final');
194
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
195
        unset($this->timeTracker);
196
197
        return $result;
198
    }
199
200
    /**
201
     * Will render the whole form and return the HTML result.
202
     *
203
     * @return string
204
     */
205
    protected function renderForm()
206
    {
207
        $this->formzConfiguration = Core::get()->getConfigurationFactory()
208
            ->getFormzConfiguration()
209
            ->getObject();
210
211
        $this->timeTracker->logTime('post-config');
212
213
        $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
214
215
        $this->injectFormInstance()
216
            ->injectObjectAndRequestResult()
217
            ->applyBehavioursOnSubmittedForm()
218
            ->addDefaultClass()
219
            ->handleDataAttributes();
220
221
        $assetHandlerConnectorManager = AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
222
        $assetHandlerConnectorManager->includeDefaultAssets();
223
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
224
            ->generateAndIncludeFormzConfigurationJavaScript()
225
            ->generateAndIncludeJavaScript()
226
            ->generateAndIncludeInlineJavaScript()
227
            ->includeJavaScriptValidationAndConditionFiles();
228
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()->includeGeneratedCss();
229
230
        $this->timeTracker->logTime('pre-render');
231
232
        // Renders the whole Fluid template.
233
        $result = call_user_func_array([$this, 'parent::render'], func_get_args());
234
235
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()->includeLanguageJavaScriptFiles();
236
237
        $this->resetVariables();
238
239
        return $result;
240
    }
241
242
    /**
243
     * Stores this class instance in the variable container for further usage.
244
     *
245
     * @throws \Exception
246
     * @return $this
247
     */
248
    protected function injectFormInstance()
249
    {
250
        if (true === isset(self::$staticVariables[self::FORM_VIEW_HELPER])) {
251
            throw new \Exception('You can not use a form view helper inside another one.', 1465242575);
252
        }
253
254
        self::$staticVariables[self::FORM_VIEW_HELPER] = $this;
255
256
        return $this;
257
    }
258
259
    /**
260
     * This function will inject in the variable container the instance of form
261
     * and its submission result. There are only two ways to be sure the values
262
     * injected are correct: when the form has actually been submitted by the
263
     * user, or when the view helper argument `object` is filled.
264
     *
265
     * @return $this
266
     */
267
    protected function injectObjectAndRequestResult()
268
    {
269
        if (false === isset(self::$staticVariables[self::FORM_INSTANCE])
270
            || false === isset(self::$staticVariables[self::FORM_RESULT])
271
        ) {
272
            $formInstance = false;
273
            $formRequestResult = false;
274
275
            $originalRequest = $this->controllerContext
276
                ->getRequest()
277
                ->getOriginalRequest();
278
279
            if (null !== $originalRequest
280
                && $originalRequest->hasArgument($this->getFormObjectName())
281
            ) {
282
                $formInstance = $originalRequest->getArgument($this->getFormObjectName());
283
                $formRequestResult = AbstractFormValidator::getFormValidationResult(
284
                    $this->getFormObjectClassName(),
285
                    $this->getFormObjectName()
286
                );
287
288
                self::$staticVariables[self::FORM_WAS_SUBMITTED] = true;
289
            } elseif (null !== $this->arguments['object']) {
290
                $formInstance = $this->arguments['object'];
291
                /*
292
                 * @todo: pas forcément un DefaultFormValidator: comment je gère ça?
293
                 * + ça prend quand même un peu de temps cette manière. Peut-on faire autrement ?
294
                 */
295
                /** @var DefaultFormValidator $formValidator */
296
                $formValidator = GeneralUtility::makeInstance(
297
                    DefaultFormValidator::class,
298
                    ['name' => $this->getFormObjectName()]
299
                );
300
                $formRequestResult = $formValidator->validate($formInstance);
301
            }
302
303
            self::$staticVariables[self::FORM_INSTANCE] = $formInstance;
304
            self::$staticVariables[self::FORM_RESULT] = $formRequestResult;
305
        }
306
307
        return $this;
308
    }
309
310
    /**
311
     * @param string $name
312
     * @return mixed|null
313
     */
314
    public static function getVariable($name)
315
    {
316
        return (isset(self::$staticVariables[$name]))
317
            ? self::$staticVariables[$name]
318
            : null;
319
    }
320
321
    /**
322
     * Deletes the values stored in the variable container.
323
     *
324
     * @return $this
325
     */
326
    protected function resetVariables()
327
    {
328
        unset(self::$staticVariables[self::FORM_VIEW_HELPER]);
329
        unset(self::$staticVariables[self::FORM_INSTANCE]);
330
        unset(self::$staticVariables[self::FORM_RESULT]);
331
        self::$staticVariables[self::FORM_WAS_SUBMITTED] = false;
332
333
        return $this;
334
    }
335
336
    /**
337
     * Will loop on the submitted form fields and apply behaviours if their
338
     * configuration contains.
339
     *
340
     * @return $this
341
     */
342
    protected function applyBehavioursOnSubmittedForm()
343
    {
344
        $originalRequest = $this->controllerContext->getRequest()->getOriginalRequest();
345
        if (null !== $originalRequest) {
346
            if ($originalRequest->hasArgument($this->getFormObjectName())) {
347
                /** @var BehavioursManager $behavioursManager */
348
                $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
349
350
                /** @var array $formProperties */
351
                $formProperties = $originalRequest->getArgument($this->getFormObjectName());
352
                $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray($formProperties, $this->formObject->getConfiguration());
353
                $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
354
            }
355
        }
356
357
        return $this;
358
    }
359
360
    /**
361
     * Will add a default class to the form element.
362
     *
363
     * To customize the class, take a look at `settings.defaultClass` in the
364
     * form TypoScript configuration.
365
     *
366
     * @return $this
367
     */
368
    protected function addDefaultClass()
369
    {
370
        $class = $this->tag->getAttribute('class');
371
        $formDefaultClass = $this->formObject->getConfiguration()->getSettings()->getDefaultClass();
372
        $class = $class . ((!empty($class)) ? ' ' : '') . $formDefaultClass;
373
        $this->tag->addAttribute('class', $class);
374
375
        return $this;
376
    }
377
378
    /**
379
     * Adds custom data attributes to the form element, based on the
380
     * submitted form values and results.
381
     *
382
     * @return $this
383
     */
384
    protected function handleDataAttributes()
385
    {
386
        $object = self::$staticVariables[self::FORM_INSTANCE];
387
        $requestResult = self::$staticVariables[self::FORM_RESULT];
388
389
        /** @var DataAttributesAssetHandler $dataAttributesAssetHandler */
390
        $dataAttributesAssetHandler =  $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
391
392
        $dataAttributes = [];
393
        if (false !== $object) {
394
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $requestResult);
395
        }
396
397
        if (false !== $requestResult) {
398
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($requestResult);
399
400
            if (true === self::$staticVariables[self::FORM_WAS_SUBMITTED]) {
401
                $dataAttributes += ['formz-submission-done' => '1'];
402
                $dataAttributes += $dataAttributesAssetHandler->getFieldsErrorsDataAttributes($requestResult);
403
            }
404
        }
405
406
        foreach ($dataAttributes as $attributeName => $attributeValue) {
407
            $this->tag->addAttribute($attributeName, $attributeValue);
408
        }
409
410
        return $this;
411
    }
412
413
    /**
414
     * Will return an error text from a Fluid view.
415
     *
416
     * @param Result $result
417
     * @return string
418
     */
419
    protected function getErrorText(Result $result)
420
    {
421
        /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
422
        $view = $this->objectManager->get(StandaloneView::class);
423
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
424
        $layoutRootPath = Core::get()->getExtensionRelativePath('Resources/Private/Layouts');
425
        $view->setLayoutRootPaths([$layoutRootPath]);
426
        $view->assign('result', $result);
427
428
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
429
        $this->pageRenderer->addCssFile(Core::get()->getResourceRelativePath($templatePath));
430
431
        return $view->render();
432
    }
433
434
    /**
435
     * Returns the class name of the form object: it is fetched from the action
436
     * of the controller which will be called when submitting this form. It
437
     * means two things:
438
     * - The action must have a parameter which has the exact same name as the
439
     *   form.
440
     * - The parameter must indicate its type.
441
     *
442
     * @return null|string
443
     * @throws \Exception
444
     */
445
    protected function getFormObjectClassName()
446
    {
447
        if (null === $this->formObjectClassName) {
448
            $request = $this->controllerContext->getRequest();
449
            $controllerObjectName = $request->getControllerObjectName();
450
            $actionName = ($this->arguments['action']) ?: $request->getControllerActionName();
451
            $actionName = $actionName . 'Action';
452
453
            if ($this->hasArgument('formClassName')) {
454
                $formClassName = $this->arguments['formClassName'];
455
            } else {
456
                /** @var ReflectionService $reflectionService */
457
                $reflectionService = $this->objectManager->get(ReflectionService::class);
458
                $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
459
460
                if (false === isset($methodParameters[$this->getFormObjectName()])) {
461
                    throw new \Exception(
462
                        'The method "' . $controllerObjectName . '::' . $actionName . '()" must have a parameter "$' . $this->getFormObjectName() . '". Note that you can also change the parameter "name" of the form view helper.',
463
                        1457441846
464
                    );
465
                }
466
467
                $formClassName = $methodParameters[$this->getFormObjectName()]['type'];
468
            }
469
470
            if (false === class_exists($formClassName)) {
471
                throw new \Exception(
472
                    'Invalid value for the form class name (current value: "' . $formClassName . '"). You need to either fill the parameter "formClassName" in the view helper, or specify the type of the parameter "$' . $this->getFormObjectName() . '" for the method "' . $controllerObjectName . '::' . $actionName . '()".',
473
                    1457442014
474
                );
475
            }
476
477
            if (false === in_array(FormInterface::class, class_implements($formClassName))) {
478
                throw new \Exception(
479
                    'Invalid value for the form class name (current value: "' . $formClassName . '"); it must be an instance of "' . FormInterface::class . '".',
480
                    1457442462
481
                );
482
            }
483
484
            $this->formObjectClassName = $formClassName;
485
        }
486
487
        return $this->formObjectClassName;
488
    }
489
490
    /**
491
     * @return Configuration
492
     */
493
    public function getFormzConfiguration()
494
    {
495
        return $this->formzConfiguration;
496
    }
497
498
    /**
499
     * @return FormObject
500
     */
501
    public function getFormObject()
502
    {
503
        return $this->formObject;
504
    }
505
}
506