FormViewHelper::initialize()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 9.28
c 0
b 0
f 0
cc 4
nc 3
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\Core\Core;
20
use Romm\Formz\Exceptions\ClassNotFoundException;
21
use Romm\Formz\Exceptions\EntryNotFoundException;
22
use Romm\Formz\Exceptions\InvalidOptionValueException;
23
use Romm\Formz\Form\FormInterface;
24
use Romm\Formz\Form\FormObject;
25
use Romm\Formz\Form\FormObjectFactory;
26
use Romm\Formz\Service\ContextService;
27
use Romm\Formz\Service\ControllerService;
28
use Romm\Formz\Service\ExtensionService;
29
use Romm\Formz\Service\StringService;
30
use Romm\Formz\Service\TimeTrackerService;
31
use Romm\Formz\Service\ViewHelper\Form\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\Fluid\View\StandaloneView;
37
38
/**
39
 * This view helper overrides the default one from Extbase, to include
40
 * everything the extension needs to work properly.
41
 *
42
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
43
 * and must be the exact same name as the form parameter in the controller
44
 * action called when the form is submitted. For instance, if your action looks
45
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
46
 * then the "name" attribute of this view helper must be "exampleForm".
47
 *
48
 * Thanks to the information of the form, the following things are automatically
49
 * handled in this view helper:
50
 *
51
 * - Class
52
 *   A custom class may be added to the form DOM element. If the TypoScript
53
 *   configuration "settings.defaultClass" is set for this form, then the given
54
 *   class will be added to the form element.
55
 *
56
 * - JavaScript
57
 *   A block of JavaScript is built from scratch, which will initialize the
58
 *   form, add validation rules to the fields, and handle activation of the
59
 *   fields validation.
60
 *
61
 * - Data attributes
62
 *   To help integrators customize every aspect they need in CSS, every useful
63
 *   information is put in data attributes in the form DOM element. For example,
64
 *   you can know in real time if the field "email" is valid if the form has the
65
 *   attribute "fz-valid-email"
66
 *
67
 * - CSS
68
 *   A block of CSS is built from scratch, which will handle the fields display,
69
 *   depending on their activation property.
70
 */
