Completed
Push — middleware-wip ( ab9573...668c38 )
by Romain
10:14
created

FieldViewHelper::checkCurrentStep()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 14
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\Configuration\View\Layouts\Layout;
17
use Romm\Formz\Configuration\View\View;
18
use Romm\Formz\Exceptions\ContextNotFoundException;
19
use Romm\Formz\Exceptions\EntryNotFoundException;
20
use Romm\Formz\Exceptions\InvalidArgumentTypeException;
21
use Romm\Formz\Exceptions\InvalidArgumentValueException;
22
use Romm\Formz\Exceptions\PropertyNotAccessibleException;
23
use Romm\Formz\Service\StringService;
24
use Romm\Formz\Service\ViewHelper\FieldViewHelperService;
25
use Romm\Formz\Service\ViewHelper\FormViewHelperService;
26
use Romm\Formz\Service\ViewHelper\SlotViewHelperService;
27
use TYPO3\CMS\Core\Utility\ArrayUtility;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
30
use TYPO3\CMS\Extbase\Error\Error;
31
use TYPO3\CMS\Extbase\Mvc\Web\Request;
32
33
/**
34
 * This view helper is used to automatize the rendering of a field layout. It
35
 * will use the TypoScript properties at the path `config.tx_formz.view.layout`.
36
 *
37
 * You need to indicate the name of the field which will be rendered, and the
38
 * name of the layout which should be used (it must be present in the TypoScript
39
 * configuration).
40
 *
41
 * Example of layout: `bootstrap.3-cols`. You may indicate only the group, then
42
 * the name of the layout will be set to `default` (if you use the layout group
43
 * `bootstrap`, the layout `default` will be used, only if it does exist of
44
 * course).
45
 */
