Completed
Push — typo3-v8-compatibility ( ae4db2...381377 )
by Romain
02:16
created

FormViewHelper::setObjectAndRequestResult()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
c 0
b 0
f 0
rs 8.5806
cc 4
eloc 23
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\Behaviours\BehavioursManager;
20
use Romm\Formz\Core\Core;
21
use Romm\Formz\Form\FormInterface;
22
use Romm\Formz\Form\FormObjectFactory;
23
use Romm\Formz\Service\ContextService;
24
use Romm\Formz\Service\ExtensionService;
25
use Romm\Formz\Service\StringService;
26
use Romm\Formz\Service\TimeTrackerService;
27
use Romm\Formz\Validation\Validator\Form\AbstractFormValidator;
28
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
29
use Romm\Formz\ViewHelpers\Service\FormService;
30
use TYPO3\CMS\Core\Page\PageRenderer;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Extbase\Error\Result;
33
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
34
use TYPO3\CMS\Fluid\View\StandaloneView;
35
36
/**
37
 * This view helper overrides the default one from Extbase, to include
38
 * everything the extension needs to work properly.
39
 *
40
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
41
 * and must be the exact same name as the form parameter in the controller
42
 * action called when the form is submitted. For instance, if your action looks
43
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
44
 * then the "name" attribute of this view helper must be "exampleForm".
45
 *
46
 * Thanks to the information of the form, the following things are automatically
47
 * handled in this view helper:
48
 *
49
 * - Class
50
 *   A custom class may be added to the form DOM element. If the TypoScript
51
 *   configuration "settings.defaultClass" is set for this form, then the given
52
 *   class will be added to the form element.
53
 *
54
 * - JavaScript
55
 *   A block of JavaScript is built from scratch, which will initialize the
56
 *   form, add validation rules to the fields, and handle activation of the
57
 *   fields validation.
58
 *
59
 * - Data attributes
60
 *   To help integrators customize every aspect they need in CSS, every useful
61
 *   information is put in data attributes in the form DOM element. For example,
62
 *   you can know in real time if the field "email" is valid if the form has the
63
 *   attribute "formz-valid-email"
64
 *
65
 * - CSS
66
 *   A block of CSS is built from scratch, which will handle the fields display,
67
 *   depending on their activation property.
68
 */
