Completed
Push — feature/middleware ( e1fdd7...832bfd )
by Romain
02:33
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
        $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...
166
        $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...
167
168
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '<')) {
169
            $view->setRenderingContext($this->renderingContext);
170
        } else {
171
            $view->setControllerContext($this->controllerContext);
172
173
            $variableProvider = $this->getVariableProvider();
174
175
            foreach ($templateArguments as $key => $value) {
176
                if ($variableProvider->exists($key)) {
177
                    $variableProvider->remove($key);
178
                }
179
180
                $variableProvider->add($key, $value);
181
            }
182
        }
183
184
        $view->setLayoutRootPaths($layoutPaths);
185
        $view->setPartialRootPaths($partialPaths);
186
        $view->assignMultiple($templateArguments);
187
188
        return $view->render();
189
    }
190
191
    /**
192
     * Will check that the given field exists in the current form definition and
193
     * inject it in the `FieldService` as `currentField`.
194
     *
195
     * Throws an error if the field is not found or incorrect.
196
     *
197
     * @param string $fieldName
198
     * @throws InvalidArgumentTypeException
199
     * @throws PropertyNotAccessibleException
200
     */
201
    protected function injectFieldInService($fieldName)
202
    {
203
        $formObject = $this->formService->getFormObject();
204
        $formConfiguration = $formObject->getDefinition();
205
206
        if (false === is_string($fieldName)) {
207
            throw InvalidArgumentTypeException::fieldViewHelperInvalidTypeNameArgument();
208
        } elseif (false === $formConfiguration->hasField($fieldName)) {
209
            throw PropertyNotAccessibleException::fieldViewHelperFieldNotAccessibleInForm($formObject, $fieldName);
210
        }
211
212
        $this->fieldService->setCurrentField($formConfiguration->getField($fieldName));
213
    }
214
215
    /**
216
     * Returns the layout instance used by this field.
217
     *
218
     * @param View $viewConfiguration
219
     * @return Layout
220
     * @throws EntryNotFoundException
221
     * @throws InvalidArgumentTypeException
222
     * @throws InvalidArgumentValueException
223
     */
224
    protected function getLayout(View $viewConfiguration)
225
    {
226
        $layout = $this->arguments['layout'];
227
228
        if (false === is_string($layout)) {
229
            throw InvalidArgumentTypeException::invalidTypeNameArgumentFieldViewHelper($layout);
230
        }
231
232
        list($layoutName, $templateName) = GeneralUtility::trimExplode('.', $layout);
233
234
        if (empty($templateName)) {
235
            $templateName = 'default';
236
        }
237
238
        if (empty($layoutName)) {
239
            throw InvalidArgumentValueException::fieldViewHelperEmptyLayout();
240
        }
241
242
        if (false === $viewConfiguration->hasLayout($layoutName)) {
243
            throw EntryNotFoundException::fieldViewHelperLayoutNotFound($layout);
244
        }
245
246
        if (false === $viewConfiguration->getLayout($layoutName)->hasItem($templateName)) {
247
            throw EntryNotFoundException::fieldViewHelperLayoutItemNotFound($layout, $templateName);
248
        }
249
250
        return $viewConfiguration->getLayout($layoutName)->getItem($templateName);
251
    }
252
253
    /**
254
     * Stores some arguments which may already have been initialized, and could
255
     * be overridden in the local scope.
256
     */
257
    protected function storeOriginalArguments()
258
    {
259
        $this->originalArguments = [];
260
        $variableProvider = $this->getVariableProvider();
261
262
        foreach (self::$reservedVariablesNames as $key) {
263
            if ($variableProvider->exists($key)) {
264
                $this->originalArguments[$key] = $variableProvider->get($key);
265
            }
266
        }
267
    }
268
269
    /**
270
     * Will restore original arguments in the template variable container.
271
     *
272
     * @param array $templateArguments
273
     */
274
    protected function restoreOriginalArguments(array $templateArguments)
275
    {
276
        $variableProvider = $this->getVariableProvider();
277
278
        foreach ($variableProvider->getAllIdentifiers() as $identifier) {
279
            if (array_key_exists($identifier, $templateArguments)) {
280
                $variableProvider->remove($identifier);
281
            }
282
        }
283
284
        foreach ($this->originalArguments as $key => $value) {
285
            if ($variableProvider->exists($key)) {
286
                $variableProvider->remove($key);
287
            }
288
289
            $variableProvider->add($key, $value);
290
        }
291
    }
292
293
    /**
294
     * This function will determinate the layout/partial root paths that should
295
     * be given to the standalone view. This must be a merge between the paths
296
     * given in the TypoScript configuration and the paths of the current view.
297
     *
298
     * This way, the user can use the layouts/partials from both the form
299
     * rendering extension, as well as the ones used by the field layout.
300
     *
301
     * Please note that TYPO3 v8+ has this behaviour by default, meaning only
302
     * the TypoScript configuration paths are needed, however in TYPO3 v7.6- we
303
     * need to access the root paths, which is *not* granted by Fluid... We are
304
     * then forced to use reflection, please don't do this at home!
305
     *
306
     * @param string $type `partial` or `layout`
307
     * @return array
308
     *
309
     * @deprecated Must be removed when TYPO3 7.6 is not supported anymore!
310
     */
311
    protected function getPaths($type)
312
    {
313
        $viewConfiguration = $this->formService->getFormObject()->getConfiguration()->getRootConfiguration()->getView();
0 ignored issues
show
Bug introduced by
The method getConfiguration() does not seem to exist on object<Romm\Formz\Form\FormObject\FormObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
314
315
        $paths = $type === 'partial'
316
            ? $viewConfiguration->getAbsolutePartialRootPaths()
317
            : $viewConfiguration->getAbsoluteLayoutRootPaths();
318
319
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '8.0.0', '>=')) {
320
            return $paths;
321
        } else {
322
            $currentView = $this->renderingContext->getViewHelperVariableContainer()->getView();
323
            $propertyName = $type === 'partial'
324
                ? 'getPartialRootPaths'
325
                : 'getLayoutRootPaths';
326
327
            $reflectionClass = new \ReflectionClass($currentView);
328
            $method = $reflectionClass->getMethod($propertyName);
329
            $method->setAccessible(true);
330
331
            $value = $method->invoke($currentView);
332
333
            return array_unique(array_merge($paths, $value));
334
        }
335
    }
336
337
    /**
338
     * @param FormViewHelperService $service
339
     */
340
    public function injectFormService(FormViewHelperService $service)
341
    {
342
        $this->formService = $service;
343
    }
344
345
    /**
346
     * @param FieldViewHelperService $service
347
     */
348
    public function injectFieldService(FieldViewHelperService $service)
349
    {
350
        $this->fieldService = $service;
351
    }
352
353
    /**
354
     * @param SlotViewHelperService $slotService
355
     */
356
    public function injectSlotService(SlotViewHelperService $slotService)
357
    {
358
        $this->slotService = $slotService;
359
    }
360
}
361