46
class FieldViewHelper extends AbstractViewHelper
47
{
48
    /**
49
     * @var bool
50
     */
51
    protected $escapeOutput = false;
52
53
    /**
54
     * @var array
55
     */
56
    public static $reservedVariablesNames = ['layout', 'formName', 'fieldName', 'fieldId'];
57
58
    /**
59
     * @var FormViewHelperService
60
     */
61
    protected $formService;
62
63
    /**
64
     * @var FieldViewHelperService
65
     */
66
    protected $fieldService;
67
68
    /**
69
     * @var SlotViewHelperService
70
     */
71
    protected $slotService;
72
73
    /**
74
     * @var array
75
     */
76
    protected $originalArguments = [];
77
78
    /**
79
     * @inheritdoc
80
     */
81
    public function initializeArguments()
82
    {
83
        $this->registerArgument('name', 'string', 'Name of the field which should be rendered.', true);
84
        $this->registerArgument('layout', 'string', 'Path of the TypoScript layout which will be used.', true);
85
        $this->registerArgument('arguments', 'array', 'Arbitrary arguments which will be sent to the field template.', false, []);
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public function render()
92
    {
93
        $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
94
95
        /*
96
         * First, we check if this view helper is called from within the
97
         * `FormViewHelper`, because it would not make sense anywhere else.
98
         */
99
        if (false === $this->formService->formContextExists()) {
100
            throw ContextNotFoundException::fieldViewHelperFormContextNotFound();
101
        }
102
103
        /*
104
         * Then, we inject the wanted field in the `FieldService` so we can know
105
         * later which field we're working with.
106
         */
107
        $this->injectFieldInService($this->arguments['name']);
108
109
        /*
110
         * Checking if the form is using steps; if it does, we check that the
111
         * field is supported by the current step.
112
         */
113
        $continue = $this->checkCurrentStep();
114
115
        if (false === $continue) {
116
            return '';
117
        }
118
119
        /*
120
         * Calling this here will process every view helper beneath this one,
121
         * allowing options and slots to be used correctly in the field layout.
122
         */
123
        $this->renderChildren();
124
125
        /*
126
         * We need to store original arguments declared for the current view
127
         * context, because we may override them during the rendering of this
128
         * view helper.
129
         */
130
        $this->storeOriginalArguments();
131
132
        /*
133
         * We merge the arguments with the ones registered with the
134
         * `OptionViewHelper`.
135
         */
136
        $templateArguments = $this->arguments['arguments'] ?: [];
137
        ArrayUtility::mergeRecursiveWithOverrule($templateArguments, $this->fieldService->getFieldOptions());
138
139
        $currentView = $viewHelperVariableContainer->getView();
140
141
        $result = $this->renderLayoutView($templateArguments);
142
143
        /*
144
         * Resetting all services data.
145
         */
146
        $this->fieldService->resetState();
147
        $this->slotService->resetState();
148
149
        $viewHelperVariableContainer->setView($currentView);
150
        $this->restoreOriginalArguments($templateArguments);
151
152
        return $result;
153
    }
154
155
    /**
156
     * Checking if the form is using steps; if it does, we check that the field
157
     * is supported by the current step. If not, an error is added to the result
158
     * of the form rendering, canceling the frontend rendering.
159
     *
160
     * @return bool
161
     */
162
    protected function checkCurrentStep()
163
    {
164
        $continue = true;
165
166
        /** @var Request $request */
167
        $request = $this->controllerContext->getRequest();
168
        $currentStep = $this->formService->getFormObject()->getCurrentStep($request);
169
170
        if ($currentStep) {
171
            $field = $this->fieldService->getCurrentField();
172
173
            if (false === $currentStep->supportsField($field)) {
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->fieldService->getCurrentField() on line 171 can be null; however, Romm\Formz\Configuration...epItem::supportsField() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
174
                $continue = false;
175
176
                $error = new Error(
177
                    'The field "%s" is not supported by the step "%s". Please add this field to the supported fields list in order to render it in your template.',
178
                    1491915002,
179
                    [$field->getName(), $currentStep->getName()]
180
                );
181
                $this->formService->getResult()->addError($error);
182
            }
183
        }
184
185
        return $continue;
186
    }
187
188
    /**
189
     * Will render the associated Fluid view for this field (configured with the
190
     * `layout` argument).
191
     *
192
     * @param array $templateArguments
193
     * @return string
194
     */
195
    protected function renderLayoutView(array $templateArguments)
196
    {
197
        $fieldName = $this->arguments['name'];
198
        $formObject = $this->formService->getFormObject();
199
        $formConfiguration = $formObject->getConfiguration();
200
        $viewConfiguration = $formConfiguration->getRootConfiguration()->getView();
201
        $layout = $this->getLayout($viewConfiguration);
202
203
        $templateArguments['layout'] = $layout->getLayout();
204
        $templateArguments['formName'] = $formObject->getName();
205
        $templateArguments['fieldName'] = $fieldName;
206
        $templateArguments['fieldId'] = ($templateArguments['fieldId']) ?: StringService::get()->sanitizeString('formz-' . $formObject->getName() . '-' . $fieldName);
207
208
        $view = $this->fieldService->getView($layout);
209
210
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
211
            $view->setRenderingContext($this->renderingContext);
212
        } else {
213
            $view->setControllerContext($this->controllerContext);
214
215
            $variableProvider = $this->getVariableProvider();
216
217
            foreach ($templateArguments as $key => $value) {
218
                if ($variableProvider->exists($key)) {
219
                    $variableProvider->remove($key);
220
                }
221
222
                $variableProvider->add($key, $value);
223
            }
224
        }
225
226
        $view->setLayoutRootPaths($viewConfiguration->getAbsoluteLayoutRootPaths());
227
        $view->setPartialRootPaths($viewConfiguration->getAbsolutePartialRootPaths());
228
        $view->assignMultiple($templateArguments);
229
230
        return $view->render();
231
    }
232
233
    /**
234
     * Will check that the given field exists in the current form definition and
235
     * inject it in the `FieldService` as `currentField`.
236
     *
237
     * Throws an error if the field is not found or incorrect.
238
     *
239
     * @param string $fieldName
240
     * @throws InvalidArgumentTypeException
241
     * @throws PropertyNotAccessibleException
242
     */
243
    protected function injectFieldInService($fieldName)
244
    {
245
        $formObject = $this->formService->getFormObject();
246
        $formConfiguration = $formObject->getConfiguration();
247
248
        if (false === is_string($fieldName)) {
249
            throw InvalidArgumentTypeException::fieldViewHelperInvalidTypeNameArgument();
250
        } elseif (false === $formConfiguration->hasField($fieldName)) {
251
            throw PropertyNotAccessibleException::fieldViewHelperFieldNotAccessibleInForm($formObject, $fieldName);
252
        }
253
254
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
255
    }
256
257
    /**
258
     * Returns the layout instance used by this field.
259
     *
260
     * @param View $viewConfiguration
261
     * @return Layout
262
     * @throws EntryNotFoundException
263
     * @throws InvalidArgumentTypeException
264
     * @throws InvalidArgumentValueException
265
     */
266
    protected function getLayout(View $viewConfiguration)
267
    {
268
        $layout = $this->arguments['layout'];
269
270
        if (false === is_string($layout)) {
271
            throw InvalidArgumentTypeException::invalidTypeNameArgumentFieldViewHelper($layout);
272
        }
273
274
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
275
276
        if (empty($templateName)) {
277
            $templateName = 'default';
278
        }
279
280
        if (empty($layoutName)) {
281
            throw InvalidArgumentValueException::fieldViewHelperEmptyLayout();
282
        }
283
284
        if (false === $viewConfiguration->hasLayout($layoutName)) {
285
            throw EntryNotFoundException::fieldViewHelperLayoutNotFound($layout);
286
        }
287
288
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
289
            throw EntryNotFoundException::fieldViewHelperLayoutItemNotFound($layout, $templateName);
290
        }
291
292
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
293
    }
