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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
474
475
            $form = $submittedForm ?: Core::get()->getObjectManager()->getEmptyObject($this->getFormClassName());
476
        }
477
478
        return $form;
479
    }
480
481
    /**
482
     * @param FormInterface $form
483
     * @return FormObject
484
     */
485
    protected function getFormObject(FormInterface $form)
486
    {
487
        return FormObjectFactory::get()->getInstanceWithFormInstance($form, $this->getFormObjectName());
488
    }
489
490
    /**
491
     * @param FormViewHelperService $service
492
     */
493
    public function injectFormService(FormViewHelperService $service)
494
    {
495
        $this->formService = $service;
496
    }
497
498
    /**
499
     * @param ControllerService $controllerService
500
     */
501
    public function injectControllerService(ControllerService $controllerService)
502
    {
503
        $this->controllerService = $controllerService;
504
    }
505
}
506