Completed
Push — master ( e90fe8...3395f4 )
by Romain
14s
created

FormViewHelper::renderViewHelper()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 19
nc 4
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
     * @return string
143
     */
144
    protected function renderViewHelper()
145
    {
146
        $this->timeTracker = TimeTracker::getAndStart();
147
        $result = '';
148
149
        if (false === Core::get()->isTypoScriptIncluded()) {
150
            if (Core::get()->isInDebugMode()) {
151
                $result = Core::get()->translate('form.typoscript_not_included.error_message');
152
            }
153
        } else {
154
            $this->formObject = Core::get()->getFormObjectFactory()
155
                ->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
156
157
            $formzValidationResult = $this->formObject->getConfigurationValidationResult();
158
159
            if ($formzValidationResult->hasErrors()) {
160
                // If the form configuration is not valid, we display the errors list.
161
                $result = $this->getErrorText($formzValidationResult);
162
            } else {
163
                // Everything is ok, we render the form.
164
                $result = $this->renderForm();
165
            }
166
167
            unset($formzValidationResult);
168
        }
169
170
        $this->timeTracker->logTime('final');
171
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
172
        unset($this->timeTracker);
173
174
        return $result;
175
    }
176
177
    /**
178
     * Will render the whole form and return the HTML result.
179
     *
180
     * @return string
181
     */
182
    protected function renderForm()
183
    {
184
        $this->formzConfiguration = Core::get()->getConfigurationFactory()
185
            ->getFormzConfiguration()
186
            ->getObject();
187
188
        $this->timeTracker->logTime('post-config');
189
190
        $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
191
192
        $this->injectFormInstance()
193
            ->injectObjectAndRequestResult()
194
            ->applyBehavioursOnSubmittedForm()
195
            ->addDefaultClass()
196
            ->handleDataAttributes();
197
198
        $assetHandlerConnectorManager = AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
199
        $assetHandlerConnectorManager->includeDefaultAssets();
200
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
201
            ->generateAndIncludeFormzConfigurationJavaScript()
202
            ->generateAndIncludeJavaScript()
203
            ->generateAndIncludeInlineJavaScript()
204
            ->includeJavaScriptValidationAndConditionFiles();
205
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()->includeGeneratedCss();
206
207
        $this->timeTracker->logTime('pre-render');
208
209
        // Renders the whole Fluid template.
210
        $result = call_user_func_array([$this, 'parent::render'], func_get_args());
211
212
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()->includeLanguageJavaScriptFiles();
213
214
        $this->resetVariables();
215
216
        return $result;
217
    }
218
219
    /**
220
     * Stores this class instance in the variable container for further usage.
221
     *
222
     * @throws \Exception
223
     * @return $this
224
     */
225
    protected function injectFormInstance()
226
    {
227
        if (true === isset(self::$staticVariables[self::FORM_VIEW_HELPER])) {
228
            throw new \Exception('You can not use a form view helper inside another one.', 1465242575);
229
        }
230
231
        self::$staticVariables[self::FORM_VIEW_HELPER] = $this;
232
233
        return $this;
234
    }
235
236
    /**
237
     * This function will inject in the variable container the instance of form
238
     * and its submission result. There are only two ways to be sure the values
239
     * injected are correct: when the form has actually been submitted by the
240
     * user, or when the view helper argument `object` is filled.
241
     *
242
     * @return $this
243
     */
244
    protected function injectObjectAndRequestResult()
245
    {
246
        if (false === isset(self::$staticVariables[self::FORM_INSTANCE])
247
            || false === isset(self::$staticVariables[self::FORM_RESULT])
248
        ) {
249
            $formInstance = false;
250
            $formRequestResult = false;
251
252
            $originalRequest = $this->controllerContext
253
                ->getRequest()
254
                ->getOriginalRequest();
255
256
            if (null !== $originalRequest
257
                && $originalRequest->hasArgument($this->getFormObjectName())
258
            ) {
259
                $formInstance = $originalRequest->getArgument($this->getFormObjectName());
260
                $formRequestResult = AbstractFormValidator::getFormValidationResult(
261
                    $this->getFormObjectClassName(),
262
                    $this->getFormObjectName()
263
                );
264
265
                self::$staticVariables[self::FORM_WAS_SUBMITTED] = true;
266
            } elseif (null !== $this->arguments['object']) {
267
                $formInstance = $this->arguments['object'];
268
                /*
269
                 * @todo: pas forcément un DefaultFormValidator: comment je gère ça?
270
                 * + ça prend quand même un peu de temps cette manière. Peut-on faire autrement ?
271
                 */
272
                /** @var DefaultFormValidator $formValidator */
273
                $formValidator = GeneralUtility::makeInstance(
274
                    DefaultFormValidator::class,
275
                    ['name' => $this->getFormObjectName()]
276
                );
277
                $formRequestResult = $formValidator->validate($formInstance);
278
            }
279
280
            self::$staticVariables[self::FORM_INSTANCE] = $formInstance;
281
            self::$staticVariables[self::FORM_RESULT] = $formRequestResult;
282
        }
283
284
        return $this;
285
    }
286
287
    /**
288
     * @param string $name
289
     * @return mixed|null
290
     */
291
    public static function getVariable($name)
292
    {
293
        return (isset(self::$staticVariables[$name]))
294
            ? self::$staticVariables[$name]
295
            : null;
296
    }
297
298
    /**
299
     * Deletes the values stored in the variable container.
300
     *
301
     * @return $this
302
     */
303
    protected function resetVariables()
304
    {
305
        unset(self::$staticVariables[self::FORM_VIEW_HELPER]);
306
        unset(self::$staticVariables[self::FORM_INSTANCE]);
307
        unset(self::$staticVariables[self::FORM_RESULT]);
308
        self::$staticVariables[self::FORM_WAS_SUBMITTED] = false;
309
310
        return $this;
311
    }