69
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
70
{
71
    /**
72
     * @var PageRenderer
73
     */
74
    protected $pageRenderer;
75
76
    /**
77
     * @var FormObjectFactory
78
     */
79
    protected $formObjectFactory;
80
81
    /**
82
     * @var FormService
83
     */
84
    protected $formService;
85
86
    /**
87
     * @var string
88
     */
89
    protected $formObjectClassName;
90
91
    /**
92
     * @var AssetHandlerFactory
93
     */
94
    protected $assetHandlerFactory;
95
96
    /**
97
     * @var TimeTrackerService
98
     */
99
    protected $timeTracker;
100
101
    /**
102
     * @inheritdoc
103
     */
104
    public function initialize()
105
    {
106
        parent::initialize();
107
108
        /*
109
         * Important: we need to instantiate the page renderer with this instead
110
         * of Extbase object manager (or with an inject function).
111
         *
112
         * This is due to some TYPO3 low level behaviour which overrides the
113
         * page renderer singleton instance, whenever a new request is used. The
114
         * problem is that the instance is not updated on Extbase side.
115
         *
116
         * Using Extbase injection can lead to old page renderer instance being
117
         * used, resulting in a leak of assets inclusion, and maybe more issues.
118
         */
119
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
120
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125
    public function initializeArguments()
126
    {
127
        parent::initializeArguments();
128
129
        // The name attribute becomes mandatory.
130
        $this->overrideArgument('name', 'string', 'Name of the form', true);
131
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
132
    }
133
134
    /**
135
     * @return string
136
     */
137
    protected function renderViewHelper()
138
    {
139
        $this->timeTracker = TimeTrackerService::getAndStart();
140
        $result = '';
141
142
        if (false === ContextService::get()->isTypoScriptIncluded()) {
143
            if (ExtensionService::get()->isInDebugMode()) {
144
                $result = ContextService::get()->translate('form.typoscript_not_included.error_message');
145
            }
146
        } else {
147
            $formObject = $this->formObjectFactory->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
148
149
            $this->formService->setFormObject($formObject);
150
            $formzValidationResult = $formObject->getConfigurationValidationResult();
151
152
            if ($formzValidationResult->hasErrors()) {
153
                // If the form configuration is not valid, we display the errors list.
154
                $result = $this->getErrorText($formzValidationResult);
155
            } else {
156
                // Everything is ok, we render the form.
157
                $result = $this->renderForm(func_get_args());
158
            }
159
160
            unset($formzValidationResult);
161
        }
162
163
        $this->timeTracker->logTime('final');
164
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
165
        unset($this->timeTracker);
166
167
        $this->formService->resetState();
168
169
        return $result;
170
    }
171
    /**
172
     * Will render the whole form and return the HTML result.
173
     *
174
     * @param array $arguments
175
     * @return string
176
     */
177
    final protected function renderForm(array $arguments)
178
    {
179
        $this->timeTracker->logTime('post-config');
180
181
        $this->assetHandlerFactory = AssetHandlerFactory::get($this->formService->getFormObject(), $this->controllerContext);
182
183
        $this->setObjectAndRequestResult()
184
            ->applyBehavioursOnSubmittedForm()
185
            ->addDefaultClass()
186
            ->handleDataAttributes();
187
188
        $assetHandlerConnectorManager = AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
189
        $assetHandlerConnectorManager->includeDefaultAssets();
190
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
191
            ->generateAndIncludeFormzConfigurationJavaScript()
192
            ->generateAndIncludeJavaScript()
193
            ->generateAndIncludeInlineJavaScript()
194
            ->includeJavaScriptValidationAndConditionFiles();
195
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()->includeGeneratedCss();
196
197
        $this->timeTracker->logTime('pre-render');
198
199
        // Renders the whole Fluid template.
200
        $result = call_user_func_array([get_parent_class(), 'render'], $arguments);
201
202
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()->includeLanguageJavaScriptFiles();
203
204
        return $result;
205
    }
206
207
    /**
208
     * This function will inject in the variable container the instance of form
209
     * and its submission result. There are only two ways to be sure the values
210
     * injected are correct: when the form has actually been submitted by the
211
     * user, or when the view helper argument `object` is filled.
212
     *
213
     * @return $this
214
     */
215
    protected function setObjectAndRequestResult()
216
    {
217
        $this->formService->activateFormContext();
218
219
        $originalRequest = $this->controllerContext
220
            ->getRequest()
221
            ->getOriginalRequest();
222
223
        if (null !== $originalRequest
224
            && $originalRequest->hasArgument($this->getFormObjectName())
225
        ) {
226
            /** @var array $formInstance */
227
            $formInstance = $originalRequest->getArgument($this->getFormObjectName());
228
229
            $formRequestResult = AbstractFormValidator::getFormValidationResult(
230
                $this->getFormObjectClassName(),
231
                $this->getFormObjectName()
232
            );
233
234
            $this->formService->setFormInstance($formInstance);
235
            $this->formService->setFormResult($formRequestResult);
0 ignored issues
show
Bug introduced by
It seems like $formRequestResult defined by \Romm\Formz\Validation\V...s->getFormObjectName()) on line 229 can be null; however, Romm\Formz\ViewHelpers\S...ervice::setFormResult() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
236
            $this->formService->markFormAsSubmitted();
237
        } elseif (null !== $this->arguments['object']) {
238
            $formInstance = $this->arguments['object'];
239
240
            /*
241
             * @todo: pas forcément un DefaultFormValidator: comment je gère ça?
242
             * + ça prend quand même un peu de temps cette manière. Peut-on faire autrement ?
243
             */
244
            /** @var DefaultFormValidator $formValidator */
245
            $formValidator = Core::instantiate(
246
                DefaultFormValidator::class,
247
                ['name' => $this->getFormObjectName()]
248
            );
249
            $formRequestResult = $formValidator->validate($formInstance);
250
251
            $this->formService->setFormInstance($formInstance);
252
            $this->formService->setFormResult($formRequestResult);
253
        }
254
255
        return $this;
256
    }
257
258
    /**
259
     * Will loop on the submitted form fields and apply behaviours if their
260
     * configuration contains.
261
     *
262
     * @return $this
263
     */
264
    protected function applyBehavioursOnSubmittedForm()
265
    {
266
        $originalRequest = $this->controllerContext
267
            ->getRequest()
268
            ->getOriginalRequest();
269
270
        if ($this->formService->formWasSubmitted()) {
271
            /** @var BehavioursManager $behavioursManager */
272
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
273
274
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
275
                $this->formService->getFormInstance(),
0 ignored issues
show
Bug introduced by
It seems like $this->formService->getFormInstance() targeting Romm\Formz\ViewHelpers\S...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...
276
                $this->formService->getFormObject()->getConfiguration()
277
            );
278
279
            $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
280
        }
281
282
        return $this;
283
    }
284
285
    /**
286
     * Will add a default class to the form element.
287
     *
288
     * To customize the class, take a look at `settings.defaultClass` in the
289
     * form TypoScript configuration.
290
     *
291
     * @return $this
292
     */
293
    protected function addDefaultClass()
