Completed
Pull Request — development (#39)
by Romain
02:14
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\Form\FormInterface;
21
use Romm\Formz\Form\FormObjectFactory;
22
use Romm\Formz\Service\ContextService;
23
use Romm\Formz\Service\ExtensionService;
24
use Romm\Formz\Service\StringService;
25
use Romm\Formz\Service\TimeTrackerService;
26
use Romm\Formz\Validation\Validator\Form\AbstractFormValidator;
27
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
28
use Romm\Formz\ViewHelpers\Service\FormzViewHelperServiceInjectionTrait;
29
use TYPO3\CMS\Core\Page\PageRenderer;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Extbase\Error\Result;
32
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
33
use TYPO3\CMS\Fluid\View\StandaloneView;
34
35
/**
36
 * This view helper overrides the default one from Extbase, to include
37
 * everything the extension needs to work properly.
38
 *
39
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
40
 * and must be the exact same name as the form parameter in the controller
41
 * action called when the form is submitted. For instance, if your action looks
42
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
43
 * then the "name" attribute of this view helper must be "exampleForm".
44
 *
45
 * Thanks to the information of the form, the following things are automatically
46
 * handled in this view helper:
47
 *
48
 * - Class
49
 *   A custom class may be added to the form DOM element. If the TypoScript
50
 *   configuration "settings.defaultClass" is set for this form, then the given
51
 *   class will be added to the form element.
52
 *
53
 * - JavaScript
54
 *   A block of JavaScript is built from scratch, which will initialize the
55
 *   form, add validation rules to the fields, and handle activation of the
56
 *   fields validation.
57
 *
58
 * - Data attributes
59
 *   To help integrators customize every aspect they need in CSS, every useful
60
 *   information is put in data attributes in the form DOM element. For example,
61
 *   you can know in real time if the field "email" is valid if the form has the
62
 *   attribute "formz-valid-email"
63
 *
64
 * - CSS
65
 *   A block of CSS is built from scratch, which will handle the fields display,
66
 *   depending on their activation property.
67
 */
68
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
69
{
70
    use FormzViewHelperServiceInjectionTrait;
71
72
    /**
73
     * @var PageRenderer
74
     */
75
    protected $pageRenderer;
76
77
    /**
78
     * @var FormObjectFactory
79
     */
80
    protected $formObjectFactory;
81
82
    /**
83
     * @var string
84
     */
85
    protected $formObjectClassName;
86
87
    /**
88
     * @var AssetHandlerFactory
89
     */
90
    protected $assetHandlerFactory;
91
92
    /**
93
     * @var TimeTrackerService
94
     */
95
    protected $timeTracker;
96
97
    /**
98
     * @inheritdoc
99
     */
100
    public function initialize()
101
    {
102
        parent::initialize();
103
104
        /*
105
         * Important: we need to instantiate the page renderer with this instead
106
         * of Extbase object manager (or with an inject function).
107
         *
108
         * This is due to some TYPO3 low level behaviour which overrides the
109
         * page renderer singleton instance, whenever a new request is used. The
110
         * problem is that the instance is not updated on Extbase side.
111
         *
112
         * Using Extbase injection can lead to old page renderer instance being
113
         * used, resulting in a leak of assets inclusion, and maybe more issues.
114
         */
115
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function initializeArguments()
122
    {
123
        parent::initializeArguments();
124
125
        // The name attribute becomes mandatory.
126
        $this->overrideArgument('name', 'string', 'Name of the form', true);
127
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
128
    }
129
130
    /**
131
     * Render the form.
132
     *
133
     * @return string
134
     */
135
    /** @noinspection PhpSignatureMismatchDuringInheritanceInspection */
136
    public function render()
137
    {
138
        $this->timeTracker = TimeTrackerService::getAndStart();
139
        $result = '';
140
141
        if (false === ContextService::get()->isTypoScriptIncluded()) {
142
            if (ExtensionService::get()->isInDebugMode()) {
143
                $result = ContextService::get()->translate('form.typoscript_not_included.error_message');
144
            }
145
        } else {
146
            $formObject = $this->formObjectFactory->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
147
148
            $this->service->setFormObject($formObject);
149
            $formzValidationResult = $formObject->getConfigurationValidationResult();
150
151
            if ($formzValidationResult->hasErrors()) {
152
                // If the form configuration is not valid, we display the errors list.
153
                $result = $this->getErrorText($formzValidationResult);
154
            } else {
155
                // Everything is ok, we render the form.
156
                $result = $this->renderForm(func_get_args());
157
            }
158
159
            unset($formzValidationResult);
160
        }
161
162
        $this->timeTracker->logTime('final');
163
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
164
        unset($this->timeTracker);
165
166
        $this->service->resetState();
167
168
        return $result;
169
    }
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->service->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->service->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->service->setFormInstance($formInstance);
235
            $this->service->setFormResult($formRequestResult);
236
            $this->service->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 = GeneralUtility::makeInstance(
246
                DefaultFormValidator::class,
247
                ['name' => $this->getFormObjectName()]
248
            );
249
            $formRequestResult = $formValidator->validate($formInstance);
250
251
            $this->service->setFormInstance($formInstance);
252
            $this->service->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->service->formWasSubmitted()) {
271
            /** @var BehavioursManager $behavioursManager */
272
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
273
274
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
275
                $this->service->getFormInstance(),
276
                $this->service->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->service
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->service->getFormInstance();
321
        $formResult = $this->service->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->service->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