Completed
Push — typo3-v8-compatibility ( d214a9...2772d2 )
by Romain
02:28
created

FieldViewHelper::renderLayoutView()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 18

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 18
nc 4
nop 1
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\EntryNotFoundException;
19
use Romm\Formz\Exceptions\InvalidArgumentTypeException;
20
use Romm\Formz\Exceptions\InvalidArgumentValueException;
21
use Romm\Formz\Service\StringService;
22
use Romm\Formz\ViewHelpers\Service\FieldService;
23
use Romm\Formz\ViewHelpers\Service\FormService;
24
use Romm\Formz\ViewHelpers\Service\SectionService;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
27
use TYPO3\CMS\Extbase\Utility\ArrayUtility;
28
29
/**
30
 * This view helper is used to automatize the rendering of a field layout. It
31
 * will use the TypoScript properties at the path `config.tx_formz.view.layout`.
32
 *
33
 * You need to indicate the name of the field which will be rendered, and the
34
 * name of the layout which should be used (it must be present in the TypoScript
35
 * configuration).
36
 *
37
 * Example of layout: `bootstrap.3-cols`. You may indicate only the group, then
38
 * the name of the layout will be set to `default` (if you use the layout group
39
 * `bootstrap`, the layout `default` will be used, only if it does exist of
40
 * course).
41
 */
42
class FieldViewHelper extends AbstractViewHelper
43
{
44
    /**
45
     * @var bool
46
     */
47
    protected $escapeOutput = false;
48
49
    /**
50
     * @var array
51
     */
52
    public static $reservedVariablesNames = ['layout', 'formName', 'fieldName', 'fieldId'];
53
54
    /**
55
     * @var FormService
56
     */
57
    protected $formService;
58
59
    /**
60
     * @var FieldService
61
     */
62
    protected $fieldService;
63
64
    /**
65
     * @var SectionService
66
     */
67
    protected $sectionService;
68
69
    /**
70
     * @var array
71
     */
72
    protected $originalArguments = [];
73
74
    /**
75
     * @inheritdoc
76
     */
77
    public function initializeArguments()
78
    {
79
        $this->registerArgument('name', 'string', 'Name of the field which should be rendered.', true);
80
        $this->registerArgument('layout', 'string', 'Path of the TypoScript layout which will be used.', true);
81
        $this->registerArgument('arguments', 'array', 'Arbitrary arguments which will be sent to the field template.', false, []);
82
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87
    public function render()
88
    {
89
        $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
90
91
        /*
92
         * First, we check if this view helper is called from within the
93
         * `FormViewHelper`, because it would not make sense anywhere else.
94
         */
95
        $this->formService->checkIsInsideFormViewHelper();
96
97
        /*
98
         * Then, we inject the wanted field in the `FieldService` so we can know
99
         * later which field we're working with.
100
         */
101
        $this->injectFieldInService($this->arguments['name']);
102
103
        /*
104
         * Calling this here will process every view helper beneath this one,
105
         * allowing options and sections to be used correctly in the field
106
         * layout.
107
         */
108
        $this->renderChildren();
109
110
        /*
111
         * We need to store original arguments declared for the current view
112
         * context, because we may override them during the rendering of this
113
         * view helper.
114
         */
115
        $this->storeOriginalArguments();
116
117
        /*
118
         * We merge the arguments with the ones registered with the
119
         * `OptionViewHelper`.
120
         */
121
        $templateArguments = $this->arguments['arguments'] ?: [];
122
        $templateArguments = ArrayUtility::arrayMergeRecursiveOverrule($templateArguments, $this->fieldService->getFieldOptions());
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3\CMS\Extbase\Utilit...ergeRecursiveOverrule() has been deprecated with message: since TYPO3 v8, will be removed in TYPO3 v9, use array_replace_recursive() instead if possible, other see the ArrayUtility in EXT:core

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...
123
124
        $currentView = $viewHelperVariableContainer->getView();
125
126
        $result = $this->renderLayoutView($templateArguments);
127
128
        /*
129
         * Resetting all services data.
130
         */
131
        $this->fieldService->removeCurrentField();
132
        $this->fieldService->resetFieldOptions();
133
        $this->sectionService->resetSectionClosures();
134
135
        $viewHelperVariableContainer->setView($currentView);
136
        $this->restoreOriginalArguments($templateArguments);
137
138
        return $result;
139
    }
140
141
    /**
142
     * Will render the associated Fluid view for this field (configured with the
143
     * `layout` argument).
144
     *
145
     * @param array $templateArguments
146
     * @return string
147
     */
148
    protected function renderLayoutView(array $templateArguments)
149
    {
150
        $fieldName = $this->arguments['name'];
151
        $formObject = $this->formService->getFormObject();
152
        $formConfiguration = $formObject->getConfiguration();
153
        $viewConfiguration = $formConfiguration->getFormzConfiguration()->getView();
154
        $layout = $this->getLayout($viewConfiguration);
155
156
        $templateArguments['layout'] = $layout->getLayout();
157
        $templateArguments['formName'] = $formObject->getName();
158
        $templateArguments['fieldName'] = $fieldName;
159
        $templateArguments['fieldId'] = ($templateArguments['fieldId']) ?: StringService::get()->sanitizeString('formz-' . $formObject->getName() . '-' . $fieldName);
160
161
        $view = $this->fieldService->getView();
162
        $view->setTemplatePathAndFilename($layout->getTemplateFile());
163
        $view->setLayoutRootPaths($viewConfiguration->getLayoutRootPaths());
164
        $view->setPartialRootPaths($viewConfiguration->getPartialRootPaths());
165
        $view->assignMultiple($templateArguments);
166
167
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
168
            $view->setRenderingContext($this->renderingContext);
169
        }
170
171
        return $view->render();
172
    }
173
174
    /**
175
     * Will check that the given field exists in the current form definition and
176
     * inject it in the `FieldService` as `currentField`.
177
     *
178
     * Throws an error if the field is not found or incorrect.
179
     *
180
     * @param string $fieldName
181
     * @throws EntryNotFoundException
182
     * @throws InvalidArgumentTypeException
183
     */
184
    protected function injectFieldInService($fieldName)
185
    {
186
        $formObject = $this->formService->getFormObject();
187
        $formConfiguration = $formObject->getConfiguration();
188
189
        if (false === is_string($fieldName)) {
190
            throw new InvalidArgumentTypeException(
191
                'The argument "name" of the view helper "' . __CLASS__ . '" must be a string.',
192
                1465243479
193
            );
194
        } elseif (false === $formConfiguration->hasField($fieldName)) {
195
            throw new EntryNotFoundException(
196
                'The form "' . $formObject->getClassName() . '" does not have an accessible property "' . $fieldName . '". Please be sure this property exists, and it has a proper getter to access its value.',
197
                1465243619);
198
        }
199
200
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
0 ignored issues
show
Bug introduced by
It seems like $formConfiguration->getField($fieldName) can be null; however, setCurrentField() 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...
201
    }
202
203
    /**
204
     * Returns the layout instance used by this field.
205
     *
206
     * @param View $viewConfiguration
207
     * @return Layout
208
     * @throws EntryNotFoundException
209
     * @throws InvalidArgumentTypeException
210
     * @throws InvalidArgumentValueException
211
     */
212
    protected function getLayout(View $viewConfiguration)
213
    {
214
        $layout = $this->arguments['layout'];
215
216
        if (false === is_string($layout)) {
217
            throw new InvalidArgumentTypeException(
218
                'The argument "layout" must be a string (' . gettype($layout) . ' given).',
219
                1485786193
220
            );
221
        }
222
223
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
224
        if (false === is_string($templateName)) {
225
            $templateName = 'default';
226
        }
227
228
        if (empty($layoutName)) {
229
            throw new InvalidArgumentValueException(
230
                'The layout name cannot be empty, please fill with a value.',
231
                1485786285
232
            );
233
        }
234
235
        if (false === $viewConfiguration->hasLayout($layoutName)) {
236
            throw new EntryNotFoundException(
237
                'The layout "' . $layout . '" could not be found. Please check your TypoScript configuration.',
238
                1465243586
239
            );
240
        }
241
242
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
243
            throw new EntryNotFoundException(
244
                'The layout "' . $layout . '" does not have an item "' . $templateName . '".',
245
                1485867803
246
            );
247
        }
248
249
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
250
    }
