Completed
Push — master ( 3395f4...940fa6 )
by Romain
13s
created

FieldViewHelper::renderLayoutView()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 8.439
c 0
b 0
f 0
cc 5
eloc 24
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\Core\Core;
19
use Romm\Formz\Exceptions\EntryNotFoundException;
20
use Romm\Formz\Exceptions\InvalidArgumentTypeException;
21
use Romm\Formz\Exceptions\InvalidArgumentValueException;
22
use Romm\Formz\Service\StringService;
23
use Romm\Formz\ViewHelpers\Service\FieldService;
24
use Romm\Formz\ViewHelpers\Service\FormService;
25
use Romm\Formz\ViewHelpers\Service\SectionService;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
28
use TYPO3\CMS\Extbase\Utility\ArrayUtility;
29
use TYPO3\CMS\Fluid\View\StandaloneView;
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 FormService
58
     */
59
    protected $formService;
60
61
    /**
62
     * @var FieldService
63
     */
64
    protected $fieldService;
65
66
    /**
67
     * @var SectionService
68
     */
69
    protected $sectionService;
70
71
    /**
72
     * @var array
73
     */
74
    protected $originalArguments = [];
75
76
    /**
77
     * @inheritdoc
78
     */
79
    public function initializeArguments()
80
    {
81
        $this->registerArgument('name', 'string', 'Name of the field which should be rendered.', true);
82
        $this->registerArgument('layout', 'string', 'Path of the TypoScript layout which will be used.', true);
83
        $this->registerArgument('arguments', 'array', 'Arbitrary arguments which will be sent to the field template.', false, []);
84
    }
85
86
    /**
87
     * @inheritdoc
88
     */
89
    public function render()
90
    {
91
        $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
92
93
        /*
94
         * First, we check if this view helper is called from within the
95
         * `FormViewHelper`, because it would not make sense anywhere else.
96
         */
97
        $this->formService->checkIsInsideFormViewHelper();
98
99
        /*
100
         * Then, we inject the wanted field in the `FieldService` so we can know
101
         * later which field we're working with.
102
         */
103
        $this->injectFieldInService($this->arguments['name']);
104
105
        /*
106
         * Calling this here will process every view helper beneath this one,
107
         * allowing options and sections to be used correctly in the field
108
         * layout.
109
         */
110
        $this->renderChildren();
111
112
        /*
113
         * We need to store original arguments declared for the current view
114
         * context, because we may override them during the rendering of this
115
         * view helper.
116
         */
117
        $this->storeOriginalArguments();
118
119
        /*
120
         * We merge the arguments with the ones registered with the
121
         * `OptionViewHelper`.
122
         */
123
        $templateArguments = $this->arguments['arguments'] ?: [];
124
        $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...
125
126
        $currentView = $viewHelperVariableContainer->getView();
127
128
        $result = $this->renderLayoutView($templateArguments);
129
130
        /*
131
         * Resetting all services data.
132
         */
133
        $this->fieldService->removeCurrentField();
134
        $this->fieldService->resetFieldOptions();
135
        $this->sectionService->resetSectionClosures();
136
137
        $viewHelperVariableContainer->setView($currentView);
138
        $this->restoreOriginalArguments($templateArguments);
139
140
        return $result;
141
    }
142
143
    /**
144
     * Will render the associated Fluid view for this field (configured with the
145
     * `layout` argument).
146
     *
147
     * @param array $templateArguments
148
     * @return string
149
     */
150
    protected function renderLayoutView(array $templateArguments)
151
    {
152
        $fieldName = $this->arguments['name'];
153
        $formObject = $this->formService->getFormObject();
154
        $formConfiguration = $formObject->getConfiguration();
155
        $viewConfiguration = $formConfiguration->getFormzConfiguration()->getView();
156
        $layout = $this->getLayout($viewConfiguration);
157
158
        $templateArguments['layout'] = $layout->getLayout();
159
        $templateArguments['formName'] = $formObject->getName();
160
        $templateArguments['fieldName'] = $fieldName;
161
        $templateArguments['fieldId'] = ($templateArguments['fieldId']) ?: StringService::get()->sanitizeString('formz-' . $formObject->getName() . '-' . $fieldName);
162
163
        /** @var StandaloneView $view */
164
        $view = Core::instantiate(StandaloneView::class);
165
        $view->setTemplatePathAndFilename($layout->getTemplateFile());
166
        $view->setLayoutRootPaths($viewConfiguration->getLayoutRootPaths());
167
        $view->setPartialRootPaths($viewConfiguration->getPartialRootPaths());
168
169
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
170
            $view->setRenderingContext($this->renderingContext);
171
        } else {
172
            $variableProvider = $this->getVariableProvider();
173
            foreach ($templateArguments as $key => $value) {
174
                if ($variableProvider->exists($key)) {
175
                    $variableProvider->remove($key);
176
                }
177
178
                $variableProvider->add($key, $value);
179
            }
180
        }
