Completed
Push — unit-test-view-helpers ( d3d368...2e4ad4 )
by Romain
02:57
created

FormViewHelper::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
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\Utility\TimeTracker;
23
use Romm\Formz\Validation\Validator\Form\AbstractFormValidator;
24
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
25
use Romm\Formz\ViewHelpers\Service\FormzViewHelperServiceInjectionTrait;
26
use TYPO3\CMS\Core\Page\PageRenderer;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Extbase\Error\Result;
29
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
30
use TYPO3\CMS\Fluid\View\StandaloneView;
31
32
/**
33
 * This view helper overrides the default one from Extbase, to include
34
 * everything the extension needs to work properly.
35
 *
36
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
37
 * and must be the exact same name as the form parameter in the controller
38
 * action called when the form is submitted. For instance, if your action looks
39
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
40
 * then the "name" attribute of this view helper must be "exampleForm".
41
 *
42
 * Thanks to the information of the form, the following things are automatically
43
 * handled in this view helper:
44
 *
45
 * - Class
46
 *   A custom class may be added to the form DOM element. If the TypoScript
47
 *   configuration "settings.defaultClass" is set for this form, then the given
48
 *   class will be added to the form element.
49
 *
50
 * - JavaScript
51
 *   A block of JavaScript is built from scratch, which will initialize the
52
 *   form, add validation rules to the fields, and handle activation of the
53
 *   fields validation.
54
 *
55
 * - Data attributes
56
 *   To help integrators customize every aspect they need in CSS, every useful
57
 *   information is put in data attributes in the form DOM element. For example,
58
 *   you can know in real time if the field "email" is valid if the form has the
59
 *   attribute "formz-valid-email"
60
 *
61
 * - CSS
62
 *   A block of CSS is built from scratch, which will handle the fields display,
63
 *   depending on their activation property.
64
 */
