Completed
Push — development ( 0be97e...f22182 )
by Romain
01:45
created

FieldViewHelper::getTemplateArguments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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\Field\FieldViewHelperService;
25
use Romm\Formz\Service\ViewHelper\Form\FormViewHelperService;
26
use Romm\Formz\Service\ViewHelper\Slot\SlotViewHelperService;
27
use TYPO3\CMS\Core\Utility\ArrayUtility;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
30
31
/**
32
 * This view helper is used to automatize the rendering of a field layout. It
33
 * will use the TypoScript properties at the path `config.tx_formz.view.layout`.
34
 *
35
 * You need to indicate the name of the field which will be rendered, and the
36
 * name of the layout which should be used (it must be present in the TypoScript
37
 * configuration).
38
 *
39
 * Example of layout: `bootstrap.3-cols`. You may indicate only the group, then
40
 * the name of the layout will be set to `default` (if you use the layout group
41
 * `bootstrap`, the layout `default` will be used, only if it does exist of
42
 * course).
43
 */
44
class FieldViewHelper extends AbstractViewHelper
45
{
46
    /**
47
     * @var bool
48
     */
49
    protected $escapeOutput = false;
50
51
    /**
52
     * @var array
53
     */
54
    public static $reservedVariablesNames = ['layout', 'formName', 'fieldName', 'fieldId'];
55
56
    /**
57
     * @var FormViewHelperService
58
     */
59
    protected $formService;
60
61
    /**
62
     * @var FieldViewHelperService
63
     */
64
    protected $fieldService;
65
66
    /**
67
     * @var SlotViewHelperService
68
     */
69
    protected $slotService;
70
71
    /**
72
     * @inheritdoc
73
     */
74
    public function initializeArguments()
75
    {
76
        $this->registerArgument('name', 'string', 'Name of the field which should be rendered.', true);
77
        $this->registerArgument('layout', 'string', 'Path of the TypoScript layout which will be used.', true);
78
        $this->registerArgument('arguments', 'array', 'Arbitrary arguments which will be sent to the field template.', false, []);
79
    }
80
81
    /**
82
     * @inheritdoc
83
     */
84
    public function render()
85
    {
86
        /*
87
         * First, we check if this view helper is called from within the
88
         * `FormViewHelper`, because it would not make sense anywhere else.
89
         */
90
        if (false === $this->formService->formContextExists()) {
91
            throw ContextNotFoundException::fieldViewHelperFormContextNotFound();
92
        }
93
94
        /*
95
         * Then, we inject the wanted field in the `FieldService` so we can know
96
         * later which field we're working with.
97
         */
98
        $this->injectFieldInService($this->arguments['name']);
99
100
        /*
101
         * Activating the slot service, which will be used all along the
102
         * rendering of this very field.
103
         */
104
        $this->slotService->activate($this->renderingContext);
0 ignored issues
show
Compatibility introduced by
$this->renderingContext of type object<TYPO3Fluid\Fluid\...deringContextInterface> is not a sub-type of object<TYPO3\CMS\Fluid\C...deringContextInterface>. It seems like you assume a child interface of the interface TYPO3Fluid\Fluid\Core\Re...nderingContextInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
105
106
        /*
107
         * Calling this here will process every view helper beneath this one,
108
         * allowing options and slots to be used correctly in the field layout.
109
         */
110
        $this->renderChildren();
111
112
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
113
            $restoreCallback = $this->storeViewDataLegacy();
0 ignored issues
show
Deprecated Code introduced by
The method Romm\Formz\ViewHelpers\F...::storeViewDataLegacy() has been deprecated with message: Will be deleted when TYPO3 7.6 is not supported anymore.

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...
114
        }
115
116
        $templateArguments = $this->getTemplateArguments();
117
118
        $result = $this->renderLayoutView($templateArguments);
119
120
        /*
121
         * Resetting all services data.
122
         */
123
        $this->fieldService->removeCurrentField();
124
        $this->slotService->resetState();
125
126
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
127
            /** @noinspection PhpUndefinedVariableInspection */
128
            $restoreCallback($templateArguments);
0 ignored issues
show
Bug introduced by
The variable $restoreCallback does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
129
        }
130
131
        return $result;
132
    }
133
134
    /**
135
     * Will return the associated Fluid view for this field (configured with the
136
     * `layout` argument).
137
     *
138
     * @param array $templateArguments
139
     * @return string
140
     */
141
    protected function renderLayoutView(array $templateArguments)
