Completed
Push — feature/middleware ( 864c18...e1fdd7 )
by Romain
02:52
created

FieldViewHelper::getPaths()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 6
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\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
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
     * @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
        if (false === $this->formService->formContextExists()) {
98
            throw ContextNotFoundException::fieldViewHelperFormContextNotFound();
99
        }
100
101
        /*
102
         * Then, we inject the wanted field in the `FieldService` so we can know
103
         * later which field we're working with.
104
         */
105
        $this->injectFieldInService($this->arguments['name']);
106
107
        /*
108
         * Calling this here will process every view helper beneath this one,
109
         * allowing options and slots to be used correctly in the field layout.
110
         */
111
        $this->renderChildren();
112
113
        /*
114
         * We need to store original arguments declared for the current view
115
         * context, because we may override them during the rendering of this
116
         * view helper.
117
         */
118
        $this->storeOriginalArguments();
119
120
        /*
121
         * We merge the arguments with the ones registered with the
122
         * `OptionViewHelper`.
123
         */
124
        $templateArguments = $this->arguments['arguments'] ?: [];
125
        ArrayUtility::mergeRecursiveWithOverrule($templateArguments, $this->fieldService->getFieldOptions());
126
127
        $currentView = $viewHelperVariableContainer->getView();
128
129
        $result = $this->renderLayoutView($templateArguments);
130
131
        /*
132
         * Resetting all services data.
133
         */
134
        $this->fieldService->resetState();
135
        $this->slotService->resetState();
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->getDefinition();
155
        $viewConfiguration = $formConfiguration->getRootConfiguration()->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
        $view = $this->fieldService->getView($layout);
164
165
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
166
            $view->setRenderingContext($this->renderingContext);
167
        } else {
168
            $view->setControllerContext($this->controllerContext);
169
170
            $variableProvider = $this->getVariableProvider();
171
172
            foreach ($templateArguments as $key => $value) {
173
                if ($variableProvider->exists($key)) {
174
                    $variableProvider->remove($key);
175
                }
176
177
                $variableProvider->add($key, $value);
178
            }
179
        }
180
181
        $view->setLayoutRootPaths($viewConfiguration->getAbsoluteLayoutRootPaths());
182
        $view->setPartialRootPaths($viewConfiguration->getAbsolutePartialRootPaths());
183
        $view->assignMultiple($templateArguments);
184
185
        return $view->render();
186
    }
187
188
    /**
189
     * Will check that the given field exists in the current form definition and
190
     * inject it in the `FieldService` as `currentField`.
191
     *
192
     * Throws an error if the field is not found or incorrect.
193
     *
194
     * @param string $fieldName
195
     * @throws InvalidArgumentTypeException
196
     * @throws PropertyNotAccessibleException
197
     */
198
    protected function injectFieldInService($fieldName)
199
    {
200
        $formObject = $this->formService->getFormObject();
201
        $formConfiguration = $formObject->getDefinition();
202
203
        if (false === is_string($fieldName)) {
204
            throw InvalidArgumentTypeException::fieldViewHelperInvalidTypeNameArgument();
205
        } elseif (false === $formConfiguration->hasField($fieldName)) {
206
            throw PropertyNotAccessibleException::fieldViewHelperFieldNotAccessibleInForm($formObject, $fieldName);
207
        }
208
209
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
210
    }
211
212
    /**
213
     * Returns the layout instance used by this field.
214
     *
215
     * @param View $viewConfiguration
216
     * @return Layout
217
     * @throws EntryNotFoundException
218
     * @throws InvalidArgumentTypeException
219
     * @throws InvalidArgumentValueException
220
     */
221
    protected function getLayout(View $viewConfiguration)
222
    {
223
        $layout = $this->arguments['layout'];
224
225
        if (false === is_string($layout)) {
226
            throw InvalidArgumentTypeException::invalidTypeNameArgumentFieldViewHelper($layout);
227
        }
228
229
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
230
231
        if (empty($templateName)) {
232
            $templateName = 'default';
233
        }
234
235
        if (empty($layoutName)) {
236
            throw InvalidArgumentValueException::fieldViewHelperEmptyLayout();
237
        }
238
239
        if (false === $viewConfiguration->hasLayout($layoutName)) {
240
            throw EntryNotFoundException::fieldViewHelperLayoutNotFound($layout);
241
        }
242
243
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
244
            throw EntryNotFoundException::fieldViewHelperLayoutItemNotFound($layout, $templateName);
245
        }
246
247
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
248
    }
249
250
    /**
251
     * Stores some arguments which may already have been initialized, and could
252
     * be overridden in the local scope.
253
     */
254
    protected function storeOriginalArguments()
255
    {
256
        $this->originalArguments = [];
257
        $variableProvider = $this->getVariableProvider();
258
259
        foreach (self::$reservedVariablesNames as $key) {
260
            if ($variableProvider->exists($key)) {
261
                $this->originalArguments[$key] = $variableProvider->get($key);
262
            }
263
        }
264
    }
265
266
    /**
267
     * Will restore original arguments in the template variable container.
268
     *
269
     * @param array $templateArguments
270
     */
271
    protected function restoreOriginalArguments(array $templateArguments)
272
    {
273
        $variableProvider = $this->getVariableProvider();
274
275
        foreach ($variableProvider->getAllIdentifiers() as $identifier) {
276
            if (array_key_exists($identifier, $templateArguments)) {
277
                $variableProvider->remove($identifier);
278
            }
279
        }
280
281
        foreach ($this->originalArguments as $key => $value) {
282
            if ($variableProvider->exists($key)) {
283
                $variableProvider->remove($key);
284
            }
285
286
            $variableProvider->add($key, $value);
287
        }
288
    }
289
290
    /**
291
     * @param FormViewHelperService $service
292
     */
293
    public function injectFormService(FormViewHelperService $service)
294
    {
295
        $this->formService = $service;
296
    }
297
298
    /**
299
     * @param FieldViewHelperService $service
300
     */
301
    public function injectFieldService(FieldViewHelperService $service)
302
    {
303
        $this->fieldService = $service;
304
    }
305
306
    /**
307
     * @param SlotViewHelperService $slotService
308
     */
309
    public function injectSlotService(SlotViewHelperService $slotService)
310
    {
311
        $this->slotService = $slotService;
312
    }
313
}
314