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