Completed
Push — middleware ( 1eef6b...9c572f )
by Romain
02:39
created

FormViewHelper::injectControllerService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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