Completed
Push — master ( 9abde4...30596a )
by
unknown
02:26
created

FormViewHelper::renderForm()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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