251
252
    /**
253
     * Stores some arguments which may already have been initialized, and could
254
     * be overridden in the local scope.
255
     */
256
    protected function storeOriginalArguments()
257
    {
258
        $this->originalArguments = [];
259
        $variableProvider = $this->getVariableProvider();
260
261
        foreach (self::$reservedVariablesNames as $key) {
262
            if ($variableProvider->exists($key)) {
0 ignored issues
show
Bug introduced by
The method exists does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
263
                $this->originalArguments[$key] = $variableProvider->get($key);
0 ignored issues
show
Bug introduced by
The method get does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
264
            }
265
        }
266
    }
267
268
    /**
269
     * Will restore original arguments in the template variable container.
270
     *
271
     * @param array $templateArguments
272
     */
273
    protected function restoreOriginalArguments(array $templateArguments)
274
    {
275
        $variableProvider = $this->getVariableProvider();
276
277
        foreach ($variableProvider->getAllIdentifiers() as $identifier) {
0 ignored issues
show
Bug introduced by
The method getAllIdentifiers does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
278
            if (array_key_exists($identifier, $templateArguments)) {
279
                $variableProvider->remove($identifier);
0 ignored issues
show
Bug introduced by
The method remove does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
280
            }
281
        }
282
283
        foreach ($this->originalArguments as $key => $value) {
284
            if ($variableProvider->exists($key)) {
0 ignored issues
show
Bug introduced by
The method exists does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
285
                $variableProvider->remove($key);
286
            }
287
288
            $variableProvider->add($key, $value);
0 ignored issues
show
Bug introduced by
The method add does only exist in TYPO3Fluid\Fluid\Core\Va...riableProviderInterface, but not in TYPO3\CMS\Fluid\Core\Rendering\RenderingContext.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
289
        }
290
    }
291
292
    /**
293
     * @param FormService $service
294
     */
295
    public function injectFormService(FormService $service)
296
    {
297
        $this->formService = $service;
298
    }
299
300
    /**
301
     * @param FieldService $service
302
     */
303
    public function injectFieldService(FieldService $service)
304
    {
305
        $this->fieldService = $service;
306
    }
307
308
    /**
309
     * @param SectionService $sectionService
310
     */
311
    public function injectSectionService(SectionService $sectionService)
312
    {
313
        $this->sectionService = $sectionService;
314
    }
315
}
316