Completed
Push — unit-test-services ( f70ec7...99caba )
by Romain
02:28
created

FormViewHelper::renderForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 51
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 51
rs 9.4109
c 0
b 0
f 0
cc 1
eloc 12
nc 1
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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