Completed
Push — tmp ( 6dfc1b )
by Romain
03:10
created

FormViewHelper::getFormInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
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\Core\Core;
20
use Romm\Formz\Exceptions\ClassNotFoundException;
21
use Romm\Formz\Exceptions\InvalidOptionValueException;
22
use Romm\Formz\Form\FormInterface;
23
use Romm\Formz\Form\FormObject\FormObject;
24
use Romm\Formz\Form\FormObject\FormObjectFactory;
25
use Romm\Formz\Service\ContextService;
26
use Romm\Formz\Service\ControllerService;
27
use Romm\Formz\Service\ExtensionService;
28
use Romm\Formz\Service\FormService;
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
130
            $this->formObjectClassName = $this->getFormClassName();
131
            $this->formObject = $this->getFormObject($this->getFormInstance());
132
133
            $this->timeTracker->logTime('post-config');
134
135
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
136
137
            $this->formService->setFormObject($this->formObject);
138
        }
139
140
        /*
141
         * Important: we need to instantiate the page renderer with this instead
142
         * of Extbase object manager (or with an inject function).
143
         *
144
         * This is due to some TYPO3 low level behaviour which overrides the
145
         * page renderer singleton instance, whenever a new request is used. The
146
         * problem is that the instance is not updated on Extbase side.
147
         *
148
         * Using Extbase injection can lead to old page renderer instance being
149
         * used, resulting in a leak of assets inclusion, and maybe more issues.
150
         */
151
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function initializeArguments()
158
    {
159
        parent::initializeArguments();
160
161
        // The name attribute becomes mandatory.
162
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
163
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
164
    }
165
166
    /**
167
     * @return string
168
     */
169
    protected function renderViewHelper()
170
    {
171
        if (false === $this->typoScriptIncluded) {
172
            return (ExtensionService::get()->isInDebugMode())
173
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
174
                : '';
175
        }
176
177
        $formzValidationResult = $this->formObject->getDefinitionValidationResult();
178
179
        $result = ($formzValidationResult->hasErrors())
180
            // If the form configuration is not valid, we display the errors list.
181
            ? $this->getErrorText($formzValidationResult)
182
            // Everything is ok, we render the form.
183
            : $this->renderForm(func_get_args());
184
185
        $this->timeTracker->logTime('final');
186
187
        if (ExtensionService::get()->isInDebugMode()) {
188
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
189
        }
190
191
        $this->formService->resetState();
192
193
        return $result;
194
    }
195
196
    /**
197
     * Will render the whole form and return the HTML result.
198
     *
199
     * @param array $arguments
200
     * @return string
201
     */
202
    final protected function renderForm(array $arguments)
203
    {
204
        /*
205
         * We begin by setting up the form service: request results and form
206
         * instance are inserted in the service, and are used afterwards.
207
         *
208
         * There are only two ways to be sure the values injected are correct:
209
         * when the form was actually submitted by the user, or when the
210
         * argument `object` of the view helper is filled with a form instance.
211
         */
212
        $this->formService->activateFormContext();
213
214
        /*
215
         * If the form was submitted, applying custom behaviours on its fields.
216
         */
217
        $this->formService->applyBehavioursOnSubmittedForm($this->controllerContext);
218
219
        /*
220
         * Adding the default class configured in TypoScript configuration to
221
         * the form HTML tag.
222
         */
223
        $this->addDefaultClass();
224
225
        /*
226
         * Handling data attributes that are added to the form HTML tag,
227
         * depending on several parameters.
228
         */
229
        $this->handleDataAttributes();
230
231
        /*
232
         * Including JavaScript and CSS assets in the page renderer.
233
         */
234
        $this->handleAssets();
235
236
        $this->timeTracker->logTime('pre-render');
237
238
        /*
239
         * Getting the result of the original Fluid `FormViewHelper` rendering.
240
         */
241
        $result = $this->getParentRenderResult($arguments);
242
243
        /*
244
         * Language files need to be included at the end, because they depend on
245
         * what was used by previous assets.
246
         */
247
        $this->getAssetHandlerConnectorManager()
248
            ->getJavaScriptAssetHandlerConnector()
249
            ->includeLanguageJavaScriptFiles();
250
251
        return $result;
252
    }