142
    {
143
        $fieldName = $this->arguments['name'];
144
        $formObject = $this->formService->getFormObject();
145
        $formConfiguration = $formObject->getConfiguration();
146
        $viewConfiguration = $formConfiguration->getRootConfiguration()->getView();
147
        $layout = $this->getLayout($viewConfiguration);
148
149
        $templateArguments['layout'] = $layout->getLayout();
150
        $templateArguments['formName'] = $formObject->getName();
151
        $templateArguments['fieldName'] = $fieldName;
152
        $templateArguments['fieldId'] = ($templateArguments['fieldId']) ?: StringService::get()->sanitizeString('formz-' . $formObject->getName() . '-' . $fieldName);
153
154
        $view = $this->fieldService->getView($layout);
155
156
        /*
157
         * Warning: we need to store the layouts/partials paths before
158
         * manipulating the rendering context!
159
         */
160
        $layoutPaths = $this->getPaths('layout');
0 ignored issues
show
Deprecated Code introduced by
The method Romm\Formz\ViewHelpers\FieldViewHelper::getPaths() has been deprecated with message: Must be removed when TYPO3 7.6 is not supported anymore!

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...
161
        $partialPaths = $this->getPaths('partial');
0 ignored issues
show
Deprecated Code introduced by
The method Romm\Formz\ViewHelpers\FieldViewHelper::getPaths() has been deprecated with message: Must be removed when TYPO3 7.6 is not supported anymore!

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...
162
163
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
164
            $view->setRenderingContext($this->renderingContext);
165
        } else {
166
            $renderingContext = $view->getRenderingContext();
167
168
            // Removing all variables previously added to the provider.
169
            $provider = $renderingContext->getVariableProvider();
170
171
            foreach ($provider->getAllIdentifiers() as $key) {
172
                $provider->remove($key);
173
            }
174
175
            /*
176
             * Updating the view dependencies: the variable container as well as
177
             * the controller context must be injected in the view.
178
             */
179
            $renderingContext->setViewHelperVariableContainer($this->viewHelperVariableContainer);
180
181
            $view->setControllerContext($this->controllerContext);
182
183
            $this->viewHelperVariableContainer->setView($view);
184
        }
185
186
        $view->setLayoutRootPaths($layoutPaths);
187
        $view->setPartialRootPaths($partialPaths);
188
        $view->assignMultiple($templateArguments);
189
190
        return $view->render();
191
    }
192
193
    /**
194
     * Temporary solution for TYPO3 6.2 to 7.6 that will store the current view
195
     * variables, to be able to restore them later.
196
     *
197
     * A callback function is returned; it will be called once the field layout
198
     * view was processed, and will restore all the view data.
199
     *
200
     * @return \Closure
201
     *
202
     * @deprecated Will be deleted when TYPO3 7.6 is not supported anymore.
203
     */
204
    protected function storeViewDataLegacy()
205
    {
206
        $originalArguments = [];
207
208
        $variableProvider = $this->getVariableProvider();
209
210
        foreach (self::$reservedVariablesNames as $key) {
211
            if ($variableProvider->exists($key)) {
212
                $originalArguments[$key] = $variableProvider->get($key);
213
            }
214
        }
215
216
        $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
217
        $currentView = $viewHelperVariableContainer->getView();
218
219
        return function (array $templateArguments) use ($originalArguments, $variableProvider, $viewHelperVariableContainer, $currentView) {
220
            $viewHelperVariableContainer->setView($currentView);
221
222
            /*
223
             * Cleaning up the variables in the provider: the original
224
             * values are restored to make the provider like it was before
225
             * the field rendering started.
226
             */
227
            foreach ($variableProvider->getAllIdentifiers() as $identifier) {
228
                if (array_key_exists($identifier, $templateArguments)) {
229
                    $variableProvider->remove($identifier);
230
                }
231
            }
232
233
            foreach ($originalArguments as $key => $value) {
234
                if ($variableProvider->exists($key)) {
235
                    $variableProvider->remove($key);
236
                }
237
238
                $variableProvider->add($key, $value);
239
            }
240
        };
241
    }
242
243
    /**
244
     * Will check that the given field exists in the current form definition and
245
     * inject it in the `FieldService` as `currentField`.
246
     *
247
     * Throws an error if the field is not found or incorrect.
248
     *
249
     * @param string $fieldName
250
     * @throws InvalidArgumentTypeException
251
     * @throws PropertyNotAccessibleException
252
     */
253
    protected function injectFieldInService($fieldName)