65
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
66
{
67
    use FormzViewHelperServiceInjectionTrait;
68
69
    /**
70
     * @var PageRenderer
71
     */
72
    protected $pageRenderer;
73
74
    /**
75
     * @var string
76
     */
77
    protected $formObjectClassName;
78
79
    /**
80
     * @var AssetHandlerFactory
81
     */
82
    protected $assetHandlerFactory;
83
84
    /**
85
     * @var TimeTracker
86
     */
87
    protected $timeTracker;
88
89
    /**
90
     * @inheritdoc
91
     */
92
    public function initialize()
93
    {
94
        parent::initialize();
95
96
        /*
97
         * Important: we need to instantiate the page renderer with this instead
98
         * of Extbase object manager (or with an inject function).
99
         *
100
         * This is due to some TYPO3 low level behaviour which overrides the
101
         * page renderer singleton instance, whenever a new request is used. The
102
         * problem is that the instance is not updated on Extbase side.
103
         *
104
         * Using Extbase injection can lead to old page renderer instance being
105
         * used, resulting in a leak of assets inclusion, and maybe more issues.
106
         */
107
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function initializeArguments()
114
    {
115
        parent::initializeArguments();
116
117
        // The name attribute becomes mandatory.
118
        $this->overrideArgument('name', 'string', 'Name of the form', true);
119
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
120
    }
121
122
    /**
123
     * Render the form.
124
     *
125
     * @return string
126
     */
127
    /** @noinspection PhpSignatureMismatchDuringInheritanceInspection */
128
    public function render()
129
    {
130
        $this->timeTracker = TimeTracker::getAndStart();
131
        $result = '';
132
133
        if (false === Core::get()->isTypoScriptIncluded()) {
134
            if (Core::get()->isInDebugMode()) {
135
                $result = Core::get()->translate('form.typoscript_not_included.error_message');
136
            }
137
        } else {
138
            $formObject = Core::get()
139
                ->getFormObjectFactory()
140
                ->getInstanceFromClassName($this->getFormObjectClassName(), $this->getFormObjectName());
141
142
            $this->service->setFormObject($formObject);
143
            $formzValidationResult = $formObject->getConfigurationValidationResult();
144
145
            if ($formzValidationResult->hasErrors()) {
146
                // If the form configuration is not valid, we display the errors list.
147
                $result = $this->getErrorText($formzValidationResult);
148
            } else {
149
                // Everything is ok, we render the form.
150
                $result = $this->renderForm(func_get_args());
151
            }
152
153
            unset($formzValidationResult);
154
        }
155
156
        $this->timeTracker->logTime('final');
157
        $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
158
        unset($this->timeTracker);
159
160
        $this->service->resetState();
161
162
        return $result;
163
    }
164
165
    /**
166
     * Will render the whole form and return the HTML result.
167
     *
168
     * @param array $arguments
169
     * @return string
170
     */
171
    final protected function renderForm(array $arguments)
172
    {
173
        $this->timeTracker->logTime('post-config');
174
175
        $this->assetHandlerFactory = AssetHandlerFactory::get($this->service->getFormObject(), $this->controllerContext);
176
177
        $this->setObjectAndRequestResult()
178
            ->applyBehavioursOnSubmittedForm()
179
            ->addDefaultClass()
180
            ->handleDataAttributes();
181
182
        $assetHandlerConnectorManager = AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
183
        $assetHandlerConnectorManager->includeDefaultAssets();
184
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
185
            ->generateAndIncludeFormzConfigurationJavaScript()
186
            ->generateAndIncludeJavaScript()
187
            ->generateAndIncludeInlineJavaScript()
188
            ->includeJavaScriptValidationAndConditionFiles();
189
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()->includeGeneratedCss();
190
191
        $this->timeTracker->logTime('pre-render');
192
193
        // Renders the whole Fluid template.
194
        $result = call_user_func_array([get_parent_class(), 'render'], $arguments);
195
196
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()->includeLanguageJavaScriptFiles();
197
198
        return $result;
199
    }
200
201
    /**
202
     * This function will inject in the variable container the instance of form
203
     * and its submission result. There are only two ways to be sure the values
204
     * injected are correct: when the form has actually been submitted by the
205
     * user, or when the view helper argument `object` is filled.
206
     *
207
     * @return $this
208
     */
209
    protected function setObjectAndRequestResult()
210
    {
211
        $this->service->activateFormContext();
212
213
        $originalRequest = $this->controllerContext
214
            ->getRequest()
215
            ->getOriginalRequest();
216
217
        if (null !== $originalRequest
218
            && $originalRequest->hasArgument($this->getFormObjectName())
219
        ) {
220
            /** @var array $formInstance */
221
            $formInstance = $originalRequest->getArgument($this->getFormObjectName());
222
223
            $formRequestResult = AbstractFormValidator::getFormValidationResult(
224
                $this->getFormObjectClassName(),
225
                $this->getFormObjectName()
226
            );
227
228
            $this->service->setFormInstance($formInstance);
229
            $this->service->setFormResult($formRequestResult);
230
            $this->service->markFormAsSubmitted();
231
        } elseif (null !== $this->arguments['object']) {
232
            $formInstance = $this->arguments['object'];
233
234
            /*
235
             * @todo: pas forcément un DefaultFormValidator: comment je gère ça?
236
             * + ça prend quand même un peu de temps cette manière. Peut-on faire autrement ?
237
             */
238
            /** @var DefaultFormValidator $formValidator */
239
            $formValidator = GeneralUtility::makeInstance(
240
                DefaultFormValidator::class,
241
                ['name' => $this->getFormObjectName()]
242
            );
243
            $formRequestResult = $formValidator->validate($formInstance);
244
245
            $this->service->setFormInstance($formInstance);
246
            $this->service->setFormResult($formRequestResult);
247
        }
248
249
        return $this;
250
    }
251
252
    /**
253
     * Will loop on the submitted form fields and apply behaviours if their
254
     * configuration contains.
255
     *
256
     * @return $this
257
     */
258
    protected function applyBehavioursOnSubmittedForm()
259
    {
260
        $originalRequest = $this->controllerContext
261
            ->getRequest()
262
            ->getOriginalRequest();
263
264
        if ($this->service->formWasSubmitted()) {
265
            /** @var BehavioursManager $behavioursManager */
266
            $behavioursManager = GeneralUtility::makeInstance(BehavioursManager::class);
267
268
            $formProperties = $behavioursManager->applyBehaviourOnPropertiesArray(
269
                $this->service->getFormInstance(),
270
                $this->service->getFormObject()->getConfiguration()
271
            );
272
273
            $originalRequest->setArgument($this->getFormObjectName(), $formProperties);
274
        }
275
276
        return $this;
277
    }
278
279
    /**
280
     * Will add a default class to the form element.
281
     *
282
     * To customize the class, take a look at `settings.defaultClass` in the
283
     * form TypoScript configuration.
284
     *
285
     * @return $this
286
     */
287
    protected function addDefaultClass()
288
    {
289
        $formDefaultClass = $this->service
290
            ->getFormObject()
291
            ->getConfiguration()
292
            ->getSettings()
293
            ->getDefaultClass();
294
295
        $class = $this->tag->getAttribute('class');
296
297
        if (false === empty($formDefaultClass)) {
298
            $class = ((!empty($class)) ? $class . ' ' : '') . $formDefaultClass;
299
        }
300
301
        $this->tag->addAttribute('class', $class);
302
303
        return $this;
304
    }
305
306
    /**
307
     * Adds custom data attributes to the form element, based on the
308
     * submitted form values and results.
309
     *
310
     * @return $this
311
     */
312
    protected function handleDataAttributes()
313
    {
314
        $object = $this->service->getFormInstance();
315
        $formResult = $this->service->getFormResult();
316
317
        /** @var DataAttributesAssetHandler $dataAttributesAssetHandler */
318
        $dataAttributesAssetHandler =  $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
319
320
        $dataAttributes = [];
321
        if ($object) {
322
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($object, $formResult);
323
        }
324
325
        if ($formResult) {
326
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes($formResult);
327
328
            if (true === $this->service->formWasSubmitted()) {
329
                $dataAttributes += ['formz-submission-done' => '1'];
330
                $dataAttributes += $dataAttributesAssetHandler->getFieldsErrorsDataAttributes($formResult);
331
            }
332
        }
333
334
        foreach ($dataAttributes as $attributeName => $attributeValue) {
335
            $this->tag->addAttribute($attributeName, $attributeValue);
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * Will return an error text from a Fluid view.
343
     *
344
     * @param Result $result
345
     * @return string
346
     */
347
    protected function getErrorText(Result $result)
348
    {
349
        /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
350
        $view = $this->objectManager->get(StandaloneView::class);
351
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
352
        $layoutRootPath = Core::get()->getExtensionRelativePath('Resources/Private/Layouts');
353
        $view->setLayoutRootPaths([$layoutRootPath]);
354
        $view->assign('result', $result);
355
356
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . Core::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
357
        $this->pageRenderer->addCssFile(Core::get()->getResourceRelativePath($templatePath));
358
359
        return $view->render();
360
    }
361
362
    /**
363
     * Returns the class name of the form object: it is fetched from the action
364
     * of the controller which will be called when submitting this form. It
365
     * means two things:
366
     * - The action must have a parameter which has the exact same name as the
367
     *   form.
368
     * - The parameter must indicate its type.
369
     *
370
     * @return null|string
371
     * @throws \Exception
372
     */
373
    protected function getFormObjectClassName()
374
    {
375
        if (null === $this->formObjectClassName) {
376
            $request = $this->controllerContext->getRequest();
377
            $controllerObjectName = $request->getControllerObjectName();
378
            $actionName = ($this->arguments['action']) ?: $request->getControllerActionName();
379
            $actionName = $actionName . 'Action';
380
381
            if ($this->hasArgument('formClassName')) {
382
                $formClassName = $this->arguments['formClassName'];
383
            } else {
384
                /** @var ReflectionService $reflectionService */
385
                $reflectionService = $this->objectManager->get(ReflectionService::class);
386
                $methodParameters = $reflectionService->getMethodParameters($controllerObjectName, $actionName);
387
388
                if (false === isset($methodParameters[$this->getFormObjectName()])) {
389
                    throw new \Exception(
390
                        'The method "' . $controllerObjectName . '::' . $actionName . '()" must have a parameter "$' . $this->getFormObjectName() . '". Note that you can also change the parameter "name" of the form view helper.',
391
                        1457441846
392
                    );
393
                }
394
395
                $formClassName = $methodParameters[$this->getFormObjectName()]['type'];
396
            }
397
398
            if (false === class_exists($formClassName)) {
399
                throw new \Exception(
400
                    '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 . '()".',
401
                    1457442014
402
                );
403
            }
404
405
            if (false === in_array(FormInterface::class, class_implements($formClassName))) {
406
                throw new \Exception(
407
                    'Invalid value for the form class name (current value: "' . $formClassName . '"); it must be an instance of "' . FormInterface::class . '".',
408
                    1457442462
409
                );
410
            }
411
412
            $this->formObjectClassName = $formClassName;
413
        }
414
415
        return $this->formObjectClassName;
416
    }
417
418
    /**
419
     * @param PageRenderer $pageRenderer
420
     */
421
    public function injectPageRenderer(PageRenderer $pageRenderer)
422
    {
423
        $this->pageRenderer = $pageRenderer;
424
    }
425
}
426