Completed
Push — cleanup-service ( b0282b...73e309 )
by Romain
02:20
created

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