Completed
Push — feature/version-2 ( 045265...d99cde )
by Romain
17s
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\Field\FieldViewHelperService;
25
use Romm\Formz\Service\ViewHelper\Form\FormViewHelperService;
26
use Romm\Formz\Service\ViewHelper\Slot\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
         * Activating the slot service, which will be used all along the
109
         * rendering of this very field.
110
         */
111
        $this->slotService->activate($this->renderingContext);
0 ignored issues
show
Compatibility introduced by
$this->renderingContext of type object<TYPO3Fluid\Fluid\...deringContextInterface> is not a sub-type of object<TYPO3\CMS\Fluid\C...deringContextInterface>. It seems like you assume a child interface of the interface TYPO3Fluid\Fluid\Core\Re...nderingContextInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

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