71
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
72
{
73
    /**
74
     * @var bool
75
     */
76
    protected $escapeOutput = false;
77
78
    /**
79
     * @var PageRenderer
80
     */
81
    protected $pageRenderer;
82
83
    /**
84
     * @var FormObject
85
     */
86
    protected $formObject;
87
88
    /**
89
     * @var FormViewHelperService
90
     */
91
    protected $formService;
92
93
    /**
94
     * @var string
95
     */
96
    protected $formObjectClassName;
97
98
    /**
99
     * @var AssetHandlerFactory
100
     */
101
    protected $assetHandlerFactory;
102
103
    /**
104
     * @var TimeTrackerService
105
     */
106
    protected $timeTracker;
107
108
    /**
109
     * @var bool
110
     */
111
    protected $typoScriptIncluded = false;
112
113
    /**
114
     * @var ControllerService
115
     */
116
    protected $controllerService;
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function initialize()
122
    {
123
        parent::initialize();
124
125
        $this->typoScriptIncluded = ContextService::get()->isTypoScriptIncluded();
126
127
        if (true === $this->typoScriptIncluded) {
128
            $this->timeTracker = TimeTrackerService::getAndStart();
129
            $this->formObjectClassName = $this->getFormClassName();
130
            $this->formObject = $this->getFormObject();
131
            $this->timeTracker->logTime('post-config');
132
            $this->formService->setFormObject($this->formObject);
133
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
134
135
            /*
136
             * If the argument `object` was filled with an instance of Form, it
137
             * is added to the `FormObject`.
138
             */
139
            $objectArgument = $this->getFormObjectArgument();
140
141
            if (null !== $objectArgument
142
                && false === $this->formObject->formWasSubmitted()
143
            ) {
144
                $this->formObject->setForm($objectArgument);
145
            }
146
        }
147
148
        /*
149
         * Important: we need to instantiate the page renderer with this instead
150
         * of Extbase object manager (or with an inject function).
151
         *
152
         * This is due to some TYPO3 low level behaviour which overrides the
153
         * page renderer singleton instance, whenever a new request is used. The
154
         * problem is that the instance is not updated on Extbase side.
155
         *
156
         * Using Extbase injection can lead to old page renderer instance being
157
         * used, resulting in a leak of assets inclusion, and maybe more issues.
158
         */
159
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
160
    }
161
162
    /**
163
     * @inheritdoc
164
     */
165
    public function initializeArguments()
166
    {
167
        parent::initializeArguments();
168
169
        // The name attribute becomes mandatory.
170
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
171
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
172
    }
173
174
    /**
175
     * @return string
176
     */
177
    protected function renderViewHelper()
178
    {
179
        if (false === $this->typoScriptIncluded) {
180
            return (ExtensionService::get()->isInDebugMode())
181
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
182
                : '';
183
        }
184
185
        $formzValidationResult = $this->formObject->getConfigurationValidationResult();
186
187
        $result = ($formzValidationResult->hasErrors())
188
            // If the form configuration is not valid, we display the errors list.
189
            ? $this->getErrorText($formzValidationResult)
190
            // Everything is ok, we render the form.
191
            : $this->renderForm(func_get_args());
192
193
        $this->timeTracker->logTime('final');
194
195
        if (ExtensionService::get()->isInDebugMode()) {
196
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
197
        }
198
199
        $this->formService->resetState();
200
201
        return $result;
202
    }
203
204
    /**
205
     * Will render the whole form and return the HTML result.
206
     *
207
     * @param array $arguments
208
     * @return string
209
     */
210
    final protected function renderForm(array $arguments)
211
    {
212
        /*
213
         * We begin by setting up the form service: request results and form
214
         * instance are inserted in the service, and are used afterwards.
215
         *
216
         * There are only two ways to be sure the values injected are correct:
217
         * when the form was actually submitted by the user, or when the
218
         * argument `object` of the view helper is filled with a form instance.
219
         */
220
        $this->formService->activateFormContext();
221
222
        /*
223
         * If the form was submitted, applying custom behaviours on its fields.
224
         */
225
        $this->formService->applyBehavioursOnSubmittedForm($this->controllerContext);
226
227
        /*
228
         * Adding the default class configured in TypoScript configuration to
229
         * the form HTML tag.
230
         */
231
        $this->addDefaultClass();
232
233
        /*
234
         * Handling data attributes that are added to the form HTML tag,
235
         * depending on several parameters.
236
         */
237
        $this->handleDataAttributes();
238
239
        /*
240
         * Including JavaScript and CSS assets in the page renderer.
241
         */
242
        $this->handleAssets();
243
244
        $this->timeTracker->logTime('pre-render');
245
246
        /*
247
         * Getting the result of the original Fluid `FormViewHelper` rendering.
248
         */
249
        $result = $this->getParentRenderResult($arguments);
250
251
        /*
252
         * Language files need to be included at the end, because they depend on
253
         * what was used by previous assets.
254
         */
255
        $this->getAssetHandlerConnectorManager()
256
            ->getJavaScriptAssetHandlerConnector()
257
            ->includeLanguageJavaScriptFiles();
258
259
        return $result;
260
    }
261
262
    /**
263
     * Will add a default class to the form element.
264
     *
265
     * To customize the class, take a look at `settings.defaultClass` in the
266
     * form TypoScript configuration.
267
     */
268
    protected function addDefaultClass()
269
    {
270
        $formDefaultClass = $this->formObject
271
            ->getConfiguration()
272
            ->getSettings()
273
            ->getDefaultClass();
274
275
        $class = $this->tag->getAttribute('class');
276
277
        if (false === empty($formDefaultClass)) {
278
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
279
            $this->tag->addAttribute('class', $class);
280
        }
281
    }
282
283
    /**
284
     * Adds custom data attributes to the form element, based on the
285
     * submitted form values and results.
286
     */
287
    protected function handleDataAttributes()
288
    {
289
        $dataAttributes = [];
290
291
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
292
293
        if ($this->formObject->hasForm()) {
294
            if (false === $this->formObject->hasFormResult()) {
295
                $form = $this->formObject->getForm();
296
                $formValidator = $this->getFormValidator($this->getFormObjectName());
297
                $formResult = $formValidator->validateGhost($form);
298
            } else {
299
                $formResult = $this->formObject->getFormResult();
300
            }
301
302
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($formResult);
303
        }
304
305
        if (true === $this->formObject->formWasSubmitted()) {
306
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
307
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes();
308
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes();
309
        }
310
311
        $this->tag->addAttributes($dataAttributes);
312
    }
313
314
    /**
315
     * Will include all JavaScript and CSS assets needed for this form.
316
     */
317
    protected function handleAssets()
318
    {
319
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
320
321
        // Default FormZ assets.
322
        $assetHandlerConnectorManager->includeDefaultAssets();
323
324
        // JavaScript assets.
325
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
326
            ->generateAndIncludeFormzConfigurationJavaScript()
327
            ->generateAndIncludeJavaScript()
328
            ->generateAndIncludeInlineJavaScript()
329
            ->includeJavaScriptValidationAndConditionFiles();
330
331
        // CSS assets.
332
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
333
            ->includeGeneratedCss();
334
    }
335
336
    /**
337
     * Will return an error text from a Fluid view.
338
     *
339
     * @param Result $result
340
     * @return string
341
     */
342
    protected function getErrorText(Result $result)
343
    {
344
        /** @var $view StandaloneView */
345
        $view = Core::instantiate(StandaloneView::class);
346
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
347
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
348
        $view->setLayoutRootPaths([$layoutRootPath]);
349
        $view->assign('result', $result);
350
351
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
352
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
353
354
        return $view->render();
355
    }
356
357
    /**
358
     * Checks the type of the argument `object`, and returns it if everything is
359
     * ok.
360
     *
361
     * @return FormInterface|null
362
     * @throws InvalidOptionValueException
363
     */
364
    protected function getFormObjectArgument()
365
    {
366
        $objectArgument = $this->arguments['object'];
367
368
        if (null === $objectArgument) {
369
            return null;
370
        }
371
372
        if (false === is_object($objectArgument)) {
373
            throw InvalidOptionValueException::formViewHelperWrongFormValueType($objectArgument);
374
        }
375
376
        if (false === $objectArgument instanceof FormInterface) {
377
            throw InvalidOptionValueException::formViewHelperWrongFormValueObjectType($objectArgument);
378
        }
379
380
        $formClassName = $this->getFormClassName();
381
382
        if (false === $objectArgument instanceof $formClassName) {
383
            throw InvalidOptionValueException::formViewHelperWrongFormValueClassName($formClassName, $objectArgument);
384
        }
385
386
        return $objectArgument;
387
    }
388
389
    /**
390
     * Returns the class name of the form object: it is fetched from the action
391
     * of the controller which will be called when submitting this form. It
392
     * means two things:
393
     * - The action must have a parameter which has the exact same name as the
394
     *   form;
395
     * - The parameter must indicate its type.
396
     *
397
     * @return string
398
     * @throws ClassNotFoundException
399
     * @throws InvalidOptionValueException
400
     */
401
    protected function getFormClassName()
402
    {
403
        $formClassName = ($this->hasArgument('formClassName'))
404
            ? $this->arguments['formClassName']
405
            : $this->getFormClassNameFromControllerAction();
406
407
        if (false === class_exists($formClassName)) {
408
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
409
        }
410
411
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
412
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
413
        }
414
415
        return $formClassName;
416
    }