254
    {
255
        $formObject = $this->formService->getFormObject();
256
        $formConfiguration = $formObject->getConfiguration();
257
258
        if (false === is_string($fieldName)) {
259
            throw InvalidArgumentTypeException::fieldViewHelperInvalidTypeNameArgument();
260
        } elseif (false === $formConfiguration->hasField($fieldName)) {
261
            throw PropertyNotAccessibleException::fieldViewHelperFieldNotAccessibleInForm($formObject, $fieldName);
262
        }
263
264
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
265
    }
266
267
    /**
268
     * Returns the layout instance used by this field.
269
     *
270
     * @param View $viewConfiguration
271
     * @return Layout
272
     * @throws EntryNotFoundException
273
     * @throws InvalidArgumentTypeException
274
     * @throws InvalidArgumentValueException
275
     */
276
    protected function getLayout(View $viewConfiguration)
277
    {
278
        $layout = $this->arguments['layout'];
279
280
        if (false === is_string($layout)) {
281
            throw InvalidArgumentTypeException::invalidTypeNameArgumentFieldViewHelper($layout);
282
        }
283
284
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
285
286
        if (empty($templateName)) {
287
            $templateName = 'default';
288
        }
289
290
        if (empty($layoutName)) {
291
            throw InvalidArgumentValueException::fieldViewHelperEmptyLayout();
292
        }
293
294
        if (false === $viewConfiguration->hasLayout($layoutName)) {
295
            throw EntryNotFoundException::fieldViewHelperLayoutNotFound($layout);
296
        }
297
298
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
299
            throw EntryNotFoundException::fieldViewHelperLayoutItemNotFound($layout, $templateName);
300
        }
301
302
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
303
    }
304
305
    /**
306
     * Merging the arguments with the ones registered with the
307
     * `OptionViewHelper`.
308
     *
309
     * @return array
310
     */
311
    protected function getTemplateArguments()
312
    {
313
        $templateArguments = $this->arguments['arguments'] ?: [];
314
        ArrayUtility::mergeRecursiveWithOverrule($templateArguments, $this->fieldService->getFieldOptions());
315
316
        return $templateArguments;
317
    }
318
319
    /**
320
     * This function will determinate the layout/partial root paths that should
321
     * be given to the standalone view. This must be a merge between the paths
322
     * given in the TypoScript configuration and the paths of the current view.
323
     *
324
     * This way, the user can use the layouts/partials from both the form
325
     * rendering extension, as well as the ones used by the field layout.
326
     *
327
     * Please note that TYPO3 v8+ has this behaviour by default, meaning only
328
     * the TypoScript configuration paths are needed, however in TYPO3 v7.6- we
329
     * need to access the root paths, which is *not* granted by Fluid... We are
330
     * then forced to use reflection, please don't do this at home!
331
     *
332
     * @param string $type `partial` or `layout`
333
     * @return array
334
     *
335
     * @deprecated Must be removed when TYPO3 7.6 is not supported anymore!
336
     */
337
    protected function getPaths($type)
338
    {
339
        $viewConfiguration = $this->formService->getFormObject()->getConfiguration()->getRootConfiguration()->getView();
340
341
        $paths = $type === 'partial'
342
            ? $viewConfiguration->getAbsolutePartialRootPaths()
343
            : $viewConfiguration->getAbsoluteLayoutRootPaths();
344
345
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '>=')) {
346
            return $paths;
347
        } else {
348
            $currentView = $this->renderingContext->getViewHelperVariableContainer()->getView();
349
            $propertyName = $type === 'partial'
350
                ? 'getPartialRootPaths'
351
                : 'getLayoutRootPaths';
352
353
            $reflectionClass = new \ReflectionClass($currentView);
354
            $method = $reflectionClass->getMethod($propertyName);
355
            $method->setAccessible(true);
356
357
            $value = $method->invoke($currentView);
358
359
            return array_unique(array_merge($paths, $value));
360
        }
361
    }
362
363
    /**
364
     * @param FormViewHelperService $service
365
     */
366
    public function injectFormService(FormViewHelperService $service)
367
    {
368
        $this->formService = $service;
369
    }
370
371
    /**
372
     * @param FieldViewHelperService $service
373
     */
374
    public function injectFieldService(FieldViewHelperService $service)
375
    {
376
        $this->fieldService = $service;
377
    }
378
379
    /**
380
     * @param SlotViewHelperService $slotService
381
     */
382
    public function injectSlotService(SlotViewHelperService $slotService)
383
    {
384
        $this->slotService = $slotService;
385
    }
386
}
387