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