294
    {
295
        $formDefaultClass = $this->formService
296
            ->getFormObject()
297
            ->getConfiguration()
298
            ->getSettings()
299
            ->getDefaultClass();
300
301
        $class = $this->tag->getAttribute('class');
302
303
        if (false === empty($formDefaultClass)) {
304
            $class = ((!empty($class)) ? $class . ' ' : '') . $formDefaultClass;
305
        }
306
307
        $this->tag->addAttribute('class', $class);
308
309
        return $this;
310
    }
311
312
    /**
313
     * Adds custom data attributes to the form element, based on the
314
     * submitted form values and results.
315
     *
316
     * @return $this
317
     */
318
    protected function handleDataAttributes()
319
    {
320
        $object = $this->formService->getFormInstance();
321
        $formResult = $this->formService->getFormResult();
322
323
        /** @var DataAttributesAssetHandler $dataAttributesAssetHandler */
324
        $dataAttributesAssetHandler =  $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
325
326
        $dataAttributes = [];
327
        if ($object) {
328
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $formResult);
329
        }
330
331
        if ($formResult) {
332
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($formResult);
333
334
            if (true === $this->formService->formWasSubmitted()) {
335
                $dataAttributes += ['formz-submission-done' => '1'];
336
                $dataAttributes += $dataAttributesAssetHandler->getFieldsErrorsDataAttributes($formResult);
337
            }
338
        }
339
340
        foreach ($dataAttributes as $attributeName => $attributeValue) {
341
            $this->tag->addAttribute($attributeName, $attributeValue);
342
        }
343
344
        return $this;
345
    }
346
347
    /**
348
     * Will return an error text from a Fluid view.
349
     *
350
     * @param Result $result
351
     * @return string
352
     */
353
    protected function getErrorText(Result $result)
354
    {
355
        /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
356
        $view = $this->objectManager->get(StandaloneView::class);
357
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
358
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
359
        $view->setLayoutRootPaths([$layoutRootPath]);
360
        $view->assign('result', $result);
361
362
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
363
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
364
365
        return $view->render();
366
    }
367
368
    /**
369
     * Returns the class name of the form object: it is fetched from the action
370
     * of the controller which will be called when submitting this form. It
371
     * means two things:
372
     * - The action must have a parameter which has the exact same name as the
373
     *   form.
374
     * - The parameter must indicate its type.
375
     *
376
     * @return null|string
377
     * @throws \Exception
378
     */
379
    protected function getFormObjectClassName()
380
    {
381
        if (null === $this->formObjectClassName) {
382
            $request = $this->controllerContext->getRequest();
383
            $controllerObjectName = $request->getControllerObjectName();
384
            $actionName = ($this->arguments['action']) ?: $request->getControllerActionName();
385
            $actionName = $actionName . 'Action';
386
387
            if ($this->hasArgument('formClassName')) {
388
                $formClassName = $this->arguments['formClassName'];
389
            } else {
390
                /** @var ReflectionService $reflectionService */
391
                $reflectionService = $this->objectManager->get(ReflectionService::class);
392
                $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
393
394
                if (false === isset($methodParameters[$this->getFormObjectName()])) {
395
                    throw new \Exception(
396
                        'The method "' . $controllerObjectName . '::' . $actionName . '()" must have a parameter "$' . $this->getFormObjectName() . '". Note that you can also change the parameter "name" of the form view helper.',
397
                        1457441846
398
                    );
399
                }
400
401
                $formClassName = $methodParameters[$this->getFormObjectName()]['type'];
402
            }
403
404
            if (false === class_exists($formClassName)) {
405
                throw new \Exception(
406
                    'Invalid value for the form class name (current value: "' . $formClassName . '"). You need to either fill the parameter "formClassName" in the view helper, or specify the type of the parameter "$' . $this->getFormObjectName() . '" for the method "' . $controllerObjectName . '::' . $actionName . '()".',
407
                    1457442014
408
                );
409
            }
410
411
            if (false === in_array(FormInterface::class, class_implements($formClassName))) {
412
                throw new \Exception(
413
                    'Invalid value for the form class name (current value: "' . $formClassName . '"); it must be an instance of "' . FormInterface::class . '".',
414
                    1457442462
415
                );
416
            }
417
418
            $this->formObjectClassName = $formClassName;
419
        }
420
421
        return $this->formObjectClassName;
422
    }
423
424
    /**
425
     * @param PageRenderer $pageRenderer
426
     */
427
    public function injectPageRenderer(PageRenderer $pageRenderer)
428
    {
429
        $this->pageRenderer = $pageRenderer;
430
    }
431
432
    /**
433
     * @param FormObjectFactory $formObjectFactory
434
     */
435
    public function injectFormObjectFactory(FormObjectFactory $formObjectFactory)
436
    {
437
        $this->formObjectFactory = $formObjectFactory;
438
    }
439
440
    /**
441
     * @param FormService $service
442
     */
443
    public function injectFormService(FormService $service)
444
    {
445
        $this->formService = $service;
446
    }
447
}
448