Completed
Push — typo3-v8-compatibility ( ae4db2...381377 )
by Romain
02:16
created

FieldViewHelper::renderLayoutView()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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