Completed
Push — master ( 3395f4...940fa6 )
by Romain
13s
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 bool
73
     */
74
    protected $escapeOutput = false;
75
76
    /**
77
     * @var PageRenderer
78
     */
79
    protected $pageRenderer;
80
81
    /**
82
     * @var FormObjectFactory
83
     */
84
    protected $formObjectFactory;
85
86
    /**
87
     * @var FormService
88
     */
89
    protected $formService;
90
91
    /**
92
     * @var string
93
     */
94
    protected $formObjectClassName;
95
96
    /**
97
     * @var AssetHandlerFactory
98
     */
99
    protected $assetHandlerFactory;
100
101
    /**
102
     * @var TimeTrackerService
103
     */
104
    protected $timeTracker;
105
106
    /**
107
     * @inheritdoc
108
     */
109
    public function initialize()
110
    {
111
        parent::initialize();
112
113
        /*
114
         * Important: we need to instantiate the page renderer with this instead
115
         * of Extbase object manager (or with an inject function).
116
         *
117
         * This is due to some TYPO3 low level behaviour which overrides the
118
         * page renderer singleton instance, whenever a new request is used. The
119
         * problem is that the instance is not updated on Extbase side.
120
         *
121
         * Using Extbase injection can lead to old page renderer instance being
122
         * used, resulting in a leak of assets inclusion, and maybe more issues.
123
         */
124
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
125
    }
126
127
    /**
128
     * @inheritdoc
129
     */
130
    public function initializeArguments()
131
    {
132
        parent::initializeArguments();
133
134
        // The name attribute becomes mandatory.
135
        $this->overrideArgument('name', 'string', 'Name of the form', true);
136
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
137
    }
138
139
    /**
140
     * @return string
141
     */
142
    protected function renderViewHelper()
143
    {
144
        $this->timeTracker = TimeTrackerService::getAndStart();
145
        $result = '';
146
147
        if (false === ContextService::get()->isTypoScriptIncluded()) {
148
            if (ExtensionService::get()->isInDebugMode()) {
149
                $result = ContextService::get()->translate('form.typoscript_not_included.error_message');
150
            }
151
        } else {
152
            $formObject = $this->formObjectFactory->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
153
154
            $this->formService->setFormObject($formObject);
155
            $formzValidationResult = $formObject->getConfigurationValidationResult();
156
157
            if ($formzValidationResult->hasErrors()) {
158
                // If the form configuration is not valid, we display the errors list.
159
                $result = $this->getErrorText($formzValidationResult);
160
            } else {
161
                // Everything is ok, we render the form.
162
                $result = $this->renderForm(func_get_args());
163
            }
164
165
            unset($formzValidationResult);
166
        }
167
168
        $this->timeTracker->logTime('final');
169
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
170
        unset($this->timeTracker);
171
172
        $this->formService->resetState();
173
174
        return $result;
175
    }
176
    /**
177
     * Will render the whole form and return the HTML result.
178
     *
179
     * @param array $arguments
180
     * @return string
181
     */
182
    final protected function renderForm(array $arguments)
183
    {
184
        $this->timeTracker->logTime('post-config');
185
186
        $this->assetHandlerFactory = AssetHandlerFactory::get($this->formService->getFormObject(), $this->controllerContext);
187
188
        $this->setObjectAndRequestResult()
189
            ->applyBehavioursOnSubmittedForm()
190
            ->addDefaultClass()
191
            ->handleDataAttributes();
192
193
        $assetHandlerConnectorManager = AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
194
        $assetHandlerConnectorManager->includeDefaultAssets();
195
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
196
            ->generateAndIncludeFormzConfigurationJavaScript()
197
            ->generateAndIncludeJavaScript()
198
            ->generateAndIncludeInlineJavaScript()
199
            ->includeJavaScriptValidationAndConditionFiles();
200
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()->includeGeneratedCss();
201
202
        $this->timeTracker->logTime('pre-render');
203
204
        // Renders the whole Fluid template.
205
        $result = call_user_func_array([get_parent_class(), 'render'], $arguments);
206
207
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()->includeLanguageJavaScriptFiles();
208
209
        return $result;
210
    }
211
212
    /**
213
     * This function will inject in the variable container the instance of form
214
     * and its submission result. There are only two ways to be sure the values
215
     * injected are correct: when the form has actually been submitted by the
216
     * user, or when the view helper argument `object` is filled.
217
     *
218
     * @return $this
219
     */
220
    protected function setObjectAndRequestResult()
221
    {
222
        $this->formService->activateFormContext();
223
224
        $originalRequest = $this->controllerContext
225
            ->getRequest()
226
            ->getOriginalRequest();
227
228
        if (null !== $originalRequest
229
            && $originalRequest->hasArgument($this->getFormObjectName())
230
        ) {
231
            /** @var array $formInstance */
232
            $formInstance = $originalRequest->getArgument($this->getFormObjectName());
233
234
            $formRequestResult = AbstractFormValidator::getFormValidationResult(
235
                $this->getFormObjectClassName(),
236
                $this->getFormObjectName()
237
            );
238
239
            $this->formService->setFormInstance($formInstance);
240
            $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 234 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...
241
            $this->formService->markFormAsSubmitted();
242
        } elseif (null !== $this->arguments['object']) {
243
            $formInstance = $this->arguments['object'];
244
245
            /*
246
             * @todo: pas forcément un DefaultFormValidator: comment je gère ça?
247
             * + ça prend quand même un peu de temps cette manière. Peut-on faire autrement ?
248
             */
249
            /** @var DefaultFormValidator $formValidator */
250
            $formValidator = Core::instantiate(
251
                DefaultFormValidator::class,
252
                ['name' => $this->getFormObjectName()]
253
            );
254
            $formRequestResult = $formValidator->validate($formInstance);
255
256
            $this->formService->setFormInstance($formInstance);
257
            $this->formService->setFormResult($formRequestResult);
258
        }
259
260
        return $this;
261
    }
