Completed
Push — middleware-wip ( c7460e...ebb7ba )
by Romain
03:45
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
        /*
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
        if (false === $this->formService->formContextExists()) {
96
            throw ContextNotFoundException::fieldViewHelperFormContextNotFound();
97
        }
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 slots to be used correctly in the field layout.
108
         */
109
        $this->renderChildren();
110
111
        /*
112
         * We need to store original arguments declared for the current view
113
         * context, because we may override them during the rendering of this
114
         * view helper.
115
         */
116
        $this->storeOriginalArguments();
117
118
        /*
119
         * We merge the arguments with the ones registered with the
120
         * `OptionViewHelper`.
121
         */
122
        $templateArguments = $this->arguments['arguments'] ?: [];
123
        ArrayUtility::mergeRecursiveWithOverrule($templateArguments, $this->fieldService->getFieldOptions());
124
125
        $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
126
        $currentView = $viewHelperVariableContainer->getView();
127
128
        $result = $this->renderLayoutView($templateArguments);
129
130
        /*
131
         * Resetting all services data.
132
         */
133
        $this->fieldService->resetState();
134
        $this->slotService->resetState();
135
136
        $viewHelperVariableContainer->setView($currentView);
137
        $this->restoreOriginalArguments($templateArguments);
138
139
        return $result;
140
    }
141
142
    /**
143
     * Will render the associated Fluid view for this field (configured with the
144
     * `layout` argument).
145
     *
146
     * @param array $templateArguments
147
     * @return string
148
     */
149
    protected function renderLayoutView(array $templateArguments)
150
    {
151
        $fieldName = $this->arguments['name'];
152
        $formObject = $this->formService->getFormObject();
153
        $formConfiguration = $formObject->getDefinition();
154
        $viewConfiguration = $formConfiguration->getRootConfiguration()->getView();
155
        $layout = $this->getLayout($viewConfiguration);
156
157
        $templateArguments['layout'] = $layout->getLayout();
158
        $templateArguments['formName'] = $formObject->getName();
159
        $templateArguments['fieldName'] = $fieldName;
160
        $templateArguments['fieldId'] = ($templateArguments['fieldId']) ?: StringService::get()->sanitizeString('formz-' . $formObject->getName() . '-' . $fieldName);
161
162
        $view = $this->fieldService->getView($layout);
163
164
        $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...
165
        $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...
166
167
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
168
            $view->setRenderingContext($this->renderingContext);
169
        } else {
170
            $view->setControllerContext($this->controllerContext);
171
172
            $variableProvider = $this->getVariableProvider();
173
174
            foreach ($templateArguments as $key => $value) {
175
                if ($variableProvider->exists($key)) {
176
                    $variableProvider->remove($key);
177
                }
178
179
                $variableProvider->add($key, $value);
180
            }
181
        }
182
183
        $view->setLayoutRootPaths($layoutPaths);
184
        $view->setPartialRootPaths($partialPaths);
185
        $view->assignMultiple($templateArguments);
186
187
        return $view->render();
188
    }
189
190
    /**
191
     * Will check that the given field exists in the current form definition and
192
     * inject it in the `FieldService` as `currentField`.
193
     *
194
     * Throws an error if the field is not found or incorrect.
195
     *
196
     * @param string $fieldName
197
     * @throws InvalidArgumentTypeException
198
     * @throws PropertyNotAccessibleException
199
     */
200
    protected function injectFieldInService($fieldName)
201
    {
202
        $formObject = $this->formService->getFormObject();
203
        $formConfiguration = $formObject->getDefinition();
204
205
        if (false === is_string($fieldName)) {
206
            throw InvalidArgumentTypeException::fieldViewHelperInvalidTypeNameArgument();
207
        } elseif (false === $formConfiguration->hasField($fieldName)) {
208
            throw PropertyNotAccessibleException::fieldViewHelperFieldNotAccessibleInForm($formObject, $fieldName);
209
        }
210
211
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
212
    }
