Completed
Push — middleware ( 1eef6b )
by Romain
02:33
created

FormViewHelper::renderHiddenReferrerFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
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\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 TYPO3\CMS\Core\Page\PageRenderer;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Extbase\Error\Result;
35
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
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 "formz-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
     * @inheritdoc
115
     */
116
    public function initialize()
117
    {
118
        parent::initialize();
119
120
        $this->typoScriptIncluded = ContextService::get()->isTypoScriptIncluded();
121
122
        if (true === $this->typoScriptIncluded) {
123
            $this->formObjectClassName = $this->getFormClassName();
124
            $this->formObject = $this->getFormObject();
125
            $this->formService->setFormObject($this->formObject);
126
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
127
        }
128
129
        /*
130
         * Important: we need to instantiate the page renderer with this instead
131
         * of Extbase object manager (or with an inject function).
132
         *
133
         * This is due to some TYPO3 low level behaviour which overrides the
134
         * page renderer singleton instance, whenever a new request is used. The
135
         * problem is that the instance is not updated on Extbase side.
136
         *
137
         * Using Extbase injection can lead to old page renderer instance being
138
         * used, resulting in a leak of assets inclusion, and maybe more issues.
139
         */
140
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
141
    }
142
143
    /**
144
     * @inheritdoc
145
     */
146
    public function initializeArguments()
147
    {
148
        parent::initializeArguments();
149
150
        // The name attribute becomes mandatory.
151
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
152
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
153
    }
154
155
    /**
156
     * @return string
157
     */
158
    protected function renderViewHelper()
159
    {
160
        $this->timeTracker = TimeTrackerService::getAndStart();
161
162
        if (false === $this->typoScriptIncluded) {
163
            return (ExtensionService::get()->isInDebugMode())
164
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
165
                : '';
166
        }
167
168
        $formzValidationResult = $this->formObject->getConfigurationValidationResult();
169
        $this->timeTracker->logTime('post-config');
170
171
        $result = ($formzValidationResult->hasErrors())
172
            // If the form configuration is not valid, we display the errors list.
173
            ? $this->getErrorText($formzValidationResult)
174
            // Everything is ok, we render the form.
175
            : $this->renderForm(func_get_args());
176
177
        $this->timeTracker->logTime('final');
178
179
        if (ExtensionService::get()->isInDebugMode()) {
180
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
181
        }
182
183
        $this->formService->resetState();
184
185
        return $result;
186
    }
187
188
    /**
189
     * Will render the whole form and return the HTML result.
190
     *
191
     * @param array $arguments
192
     * @return string
193
     */
194
    final protected function renderForm(array $arguments)
195
    {
196
        /*
197
         * We begin by setting up the form service: request results and form
198
         * instance are inserted in the service, and are used afterwards.
199
         *
200
         * There are only two ways to be sure the values injected are correct:
201
         * when the form was actually submitted by the user, or when the
202
         * argument `object` of the view helper is filled with a form instance.
203
         */
204
        $this->formService
205
            ->activateFormContext()
206
            ->setUpData(
207
                $this->getFormObjectName(),
208
                $this->controllerContext->getRequest()->getOriginalRequest(),
209
                $this->arguments['object']
210
            );
211
212
        /*
213
         * Adding the default class configured in TypoScript configuration to
214
         * the form HTML tag.
215
         */
216
        $this->addDefaultClass();
217
218
        /*
219
         * If the form was submitted, applying custom behaviours on its fields.
220
         */
221
        $this->applyBehavioursOnSubmittedForm();
222
223
        /*
224
         * Handling data attributes that are added to the form HTML tag,
225
         * depending on several parameters.
226
         */
227
        $this->handleDataAttributes();
228
229
        /*
230
         * Including JavaScript and CSS assets in the page renderer.
231
         */
232
        $this->handleAssets();
233
234
        $this->timeTracker->logTime('pre-render');
235
236
        /*
237
         * Getting the result of the original Fluid `FormViewHelper` rendering.
238
         */
239
        $result = $this->getParentRenderResult($arguments);
240
241
        /*
242
         * Language files need to be included at the end, because they depend on
243
         * what was used by previous assets.
244
         */
245
        $this->getAssetHandlerConnectorManager()
246
            ->getJavaScriptAssetHandlerConnector()
247
            ->includeLanguageJavaScriptFiles();
248
249
        return $result;
250
    }
251
252
    /**
253
     * @todo
254
     *
255
     * @return string
256
     */
257
    protected function renderHiddenReferrerFields()
258
    {
259
        $result = parent::renderHiddenReferrerFields();
260
261
        $data = [];
262
263
        $result .= '<input type="hidden" name="' . $this->prefixFieldName('formz') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($data)))) . '" />' . LF;
264
265
        return $result;
266
    }
267
268
    /**
269
     * Will loop on the submitted form fields and apply behaviours if their
270
     * configuration contains.
271
     */
272
    protected function applyBehavioursOnSubmittedForm()
273
    {
274
        if ($this->formService->formWasSubmitted()) {
275
            /** @var BehavioursManager $behavioursManager */
276
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
277
278
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
279
                $this->formService->getFormInstance(),
0 ignored issues
show
Bug introduced by
It seems like $this->formService->getFormInstance() targeting Romm\Formz\Service\ViewH...vice::getFormInstance() can also be of type object<Romm\Formz\Form\FormInterface>; however, Romm\Formz\Behaviours\Be...iourOnPropertiesArray() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
280
                $this->formObject->getConfiguration()
281
            );
282
283
            $this->controllerContext
284
                ->getRequest()
285
                ->getOriginalRequest()
286
                ->setArgument($this->getFormObjectName(), $formProperties);
287
        }