312
313
    /**
314
     * Will loop on the submitted form fields and apply behaviours if their
315
     * configuration contains.
316
     *
317
     * @return $this
318
     */
319
    protected function applyBehavioursOnSubmittedForm()
320
    {
321
        $originalRequest = $this->controllerContext->getRequest()->getOriginalRequest();
322
        if (null !== $originalRequest) {
323
            if ($originalRequest->hasArgument($this->getFormObjectName())) {
324
                /** @var BehavioursManager $behavioursManager */
325
                $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
326
327
                /** @var array $formProperties */
328
                $formProperties = $originalRequest->getArgument($this->getFormObjectName());
329
                $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray($formProperties, $this->formObject->getConfiguration());
330
                $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
331
            }
332
        }
333
334
        return $this;
335
    }
336
337
    /**
338
     * Will add a default class to the form element.
339
     *
340
     * To customize the class, take a look at `settings.defaultClass` in the
341
     * form TypoScript configuration.
342
     *
343
     * @return $this
344
     */
345
    protected function addDefaultClass()
346
    {
347
        $class = $this->tag->getAttribute('class');
348
        $formDefaultClass = $this->formObject->getConfiguration()->getSettings()->getDefaultClass();
349
        $class = $class . ((!empty($class)) ? ' ' : '') . $formDefaultClass;
350
        $this->tag->addAttribute('class', $class);
351
352
        return $this;
353
    }
354
355
    /**
356
     * Adds custom data attributes to the form element, based on the
357
     * submitted form values and results.
358
     *
359
     * @return $this
360
     */
361
    protected function handleDataAttributes()
362
    {
363
        $object = self::$staticVariables[self::FORM_INSTANCE];
364
        $requestResult = self::$staticVariables[self::FORM_RESULT];
365
366
        /** @var DataAttributesAssetHandler $dataAttributesAssetHandler */
367
        $dataAttributesAssetHandler =  $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
368
369
        $dataAttributes = [];
370
        if (false !== $object) {
371
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $requestResult);
372
        }
373
374
        if (false !== $requestResult) {
375
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($requestResult);
376
377
            if (true === self::$staticVariables[self::FORM_WAS_SUBMITTED]) {
378
                $dataAttributes += ['formz-submission-done' => '1'];
379
                $dataAttributes += $dataAttributesAssetHandler->getFieldsErrorsDataAttributes($requestResult);
380
            }
381
        }
382
383
        foreach ($dataAttributes as $attributeName => $attributeValue) {
384
            $this->tag->addAttribute($attributeName, $attributeValue);
385
        }
386
387
        return $this;
388
    }
389
390
    /**
391
     * Will return an error text from a Fluid view.
392
     *
393
     * @param Result $result
394
     * @return string
395
     */
396
    protected function getErrorText(Result $result)
397
    {
398
        /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
399
        $view = $this->objectManager->get(StandaloneView::class);
400
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
401
        $layoutRootPath = Core::get()->getExtensionRelativePath('Resources/Private/Layouts');
402
        $view->setLayoutRootPaths([$layoutRootPath]);
403
        $view->assign('result', $result);
404
405
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
406
        $this->pageRenderer->addCssFile(Core::get()->getResourceRelativePath($templatePath));
407
408
        return $view->render();
409
    }
410
411
    /**
412
     * Returns the class name of the form object: it is fetched from the action
413
     * of the controller which will be called when submitting this form. It
414
     * means two things:
415
     * - The action must have a parameter which has the exact same name as the
416
     *   form.
417
     * - The parameter must indicate its type.
418
     *
419
     * @return null|string
420
     * @throws \Exception
421
     */
422
    protected function getFormObjectClassName()
423
    {
424
        if (null === $this->formObjectClassName) {
425
            $request = $this->controllerContext->getRequest();
426
            $controllerObjectName = $request->getControllerObjectName();
427
            $actionName = ($this->arguments['action']) ?: $request->getControllerActionName();
428
            $actionName = $actionName . 'Action';
429
430
            if ($this->hasArgument('formClassName')) {
431
                $formClassName = $this->arguments['formClassName'];
432
            } else {
433
                /** @var ReflectionService $reflectionService */
434
                $reflectionService = $this->objectManager->get(ReflectionService::class);
435
                $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
436
437
                if (false === isset($methodParameters[$this->getFormObjectName()])) {
438
                    throw new \Exception(
439
                        'The method "' . $controllerObjectName . '::' . $actionName . '()" must have a parameter "$' . $this->getFormObjectName() . '". Note that you can also change the parameter "name" of the form view helper.',
440
                        1457441846
441
                    );
442
                }
443
444
                $formClassName = $methodParameters[$this->getFormObjectName()]['type'];
445
            }
446
447
            if (false === class_exists($formClassName)) {
448
                throw new \Exception(
449
                    '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 . '()".',
450
                    1457442014
451
                );
452
            }
453
454
            if (false === in_array(FormInterface::class, class_implements($formClassName))) {
455
                throw new \Exception(
456
                    'Invalid value for the form class name (current value: "' . $formClassName . '"); it must be an instance of "' . FormInterface::class . '".',
457
                    1457442462
458
                );
459
            }
460
461
            $this->formObjectClassName = $formClassName;
462
        }
463
464
        return $this->formObjectClassName;
465
    }
466
467
    /**
468
     * @return Configuration
469
     */
470
    public function getFormzConfiguration()
471
    {
472
        return $this->formzConfiguration;
473
    }
474
475
    /**
476
     * @return FormObject
477
     */
478
    public function getFormObject()
479
    {
480
        return $this->formObject;
481
    }
482
}
483