213
214
    /**
215
     * Returns the layout instance used by this field.
216
     *
217
     * @param View $viewConfiguration
218
     * @return Layout
219
     * @throws EntryNotFoundException
220
     * @throws InvalidArgumentTypeException
221
     * @throws InvalidArgumentValueException
222
     */
223
    protected function getLayout(View $viewConfiguration)
224
    {
225
        $layout = $this->arguments['layout'];
226
227
        if (false === is_string($layout)) {
228
            throw InvalidArgumentTypeException::invalidTypeNameArgumentFieldViewHelper($layout);
229
        }
230
231
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
232
233
        if (empty($templateName)) {
234
            $templateName = 'default';
235
        }
236
237
        if (empty($layoutName)) {
238
            throw InvalidArgumentValueException::fieldViewHelperEmptyLayout();
239
        }
240
241
        if (false === $viewConfiguration->hasLayout($layoutName)) {
242
            throw EntryNotFoundException::fieldViewHelperLayoutNotFound($layout);
243
        }
244
245
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
246
            throw EntryNotFoundException::fieldViewHelperLayoutItemNotFound($layout, $templateName);
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)) {
263
                $this->originalArguments[$key] = $variableProvider->get($key);
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) {
278
            if (array_key_exists($identifier, $templateArguments)) {
279
                $variableProvider->remove($identifier);
280
            }
281
        }
282
283
        foreach ($this->originalArguments as $key => $value) {
284
            if ($variableProvider->exists($key)) {
285
                $variableProvider->remove($key);
286
            }
287
288
            $variableProvider->add($key, $value);
289
        }
290
    }
291
292
    /**
293
     * This function will determinate the layout/partial root paths that should
294
     * be given to the standalone view. This must be a merge between the paths
295
     * given in the TypoScript configuration and the paths of the current view.
296
     *
297
     * This way, the user can use the layouts/partials from both the form
298
     * rendering extension, as well as the ones used by the field layout.
299
     *
300
     * Please note that TYPO3 v8+ has this behaviour by default, meaning only
301
     * the TypoScript configuration paths are needed, however in TYPO3 v7.6- we
302
     * need to access the root paths, which is *not* granted by Fluid... We are
303
     * then forced to use reflection, please don't do this at home!
304
     *
305
     * @param string $type `partial` or `layout`
306
     * @return array
307
     *
308
     * @deprecated Must be removed when TYPO3 7.6 is not supported anymore!
309
     */
310
    protected function getPaths($type)
311
    {
312
        $viewConfiguration = $this->formService->getFormObject()->getDefinition()->getRootConfiguration()->getView();
313
314
        $paths = $type === 'partial'
315
            ? $viewConfiguration->getAbsolutePartialRootPaths()
316
            : $viewConfiguration->getAbsoluteLayoutRootPaths();
317
318
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '>=')) {
319
            return $paths;
320
        } else {
321
            $currentView = $this->renderingContext->getViewHelperVariableContainer()->getView();
322
            $propertyName = $type === 'partial'
323
                ? 'getPartialRootPaths'
324
                : 'getLayoutRootPaths';
325
326
            $reflectionClass = new \ReflectionClass($currentView);
327
            $method = $reflectionClass->getMethod($propertyName);
328
            $method->setAccessible(true);
329
330
            $value = $method->invoke($currentView);
331
332
            return array_unique(array_merge($paths, $value));
333
        }
334
    }
335
336
    /**
337
     * @param FormViewHelperService $service
338
     */
339
    public function injectFormService(FormViewHelperService $service)
340
    {
341
        $this->formService = $service;
342
    }
343
344
    /**
345
     * @param FieldViewHelperService $service
346
     */
347
    public function injectFieldService(FieldViewHelperService $service)
348
    {
349
        $this->fieldService = $service;
350
    }
351
352
    /**
353
     * @param SlotViewHelperService $slotService
354
     */
355
    public function injectSlotService(SlotViewHelperService $slotService)
356
    {
357
        $this->slotService = $slotService;
358
    }
359
}
360