253
254
    /**
255
     * Will add a default class to the form element.
256
     *
257
     * To customize the class, take a look at `settings.defaultClass` in the
258
     * form TypoScript configuration.
259
     */
260
    protected function addDefaultClass()
261
    {
262
        $formDefaultClass = $this->formObject
263
            ->getDefinition()
264
            ->getSettings()
265
            ->getDefaultClass();
266
267
        $class = $this->tag->getAttribute('class');
268
269
        if (false === empty($formDefaultClass)) {
270
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
271
            $this->tag->addAttribute('class', $class);
272
        }
273
    }
274
275
    /**
276
     * Adds custom data attributes to the form element, based on the
277
     * submitted form values and results.
278
     */
279
    protected function handleDataAttributes()
280
    {
281
        $dataAttributes = [];
282
283
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
284
285
        if ($this->formObject->hasForm()) {
286
            if (false === $this->formObject->formWasValidated()) {
287
                $form = $this->formObject->getForm();
288
                $formValidator = $this->getFormValidator($this->getFormObjectName());
289
                $formResult = $formValidator->validateGhost($form);
290
            } else {
291
                $formResult = $this->formObject->getFormResult();
292
            }
293
294
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($formResult);
295
        }
296
297
        if (true === $this->formObject->formWasSubmitted()) {
298
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
299
        }
300
301
        if (true === $this->formObject->formWasValidated()) {
302
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes();
303
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes();
304
        }
305
306
        $this->tag->addAttributes($dataAttributes);
307
    }
308
309
    /**
310
     * Will include all JavaScript and CSS assets needed for this form.
311
     */
312
    protected function handleAssets()
313
    {
314
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
315
316
        // Default FormZ assets.
317
        $assetHandlerConnectorManager->includeDefaultAssets();
318
319
        // JavaScript assets.
320
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
321
            ->generateAndIncludeFormzConfigurationJavaScript()
322
            ->generateAndIncludeJavaScript()
323
            ->generateAndIncludeInlineJavaScript()
324
            ->includeJavaScriptValidationAndConditionFiles();
325
326
        // CSS assets.
327
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
328
            ->includeGeneratedCss();
329
    }
330
331
    /**
332
     * Will return an error text from a Fluid view.
333
     *
334
     * @param Result $result
335
     * @return string
336
     */
337
    protected function getErrorText(Result $result)
338
    {
339
        /** @var $view StandaloneView */
340
        $view = Core::instantiate(StandaloneView::class);
341
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
342
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
343
        $view->setLayoutRootPaths([$layoutRootPath]);
344
        $view->assign('result', $result);
345
346
        $templatePath = GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Public/StyleSheets/Form.ErrorBlock.css');
347
        $this->pageRenderer->addCssFile(StringService::get()->getResourceRelativePath($templatePath));
348
349
        return $view->render();
350
    }
351
352
    /**
353
     * Checks the type of the argument `object`, and returns it if everything is
354
     * ok.
355
     *
356
     * @return FormInterface|null
357
     * @throws InvalidOptionValueException
358
     */
359
    protected function getFormObjectArgument()
360
    {
361
        $objectArgument = $this->arguments['object'];
362
363
        if (null === $objectArgument) {
364
            return null;
365
        }
366
367
        if (false === is_object($objectArgument)) {
368
            throw InvalidOptionValueException::formViewHelperWrongFormValueType($objectArgument);
369
        }
370
371
        if (false === $objectArgument instanceof FormInterface) {
372
            throw InvalidOptionValueException::formViewHelperWrongFormValueObjectType($objectArgument);
373
        }
374
375
        $formClassName = $this->getFormClassName();
376
377
        if (false === $objectArgument instanceof $formClassName) {
378
            throw InvalidOptionValueException::formViewHelperWrongFormValueClassName($formClassName, $objectArgument);
379
        }
380
381
        return $objectArgument;
382
    }
383
384
    /**
385
     * Returns the class name of the form object: it is fetched from the action
386
     * of the controller which will be called when submitting this form. It
387
     * means two things:
388
     * - The action must have a parameter which has the exact same name as the
389
     *   form;
390
     * - The parameter must indicate its type.
391
     *
392
     * @return string
393
     * @throws ClassNotFoundException
394
     * @throws InvalidOptionValueException
395
     */