294
295
    /**
296
     * Stores some arguments which may already have been initialized, and could
297
     * be overridden in the local scope.
298
     */
299
    protected function storeOriginalArguments()
300
    {
301
        $this->originalArguments = [];
302
        $variableProvider = $this->getVariableProvider();
303
304
        foreach (self::$reservedVariablesNames as $key) {
305
            if ($variableProvider->exists($key)) {
306
                $this->originalArguments[$key] = $variableProvider->get($key);
307
            }
308
        }
309
    }
310
311
    /**
312
     * Will restore original arguments in the template variable container.
313
     *
314
     * @param array $templateArguments
315
     */
316
    protected function restoreOriginalArguments(array $templateArguments)
317
    {
318
        $variableProvider = $this->getVariableProvider();
319
320
        foreach ($variableProvider->getAllIdentifiers() as $identifier) {
321
            if (array_key_exists($identifier, $templateArguments)) {
322
                $variableProvider->remove($identifier);
323
            }
324
        }
325
326
        foreach ($this->originalArguments as $key => $value) {
327
            if ($variableProvider->exists($key)) {
328
                $variableProvider->remove($key);
329
            }
330
331
            $variableProvider->add($key, $value);
332
        }
333
    }
334
335
    /**
336
     * @param FormViewHelperService $service
337
     */
338
    public function injectFormService(FormViewHelperService $service)
339
    {
340
        $this->formService = $service;
341
    }
342
343
    /**
344
     * @param FieldViewHelperService $service
345
     */
346
    public function injectFieldService(FieldViewHelperService $service)
347
    {
348
        $this->fieldService = $service;
349
    }
350
351
    /**
352
     * @param SlotViewHelperService $slotService
353
     */
354
    public function injectSlotService(SlotViewHelperService $slotService)
355
    {
356
        $this->slotService = $slotService;
357
    }
358
}
359