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