181
182
        $view->assignMultiple($templateArguments);
183
184
        return $view->render();
185
    }
186
187
    /**
188
     * Will check that the given field exists in the current form definition and
189
     * inject it in the `FieldService` as `currentField`.
190
     *
191
     * Throws an error if the field is not found or incorrect.
192
     *
193
     * @param string $fieldName
194
     * @throws EntryNotFoundException
195
     * @throws InvalidArgumentTypeException
196
     */
197
    protected function injectFieldInService($fieldName)
198
    {
199
        $formObject = $this->formService->getFormObject();
200
        $formConfiguration = $formObject->getConfiguration();
201
202
        if (false === is_string($fieldName)) {
203
            throw new InvalidArgumentTypeException(
204
                'The argument "name" of the view helper "' . __CLASS__ . '" must be a string.',
205
                1465243479
206
            );
207
        } elseif (false === $formConfiguration->hasField($fieldName)) {
208
            throw new EntryNotFoundException(
209
                '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.',
210
                1465243619);
211
        }
212
213
        $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...
214
    }
215
216
    /**
217
     * Returns the layout instance used by this field.
218
     *
219
     * @param View $viewConfiguration
220
     * @return Layout
221
     * @throws EntryNotFoundException
222
     * @throws InvalidArgumentTypeException
223
     * @throws InvalidArgumentValueException
224
     */
225
    protected function getLayout(View $viewConfiguration)
226
    {
227
        $layout = $this->arguments['layout'];
228
229
        if (false === is_string($layout)) {
230
            throw new InvalidArgumentTypeException(
231
                'The argument "layout" must be a string (' . gettype($layout) . ' given).',
232
                1485786193
233
            );
234
        }
235
236
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
237
        if (false === is_string($templateName)) {
238
            $templateName = 'default';
239
        }
240
241
        if (empty($layoutName)) {
242
            throw new InvalidArgumentValueException(
243
                'The layout name cannot be empty, please fill with a value.',
244
                1485786285
245
            );
246
        }
247
248
        if (false === $viewConfiguration->hasLayout($layoutName)) {
249
            throw new EntryNotFoundException(
250
                'The layout "' . $layout . '" could not be found. Please check your TypoScript configuration.',
251
                1465243586
252
            );
253
        }
254
255
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
256
            throw new EntryNotFoundException(
257
                'The layout "' . $layout . '" does not have an item "' . $templateName . '".',
258
                1485867803
259
            );
260
        }
261
262
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
263
    }
264
265
    /**
266
     * Stores some arguments which may already have been initialized, and could
267
     * be overridden in the local scope.
268
     */
269
    protected function storeOriginalArguments()
270
    {
271
        $this->originalArguments = [];
272
        $variableProvider = $this->getVariableProvider();
273
274
        foreach (self::$reservedVariablesNames as $key) {
275
            if ($variableProvider->exists($key)) {
276
                $this->originalArguments[$key] = $variableProvider->get($key);
277
            }
278
        }
279
    }
280
281
    /**
282
     * Will restore original arguments in the template variable container.
283
     *
284
     * @param array $templateArguments
285
     */
286
    protected function restoreOriginalArguments(array $templateArguments)
287
    {
288
        $variableProvider = $this->getVariableProvider();
289
290
        foreach ($variableProvider->getAllIdentifiers() as $identifier) {
291
            if (array_key_exists($identifier, $templateArguments)) {
292
                $variableProvider->remove($identifier);
293
            }
294
        }
295
296
        foreach ($this->originalArguments as $key => $value) {
297
            if ($variableProvider->exists($key)) {
298
                $variableProvider->remove($key);
299
            }
300
301
            $variableProvider->add($key, $value);
302
        }
303
    }
304
305
    /**
306
     * @param FormService $service
307
     */
308
    public function injectFormService(FormService $service)
309
    {
310
        $this->formService = $service;
311
    }
312
313
    /**
314
     * @param FieldService $service
315
     */
316
    public function injectFieldService(FieldService $service)
317
    {
318
        $this->fieldService = $service;
319
    }
320
321
    /**
322
     * @param SectionService $sectionService
323
     */
324
    public function injectSectionService(SectionService $sectionService)
325
    {
326
        $this->sectionService = $sectionService;
327
    }
328
}
329