396
    protected function getFormClassName()
397
    {
398
        $formClassName = ($this->hasArgument('formClassName'))
399
            ? $this->arguments['formClassName']
400
            : $this->getFormClassNameFromControllerAction();
401
402
        if (false === class_exists($formClassName)) {
403
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
404
        }
405
406
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
407
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
408
        }
409
410
        return $formClassName;
411
    }
412
413
    /**
414
     * Will fetch the name of the controller action argument bound to this
415
     * request.
416
     *
417
     * @return string
418
     */
419
    protected function getFormClassNameFromControllerAction()
420
    {
421
        return $this->controllerService->getFormClassNameFromControllerAction(
422
            $this->getControllerName(),
423
            $this->getControllerActionName(),
424
            $this->getFormObjectName()
425
        );
426
    }
427
428
    /**
429
     * Renders the whole Fluid template.
430
     *
431
     * @param array $arguments
432
     * @return string
433
     */
434
    protected function getParentRenderResult(array $arguments)
435
    {
436
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
437
    }
438
439
    /**
440
     * @return string
441
     */
442
    protected function getControllerName()
443
    {
444
        return ($this->arguments['controller'])
445
            ?: $this->controllerContext
446
                ->getRequest()
447
                ->getControllerObjectName();
448
    }
449
450
    /**
451
     * @return string
452
     */
453
    protected function getControllerActionName()
454
    {
455
        return ($this->arguments['action'])
456
            ?: $this->controllerContext
457
                ->getRequest()
458
                ->getControllerActionName();
459
    }
460
461
    /**
462
     * @param string $formName
463
     * @return DefaultFormValidator
464
     */
465
    protected function getFormValidator($formName)
466
    {
467
        /** @var DefaultFormValidator $validation */
468
        $validation = Core::instantiate(DefaultFormValidator::class, ['name' => $formName]);
469
470
        return $validation;
471
    }
472
473
    /**
474
     * @return AssetHandlerConnectorManager
475
     */
476
    protected function getAssetHandlerConnectorManager()
477
    {
478
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
479
    }
480
481
    /**
482
     * @return DataAttributesAssetHandler
483
     */
484
    protected function getDataAttributesAssetHandler()
485
    {
486
        /** @var DataAttributesAssetHandler $assetHandler */
487
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
488
489
        return $assetHandler;
490
    }
491
492
    /**
493
     * @return FormInterface
494
     */
495
    protected function getFormInstance()
496
    {
497
        /*
498
         * If the argument `object` was filled with an instance of Form, it
499
         * becomes the form instance. Otherwise we try to fetch an instance from
500
         * the form with errors list. If there is still no form, we create an
501
         * empty instance.
502
         */
503
        $objectArgument = $this->getFormObjectArgument();
504
505
        if ($objectArgument) {
506
            $form = $objectArgument;
507
        } else {
508
            $submittedForm = FormService::getFormWithErrors($this->getFormClassName());
0 ignored issues
show
Deprecated Code introduced by
The method Romm\Formz\Service\FormS...ce::getFormWithErrors() has been deprecated with message: This method is deprecated, please try not to use it if you can. It will be removed in FormZ v2, where you will have a whole new way to get a validated form.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
509
510
            $form = $submittedForm ?: Core::get()->getObjectManager()->getEmptyObject($this->getFormClassName());
511
        }
512
513
        return $form;
514
    }
515
516
    /**
517
     * @param FormInterface $form
518
     * @return FormObject
519
     */
520
    protected function getFormObject(FormInterface $form)
521
    {
522
        return FormObjectFactory::get()->getInstanceWithFormInstance($form, $this->getFormObjectName());
523
    }
524
525
    /**
526
     * @param FormViewHelperService $service
527
     */
528
    public function injectFormService(FormViewHelperService $service)
529
    {
530
        $this->formService = $service;
531
    }
532
533
    /**
534
     * @param ControllerService $controllerService
535
     */
536
    public function injectControllerService(ControllerService $controllerService)
537
    {
538
        $this->controllerService = $controllerService;
539
    }
540
}
541