Completed
Push — unit-test-view-helpers ( 33f1c6...2824c7 )
by Romain
02:16
created

FormViewHelper::injectFormService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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