Completed
Push — unit-tests-conditions ( fc6a80...b4e1a6 )
by Romain
02:09
created

FormViewHelper::getFormClassName()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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