Completed
Pull Request — development (#86)
by Philippe
03:27
created

FormViewHelper::injectFormzCollector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
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\Core\Core;
20
use Romm\Formz\DataCollectors\FormzCollector;
21
use Romm\Formz\Exceptions\ClassNotFoundException;
22
use Romm\Formz\Exceptions\EntryNotFoundException;
23
use Romm\Formz\Exceptions\InvalidOptionValueException;
24
use Romm\Formz\Form\FormInterface;
25
use Romm\Formz\Form\FormObject;
26
use Romm\Formz\Form\FormObjectFactory;
27
use Romm\Formz\Service\ContextService;
28
use Romm\Formz\Service\ControllerService;
29
use Romm\Formz\Service\ExtensionService;
30
use Romm\Formz\Service\StringService;
31
use Romm\Formz\Service\TimeTrackerService;
32
use Romm\Formz\Service\ViewHelper\Form\FormViewHelperService;
33
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
34
use TYPO3\CMS\Core\Page\PageRenderer;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
use TYPO3\CMS\Extbase\Error\Result;
37
use TYPO3\CMS\Fluid\View\StandaloneView;
38
39
/**
40
 * This view helper overrides the default one from Extbase, to include
41
 * everything the extension needs to work properly.
42
 *
43
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
44
 * and must be the exact same name as the form parameter in the controller
45
 * action called when the form is submitted. For instance, if your action looks
46
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
47
 * then the "name" attribute of this view helper must be "exampleForm".
48
 *
49
 * Thanks to the information of the form, the following things are automatically
50
 * handled in this view helper:
51
 *
52
 * - Class
53
 *   A custom class may be added to the form DOM element. If the TypoScript
54
 *   configuration "settings.defaultClass" is set for this form, then the given
55
 *   class will be added to the form element.
56
 *
57
 * - JavaScript
58
 *   A block of JavaScript is built from scratch, which will initialize the
59
 *   form, add validation rules to the fields, and handle activation of the
60
 *   fields validation.
61
 *
62
 * - Data attributes
63
 *   To help integrators customize every aspect they need in CSS, every useful
64
 *   information is put in data attributes in the form DOM element. For example,
65
 *   you can know in real time if the field "email" is valid if the form has the
66
 *   attribute "fz-valid-email"
67
 *
68
 * - CSS
69
 *   A block of CSS is built from scratch, which will handle the fields display,
70
 *   depending on their activation property.
71
 */
72
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
73
{
74
    /**
75
     * @var bool
76
     */
77
    protected $escapeOutput = false;
78
79
    /**
80
     * @var PageRenderer
81
     */
82
    protected $pageRenderer;
83
84
    /**
85
     * @var FormObject
86
     */
87
    protected $formObject;
88
89
    /**
90
     * @var FormViewHelperService
91
     */
92
    protected $formService;
93
94
    /**
95
     * @var string
96
     */
97
    protected $formObjectClassName;
98
99
    /**
100
     * @var AssetHandlerFactory
101
     */
102
    protected $assetHandlerFactory;
103
104
    /**
105
     * @var TimeTrackerService
106
     */
107
    protected $timeTracker;
108
109
    /**
110
     * @var bool
111
     */
112
    protected $typoScriptIncluded = false;
113
114
    /**
115
     * @var ControllerService
116
     */
117
    protected $controllerService;
118
119
    /**
120
     * @var \Romm\Formz\DataCollectors\FormzCollector
121
     */
122
    protected $formzCollector;
123
124
    /**
125
     * @inheritdoc
126
     */
127
    public function initialize()
128
    {
129
        parent::initialize();
130
131
        $this->typoScriptIncluded = ContextService::get()->isTypoScriptIncluded();
132
133
        if (true === $this->typoScriptIncluded) {
134
            $this->timeTracker = TimeTrackerService::getAndStart();
135
            $this->formObjectClassName = $this->getFormClassName();
136
            $this->formObject = $this->getFormObject();
137
            $this->timeTracker->logTime('post-config');
138
            $this->formService->setFormObject($this->formObject);
139
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
140
141
            /*
142
             * If the argument `object` was filled with an instance of Form, it
143
             * is added to the `FormObject`.
144
             */
145
            $objectArgument = $this->getFormObjectArgument();
146
147
            if (null !== $objectArgument
148
                && false === $this->formObject->formWasSubmitted()
149
            ) {
150
                $this->formObject->setForm($objectArgument);
151
            }
152
        }
153
154
        /*
155
         * Important: we need to instantiate the page renderer with this instead
156
         * of Extbase object manager (or with an inject function).
157
         *
158
         * This is due to some TYPO3 low level behaviour which overrides the
159
         * page renderer singleton instance, whenever a new request is used. The
160
         * problem is that the instance is not updated on Extbase side.
161
         *
162
         * Using Extbase injection can lead to old page renderer instance being
163
         * used, resulting in a leak of assets inclusion, and maybe more issues.
164
         */
165
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
166
    }
167
168
    /**
169
     * @inheritdoc
170
     */
171
    public function initializeArguments()
172
    {
173
        parent::initializeArguments();
174
175
        // The name attribute becomes mandatory.
176
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
177
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
178
    }
179
180
    /**
181
     * @return string
182
     */
183
    protected function renderViewHelper()
184
    {
185
        if (false === $this->typoScriptIncluded) {
186
            return (ExtensionService::get()->isInDebugMode())
187
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
188
                : '';
189
        }
190
191
        $formzValidationResult = $this->formObject->getConfigurationValidationResult();
192
193
        $result = ($formzValidationResult->hasErrors())
194
            // If the form configuration is not valid, we display the errors list.
195
            ? $this->getErrorText($formzValidationResult)
196
            // Everything is ok, we render the form.
197
            : $this->renderForm(func_get_args());
198
199
        $this->timeTracker->logTime('final');
200
201
        if (ExtensionService::get()->isInDebugMode()) {
202
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
203
        }
204
205
        $this->formService->resetState();
206
207
        return $result;
208
    }
209
210
    /**
211
     * Will render the whole form and return the HTML result.
212
     *
213
     * @param array $arguments
214
     * @return string
215
     */
216
    final protected function renderForm(array $arguments)
217
    {
218
        /*
219
         * We begin by setting up the form service: request results and form
220
         * instance are inserted in the service, and are used afterwards.
221
         *
222
         * There are only two ways to be sure the values injected are correct:
223
         * when the form was actually submitted by the user, or when the
224
         * argument `object` of the view helper is filled with a form instance.
225
         */
226
        $this->formService->activateFormContext();
227
228
        /*
229
         * If the form was submitted, applying custom behaviours on its fields.
230
         */
231
        $this->formService->applyBehavioursOnSubmittedForm($this->controllerContext);
232
233
        /*
234
         * Adding the default class configured in TypoScript configuration to
235
         * the form HTML tag.
236
         */
237
        $this->addDefaultClass();
238
239
        /*
240
         * Handling data attributes that are added to the form HTML tag,
241
         * depending on several parameters.
242
         */
243
        $this->handleDataAttributes();
244
245
        /*
246
         * Including JavaScript and CSS assets in the page renderer.
247
         */
248
        $this->handleAssets();
249
250
        $this->timeTracker->logTime('pre-render');
251
252
        /*
253
         * Getting the result of the original Fluid `FormViewHelper` rendering.
254
         */
255
        $result = $this->getParentRenderResult($arguments);
256
257
        /*
258
         * Language files need to be included at the end, because they depend on
259
         * what was used by previous assets.
260
         */
261
        $this->getAssetHandlerConnectorManager()
262
            ->getJavaScriptAssetHandlerConnector()
263
            ->includeLanguageJavaScriptFiles();
264
265
        return $result;
266
    }
267
268
    /**
269
     * Will add a default class to the form element.
270
     *
271
     * To customize the class, take a look at `settings.defaultClass` in the
272
     * form TypoScript configuration.
273
     */
274
    protected function addDefaultClass()
275
    {
276
        $formDefaultClass = $this->formObject
277
            ->getConfiguration()
278
            ->getSettings()
279
            ->getDefaultClass();
280
281
        $class = $this->tag->getAttribute('class');
282
283
        if (false === empty($formDefaultClass)) {
284
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
285
            $this->tag->addAttribute('class', $class);
286
        }
287
    }
288
289
    /**
290
     * Adds custom data attributes to the form element, based on the
291
     * submitted form values and results.
292
     */
293
    protected function handleDataAttributes()
294
    {
295
        $dataAttributes = [];
296
297
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
298
299
        if ($this->formObject->hasForm()) {
300
            if (false === $this->formObject->hasFormResult()) {
301
                $form = $this->formObject->getForm();
302
                $formValidator = $this->getFormValidator($this->getFormObjectName());
303
                $formResult = $formValidator->validateGhost($form);
304
            } else {
305
                $formResult = $this->formObject->getFormResult();
306
            }
307
308
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($formResult);
309
        }
310
311
        if (true === $this->formObject->formWasSubmitted()) {
312
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
313
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes();
314
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes();
315
        }
316
317
        $this->tag->addAttributes($dataAttributes);
318
    }
319
320
    /**
321
     * Will include all JavaScript and CSS assets needed for this form.
322
     */
323
    protected function handleAssets()
324
    {
325
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
326
327
        // Default FormZ assets.
328
        $assetHandlerConnectorManager->includeDefaultAssets();
329
330
        // JavaScript assets.
331
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
332
            ->generateAndIncludeFormzConfigurationJavaScript()
333
            ->generateAndIncludeJavaScript()
334
            ->generateAndIncludeInlineJavaScript()
335
            ->includeJavaScriptValidationAndConditionFiles();
336
337
        // CSS assets.
338
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
339
            ->includeGeneratedCss();
340
    }
341
342
    /**
343
     * Will return an error text from a Fluid view.
344
     *
345
     * @param Result $result
346
     * @return string
347
     */
348
    protected function getErrorText(Result $result)
349
    {
350
        /** @var $view StandaloneView */
351
        $view = Core::instantiate(StandaloneView::class);
352
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
353
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
354
        $view->setLayoutRootPaths([$layoutRootPath]);
355
        $view->assign('result', $result);
356
357
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
358
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
359
360
        return $view->render();
361
    }
362
363
    /**
364
     * Checks the type of the argument `object`, and returns it if everything is
365
     * ok.
366
     *
367
     * @return FormInterface|null
368
     * @throws InvalidOptionValueException
369
     */
370
    protected function getFormObjectArgument()
371
    {
372
        $objectArgument = $this->arguments['object'];
373
374
        if (null === $objectArgument) {
375
            return null;
376
        }
377
378
        if (false === is_object($objectArgument)) {
379
            throw InvalidOptionValueException::formViewHelperWrongFormValueType($objectArgument);
380
        }
381
382
        if (false === $objectArgument instanceof FormInterface) {
383
            throw InvalidOptionValueException::formViewHelperWrongFormValueObjectType($objectArgument);
384
        }
385
386
        $formClassName = $this->getFormClassName();
387
388
        if (false === $objectArgument instanceof $formClassName) {
389
            throw InvalidOptionValueException::formViewHelperWrongFormValueClassName($formClassName, $objectArgument);
390
        }
391
392
        return $objectArgument;
393
    }
394
395
    /**
396
     * Returns the class name of the form object: it is fetched from the action
397
     * of the controller which will be called when submitting this form. It
398
     * means two things:
399
     * - The action must have a parameter which has the exact same name as the
400
     *   form;
401
     * - The parameter must indicate its type.
402
     *
403
     * @return string
404
     * @throws ClassNotFoundException
405
     * @throws InvalidOptionValueException
406
     */
407
    protected function getFormClassName()
408
    {
409
        $formClassName = ($this->hasArgument('formClassName'))
410
            ? $this->arguments['formClassName']
411
            : $this->getFormClassNameFromControllerAction();
412
413
        if (false === class_exists($formClassName)) {
414
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
415
        }
416
417
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
418
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
419
        }
420
421
        return $formClassName;
422
    }
423
424
    /**
425
     * Will fetch the name of the controller action argument bound to this
426
     * request.
427
     *
428
     * @return string
429
     * @throws EntryNotFoundException
430
     */
431
    protected function getFormClassNameFromControllerAction()
432
    {
433
        return $this->controllerService->getFormClassNameFromControllerAction(
434
            $this->getControllerName(),
435
            $this->getControllerActionName(),
436
            $this->getFormObjectName()
437
        );
438
    }
439
440
    /**
441
     * Renders the whole Fluid template.
442
     *
443
     * @param array $arguments
444
     * @return string
445
     */
446
    protected function getParentRenderResult(array $arguments)
447
    {
448
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
449
    }
450
451
    /**
452
     * @return string
453
     */
454
    protected function getControllerName()
455
    {
456
        return ($this->arguments['controller'])
457
            ?: $this->controllerContext
458
                ->getRequest()
459
                ->getControllerObjectName();
460
    }
461
462
    /**
463
     * @return string
464
     */
465
    protected function getControllerActionName()
466
    {
467
        return ($this->arguments['action'])
468
            ?: $this->controllerContext
469
                ->getRequest()
470
                ->getControllerActionName();
471
    }
472
473
    /**
474
     * @param string $formName
475
     * @return DefaultFormValidator
476
     */
477
    protected function getFormValidator($formName)
478
    {
479
        /** @var DefaultFormValidator $validation */
480
        $validation = Core::instantiate(DefaultFormValidator::class, ['name' => $formName]);
481
482
        return $validation;
483
    }
484
485
    /**
486
     * @return AssetHandlerConnectorManager
487
     */
488
    protected function getAssetHandlerConnectorManager()
489
    {
490
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
491
    }
492
493
    /**
494
     * @return DataAttributesAssetHandler
495
     */
496
    protected function getDataAttributesAssetHandler()
497
    {
498
        /** @var DataAttributesAssetHandler $assetHandler */
499
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
500
501
        return $assetHandler;
502
    }
503
504
    /**
505
     * @return FormObject
506
     */
507
    protected function getFormObject()
508
    {
509
        /** @var FormObjectFactory $formObjectFactory */
510
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
511
512
        return $formObjectFactory->getInstanceFromClassName($this->formObjectClassName, $this->getFormObjectName());
513
    }
514
515
    /**
516
     * @param FormViewHelperService $service
517
     */
518
    public function injectFormService(FormViewHelperService $service)
519
    {
520
        $this->formService = $service;
521
    }
522
523
    /**
524
     * @param ControllerService $controllerService
525
     */
526
    public function injectControllerService(ControllerService $controllerService)
527
    {
528
        $this->controllerService = $controllerService;
529
    }
530
531
    /**
532
     * @param FormzCollector $formzCollector
533
     */
534
    public function injectFormzCollector(FormzCollector $formzCollector) {
535
        $this->formzCollector = $formzCollector;
536
    }
537
}
538