Completed
Push — middleware-wip ( 5dac77...4ea7b3 )
by Romain
02:46
created

FormViewHelper::getFormObjectArgument()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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