417
418
    /**
419
     * Will fetch the name of the controller action argument bound to this
420
     * request.
421
     *
422
     * @return string
423
     * @throws EntryNotFoundException
424
     */
425
    protected function getFormClassNameFromControllerAction()
426
    {
427
        return $this->controllerService->getFormClassNameFromControllerAction(
428
            $this->getControllerName(),
429
            $this->getControllerActionName(),
430
            $this->getFormObjectName()
431
        );
432
    }
433
434
    /**
435
     * Renders the whole Fluid template.
436
     *
437
     * @param array $arguments
438
     * @return string
439
     */
440
    protected function getParentRenderResult(array $arguments)
441
    {
442
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
443
    }
444
445
    /**
446
     * @return string
447
     */
448
    protected function getControllerName()
449
    {
450
        return ($this->arguments['controller'])
451
            ?: $this->controllerContext
452
                ->getRequest()
453
                ->getControllerObjectName();
454
    }
455
456
    /**
457
     * @return string
458
     */
459
    protected function getControllerActionName()
460
    {
461
        return ($this->arguments['action'])
462
            ?: $this->controllerContext
463
                ->getRequest()
464
                ->getControllerActionName();
465
    }
466
467
    /**
468
     * @param string $formName
469
     * @return DefaultFormValidator
470
     */
471
    protected function getFormValidator($formName)
472
    {
473
        /** @var DefaultFormValidator $validation */
474
        $validation = Core::instantiate(DefaultFormValidator::class, ['name' => $formName]);
475
476
        return $validation;
477
    }
478
479
    /**
480
     * @return AssetHandlerConnectorManager
481
     */
482
    protected function getAssetHandlerConnectorManager()
483
    {
484
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
485
    }
486
487
    /**
488
     * @return DataAttributesAssetHandler
489
     */
490
    protected function getDataAttributesAssetHandler()
491
    {
492
        /** @var DataAttributesAssetHandler $assetHandler */
493
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
494
495
        return $assetHandler;
496
    }
497
498
    /**
499
     * @return FormObject
500
     */
501
    protected function getFormObject()
502
    {
503
        /** @var FormObjectFactory $formObjectFactory */
504
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
505
506
        return $formObjectFactory->getInstanceFromClassName($this->formObjectClassName, $this->getFormObjectName());
507
    }
508
509
    /**
510
     * @param FormViewHelperService $service
511
     */
512
    public function injectFormService(FormViewHelperService $service)
513
    {
514
        $this->formService = $service;
515
    }
516
517
    /**
518
     * @param ControllerService $controllerService
519
     */
520
    public function injectControllerService(ControllerService $controllerService)
521
    {
522
        $this->controllerService = $controllerService;
523
    }
524
}
525