262
263
    /**
264
     * Will loop on the submitted form fields and apply behaviours if their
265
     * configuration contains.
266
     *
267
     * @return $this
268
     */
269
    protected function applyBehavioursOnSubmittedForm()
270
    {
271
        $originalRequest = $this->controllerContext
272
            ->getRequest()
273
            ->getOriginalRequest();
274
275
        if ($this->formService->formWasSubmitted()) {
276
            /** @var BehavioursManager $behavioursManager */
277
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
278
279
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
280
                $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...
281
                $this->formService->getFormObject()->getConfiguration()
282
            );
283
284
            $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
285
        }
286
287
        return $this;
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
     * @return $this
297
     */
298
    protected function addDefaultClass()
299
    {
300
        $formDefaultClass = $this->formService
301
            ->getFormObject()
302
            ->getConfiguration()
303
            ->getSettings()
304
            ->getDefaultClass();
305
306
        $class = $this->tag->getAttribute('class');
307
308
        if (false === empty($formDefaultClass)) {
309
            $class = ((!empty($class)) ? $class . ' ' : '') . $formDefaultClass;
310
        }
311
312
        $this->tag->addAttribute('class', $class);
313
314
        return $this;
315
    }
316
317
    /**
318
     * Adds custom data attributes to the form element, based on the
319
     * submitted form values and results.
320
     *
321
     * @return $this
322
     */
323
    protected function handleDataAttributes()
324
    {
325
        $object = $this->formService->getFormInstance();
326
        $formResult = $this->formService->getFormResult();
327
328
        /** @var DataAttributesAssetHandler $dataAttributesAssetHandler */
329
        $dataAttributesAssetHandler =  $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
330
331
        $dataAttributes = [];
332
        if ($object) {
333
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $formResult);
334
        }
335
336
        if ($formResult) {
337
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($formResult);
338
339
            if (true === $this->formService->formWasSubmitted()) {
340
                $dataAttributes += ['formz-submission-done' => '1'];
341
                $dataAttributes += $dataAttributesAssetHandler->getFieldsErrorsDataAttributes($formResult);
342
            }
343
        }
344
345
        foreach ($dataAttributes as $attributeName => $attributeValue) {
346
            $this->tag->addAttribute($attributeName, $attributeValue);
347
        }
348
349
        return $this;
350
    }
351
352
    /**
353
     * Will return an error text from a Fluid view.
354
     *
355
     * @param Result $result
356
     * @return string
357
     */
358
    protected function getErrorText(Result $result)
359
    {
360
        /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
361
        $view = $this->objectManager->get(StandaloneView::class);
362
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
363
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
364
        $view->setLayoutRootPaths([$layoutRootPath]);
365
        $view->assign('result', $result);
366
367
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
368
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
369
370
        return $view->render();
371
    }
372
373
    /**
374
     * Returns the class name of the form object: it is fetched from the action
375
     * of the controller which will be called when submitting this form. It
376
     * means two things:
377
     * - The action must have a parameter which has the exact same name as the
378
     *   form.
379
     * - The parameter must indicate its type.
380
     *
381
     * @return null|string
382
     * @throws \Exception
383
     */
384
    protected function getFormObjectClassName()
385
    {
386
        if (null === $this->formObjectClassName) {
387
            $request = $this->controllerContext->getRequest();
388
            $controllerObjectName = $request->getControllerObjectName();
389
            $actionName = ($this->arguments['action']) ?: $request->getControllerActionName();
390
            $actionName = $actionName . 'Action';
391
392
            if ($this->hasArgument('formClassName')) {
393
                $formClassName = $this->arguments['formClassName'];
394
            } else {
395
                /** @var ReflectionService $reflectionService */
396
                $reflectionService = $this->objectManager->get(ReflectionService::class);
397
                $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
398
399
                if (false === isset($methodParameters[$this->getFormObjectName()])) {
400
                    throw new \Exception(
401
                        'The method "' . $controllerObjectName . '::' . $actionName . '()" must have a parameter "$' . $this->getFormObjectName() . '". Note that you can also change the parameter "name" of the form view helper.',
402
                        1457441846
403
                    );
404
                }
405
406
                $formClassName = $methodParameters[$this->getFormObjectName()]['type'];
407
            }
408
409
            if (false === class_exists($formClassName)) {
410
                throw new \Exception(
411
                    '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 . '()".',
412
                    1457442014
413
                );
414
            }
415
416
            if (false === in_array(FormInterface::class, class_implements($formClassName))) {
417
                throw new \Exception(
418
                    'Invalid value for the form class name (current value: "' . $formClassName . '"); it must be an instance of "' . FormInterface::class . '".',
419
                    1457442462
420
                );
421
            }
422
423
            $this->formObjectClassName = $formClassName;
424
        }
425
426
        return $this->formObjectClassName;
427
    }
428
429
    /**
430
     * @param PageRenderer $pageRenderer
431
     */
432
    public function injectPageRenderer(PageRenderer $pageRenderer)
433
    {
434
        $this->pageRenderer = $pageRenderer;
435
    }
436
437
    /**
438
     * @param FormObjectFactory $formObjectFactory
439
     */
440
    public function injectFormObjectFactory(FormObjectFactory $formObjectFactory)
441
    {
442
        $this->formObjectFactory = $formObjectFactory;
443
    }
444
445
    /**
446
     * @param FormService $service
447
     */
448
    public function injectFormService(FormService $service)
449
    {
450
        $this->formService = $service;
451
    }
452
}
453