288
    }
289
290
    /**
291
     * Will add a default class to the form element.
292
     *
293
     * To customize the class, take a look at `settings.defaultClass` in the
294
     * form TypoScript configuration.
295
     */
296
    protected function addDefaultClass()
297
    {
298
        $formDefaultClass = $this->formObject
299
            ->getConfiguration()
300
            ->getSettings()
301
            ->getDefaultClass();
302
303
        $class = $this->tag->getAttribute('class');
304
305
        if (false === empty($formDefaultClass)) {
306
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
307
            $this->tag->addAttribute('class', $class);
308
        }
309
    }
310
311
    /**
312
     * Adds custom data attributes to the form element, based on the
313
     * submitted form values and results.
314
     */
315
    protected function handleDataAttributes()
316
    {
317
        $dataAttributes = [];
318
        $object = $this->formService->getFormInstance();
319
        $formResult = $this->formService->getFormResult();
320
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
321
322
        if ($object && $formResult) {
323
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $formResult);
324
        }
325
326
        if ($formResult
327
            && true === $this->formService->formWasSubmitted()
328
        ) {
329
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
330
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($formResult);
331
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes($formResult);
332
        }
333
334
        $this->tag->addAttributes($dataAttributes);
335
    }
336
337
    /**
338
     * Will include all JavaScript and CSS assets needed for this form.
339
     */
340
    protected function handleAssets()
341
    {
342
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
343
344
        // Default Formz assets.
345
        $assetHandlerConnectorManager->includeDefaultAssets();
346
347
        // JavaScript assets.
348
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
349
            ->generateAndIncludeFormzConfigurationJavaScript()
350
            ->generateAndIncludeJavaScript()
351
            ->generateAndIncludeInlineJavaScript()
352
            ->includeJavaScriptValidationAndConditionFiles();
353
354
        // CSS assets.
355
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
356
            ->includeGeneratedCss();
357
    }
358
359
    /**
360
     * Will return an error text from a Fluid view.
361
     *
362
     * @param Result $result
363
     * @return string
364
     */
365
    protected function getErrorText(Result $result)
366
    {
367
        /** @var $view StandaloneView */
368
        $view = Core::instantiate(StandaloneView::class);
369
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
370
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
371
        $view->setLayoutRootPaths([$layoutRootPath]);
372
        $view->assign('result', $result);
373
374
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
375
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
376
377
        return $view->render();
378
    }
379
380
    /**
381
     * Returns the class name of the form object: it is fetched from the action
382
     * of the controller which will be called when submitting this form. It
383
     * means two things:
384
     * - The action must have a parameter which has the exact same name as the
385
     *   form;
386
     * - The parameter must indicate its type.
387
     *
388
     * @return string
389
     * @throws ClassNotFoundException
390
     * @throws InvalidOptionValueException
391
     */
392
    protected function getFormClassName()
393
    {
394
        $formClassName = ($this->hasArgument('formClassName'))
395
            ? $this->arguments['formClassName']
396
            : $this->getFormClassNameFromControllerAction();
397
398
        if (false === class_exists($formClassName)) {
399
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
400
        }
401
402
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
403
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
404
        }
405
406
        return $formClassName;
407
    }
408
409
    /**
410
     * Will fetch the name of the controller action argument bound to this
411
     * request.
412
     *
413
     * @return string
414
     * @throws EntryNotFoundException
415
     */
416
    protected function getFormClassNameFromControllerAction()
417
    {
418
        $controllerObjectName = $this->getControllerName();
419
        $actionName = $this->getControllerActionName();
420
421
        /** @var ReflectionService $reflectionService */
422
        $reflectionService = Core::instantiate(ReflectionService::class);
423
        $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
424
425
        if (false === isset($methodParameters[$this->getFormObjectName()])) {
426
            throw EntryNotFoundException::formViewHelperControllerActionArgumentMissing($controllerObjectName, $actionName, $this->getFormObjectName());
427
        }
428
429
        return $methodParameters[$this->getFormObjectName()]['type'];
430
    }
431
432
    /**
433
     * Renders the whole Fluid template.
434
     *
435
     * @param array $arguments
436
     * @return string
437
     */
438
    protected function getParentRenderResult(array $arguments)
439
    {
440
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
441
    }
442
443
    /**
444
     * @return string
445
     */
446
    protected function getControllerName()
447
    {
448
        return ($this->arguments['controller'])
449
            ?: $this->controllerContext
450
                ->getRequest()
451
                ->getControllerObjectName();
452
    }
453
454
    /**
455
     * @return string
456
     */
457
    protected function getControllerActionName()
458
    {
459
        $actionName = ($this->arguments['action'])
460
            ?: $this->controllerContext
461
                ->getRequest()
462
                ->getControllerActionName();
463
464
        return $actionName . 'Action';
465
    }
466
467
    /**
468
     * @return AssetHandlerConnectorManager
469
     */
470
    protected function getAssetHandlerConnectorManager()
471
    {
472
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
473
    }
474
475
    /**
476
     * @return DataAttributesAssetHandler
477
     */
478
    protected function getDataAttributesAssetHandler()
479
    {
480
        return $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
481
    }
482
483
    /**
484
     * @return FormObject
485
     */
486
    protected function getFormObject()
487
    {
488
        /** @var FormObjectFactory $formObjectFactory */
489
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
490
491
        return $formObjectFactory->getInstanceFromClassName($this->formObjectClassName, $this->getFormObjectName());
492
    }
493
494
    /**
495
     * @param FormViewHelperService $service
496
     */
497
    public function injectFormService(FormViewHelperService $service)
498
    {
499
        $this->formService = $service;
500
    }
501
}
502