Passed
Push — master ( 5527b0...962042 )
by
unknown
13:01
created

AbstractFinisher::substituteRuntimeReferences()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 56
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 5
nop 2
dl 0
loc 56
rs 8.5866
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
/*
19
 * Inspired by and partially taken from the Neos.Form package (www.neos.io)
20
 */
21
22
namespace TYPO3\CMS\Form\Domain\Finishers;
23
24
use TYPO3\CMS\Core\Utility\ArrayUtility;
25
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
26
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
27
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
28
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
29
use TYPO3\CMS\Form\Domain\Model\FormElements\StringableFormElementInterface;
30
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
31
use TYPO3\CMS\Form\Service\TranslationService;
32
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
33
34
/**
35
 * Finisher base class.
36
 *
37
 * Scope: frontend
38
 * **This class is meant to be sub classed by developers**
39
 */
40
abstract class AbstractFinisher implements FinisherInterface
41
{
42
43
    /**
44
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
45
     * @deprecated since v11, will be removed in v12. Drop together with inject method and ObjectManager removal.
46
     */
47
    protected $objectManager;
48
49
    /**
50
     * @var string
51
     */
52
    protected $finisherIdentifier = '';
53
54
    /**
55
     * @var string
56
     */
57
    protected $shortFinisherIdentifier = '';
58
59
    /**
60
     * The options which have been set from the outside. Instead of directly
61
     * accessing them, you should rather use parseOption().
62
     *
63
     * @var array
64
     */
65
    protected $options = [];
66
67
    /**
68
     * These are the default options of the finisher.
69
     * Override them in your concrete implementation.
70
     * Default options should not be changed from "outside"
71
     *
72
     * @var array
73
     */
74
    protected $defaultOptions = [];
75
76
    /**
77
     * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherContext
78
     */
79
    protected $finisherContext;
80
81
    /**
82
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
83
     * @internal
84
     * @deprecated since v11, will be removed in v12. Drop together with property and ObjectManager removal.
85
     */
86
    public function injectObjectManager(ObjectManagerInterface $objectManager)
87
    {
88
        $this->objectManager = $objectManager;
89
    }
90
91
    /**
92
     * @deprecated since v11, will be removed in v12. Drop together with ObjectManager cleanup.
93
     */
94
    public function __construct()
95
    {
96
        // no-op so parent::construct() calls don't fail in v11.
97
    }
98
99
    /**
100
     * @param string $finisherIdentifier The identifier for this finisher
101
     */
102
    public function setFinisherIdentifier(string $finisherIdentifier): void
103
    {
104
        if (empty($this->finisherIdentifier)) {
105
            // @deprecated since v11, will be removed in v12. Do not override finisher in case
106
            // some class implemented setting something in initializeObject() or constructor.
107
            // In v12, drop the if condition, but keep the body and enable setFinisherIdentifier()
108
            // in FinisherInterface.
109
            $this->finisherIdentifier = $finisherIdentifier;
110
            $this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $this->finisherIdentifier) ?? '';
111
        }
112
    }
113
114
    /**
115
     * @return string
116
     */
117
    public function getFinisherIdentifier(): string
118
    {
119
        return $this->finisherIdentifier;
120
    }
121
122
    /**
123
     * @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
124
     */
125
    public function setOptions(array $options)
126
    {
127
        $this->options = $options;
128
    }
129
130
    /**
131
     * Sets a single finisher option (@see setOptions())
132
     *
133
     * @param string $optionName name of the option to be set
134
     * @param mixed $optionValue value of the option
135
     */
136
    public function setOption(string $optionName, $optionValue)
137
    {
138
        $this->options[$optionName] = $optionValue;
139
    }
140
141
    /**
142
     * Executes the finisher
143
     *
144
     * @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
145
     * @return string|null
146
     */
147
    final public function execute(FinisherContext $finisherContext)
148
    {
149
        $this->finisherContext = $finisherContext;
150
151
        if (!$this->isEnabled()) {
152
            return null;
153
        }
154
155
        return $this->executeInternal();
156
    }
157
158
    /**
159
     * This method is called in the concrete finisher whenever self::execute() is called.
160
     *
161
     * Override and fill with your own implementation!
162
     *
163
     * @return string|null
164
     */
165
    abstract protected function executeInternal();
166
167
    /**
168
     * Read the option called $optionName from $this->options, and parse {...}
169
     * as object accessors.
170
     *
171
     * Then translate the value.
172
     *
173
     * If $optionName was not found, the corresponding default option is returned (from $this->defaultOptions)
174
     *
175
     * @param string $optionName
176
     * @return string|array|null
177
     */
178
    protected function parseOption(string $optionName)
179
    {
180
        if ($optionName === 'translation') {
181
            return null;
182
        }
183
184
        try {
185
            $optionValue = ArrayUtility::getValueByPath($this->options, $optionName, '.');
186
        } catch (MissingArrayPathException $exception) {
187
            $optionValue = null;
188
        }
189
        try {
190
            $defaultValue = ArrayUtility::getValueByPath($this->defaultOptions, $optionName, '.');
191
        } catch (MissingArrayPathException $exception) {
192
            $defaultValue = null;
193
        }
194
195
        if ($optionValue === null && $defaultValue !== null) {
196
            $optionValue = $defaultValue;
197
        }
198
199
        if ($optionValue === null) {
200
            return null;
201
        }
202
203
        if (!is_string($optionValue) && !is_array($optionValue)) {
204
            return $optionValue;
205
        }
206
207
        $formRuntime = $this->finisherContext->getFormRuntime();
208
        $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
209
210
        if (is_string($optionValue)) {
211
            $translationOptions = isset($this->options['translation']) && \is_array($this->options['translation'])
212
                                ? $this->options['translation']
213
                                : [];
214
215
            $optionValue = $this->translateFinisherOption(
216
                $optionValue,
217
                $formRuntime,
218
                $optionName,
219
                $optionValue,
220
                $translationOptions
221
            );
222
223
            $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
224
        }
225
226
        if (empty($optionValue)) {
227
            if ($defaultValue !== null) {
228
                $optionValue = $defaultValue;
229
            }
230
        }
231
        return $optionValue;
232
    }
233
234
    /**
235
     * Wraps TranslationService::translateFinisherOption to recursively
236
     * invoke all array items of resolved form state values or nested
237
     * finisher option configuration settings.
238
     *
239
     * @param string|array $subject
240
     * @param FormRuntime $formRuntime
241
     * @param string $optionName
242
     * @param string|array $optionValue
243
     * @param array $translationOptions
244
     * @return array|string
245
     */
246
    protected function translateFinisherOption(
247
        $subject,
248
        FormRuntime $formRuntime,
249
        string $optionName,
250
        $optionValue,
251
        array $translationOptions
252
    ) {
253
        if (is_array($subject)) {
254
            foreach ($subject as $key => $value) {
255
                $subject[$key] = $this->translateFinisherOption(
256
                    $value,
257
                    $formRuntime,
258
                    $optionName . '.' . $value,
259
                    $value,
260
                    $translationOptions
261
                );
262
            }
263
            return $subject;
264
        }
265
266
        return TranslationService::getInstance()->translateFinisherOption(
267
            $formRuntime,
268
            $this->finisherIdentifier,
269
            $optionName,
270
            $optionValue,
0 ignored issues
show
Bug introduced by
It seems like $optionValue can also be of type array; however, parameter $optionValue of TYPO3\CMS\Form\Service\T...anslateFinisherOption() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
            /** @scrutinizer ignore-type */ $optionValue,
Loading history...
271
            $translationOptions
272
        );
273
    }
274
275
    /**
276
     * You can encapsulate an option value with {}.
277
     * This enables you to access every gettable property from the
278
     * TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
279
     *
280
     * For example: {formState.formValues.<elementIdentifier>}
281
     * or {<elementIdentifier>}
282
     *
283
     * Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elementIdentifier>]"
284
     * There is a special option value '{__currentTimestamp}'.
285
     * This will be replaced with the current timestamp.
286
     *
287
     * @param string|array $needle
288
     * @param FormRuntime $formRuntime
289
     * @return mixed
290
     */
291
    protected function substituteRuntimeReferences($needle, FormRuntime $formRuntime)
292
    {
293
        // neither array nor string, directly return
294
        if (!is_array($needle) && !is_string($needle)) {
0 ignored issues
show
introduced by
The condition is_string($needle) is always true.
Loading history...
295
            return $needle;
296
        }
297
298
        // resolve (recursively) all array items
299
        if (is_array($needle)) {
300
            $substitutedNeedle = [];
301
            foreach ($needle as $key => $item) {
302
                $key = $this->substituteRuntimeReferences($key, $formRuntime);
303
                $item = $this->substituteRuntimeReferences($item, $formRuntime);
304
                $substitutedNeedle[$key] = $item;
305
            }
306
            return $substitutedNeedle;
307
        }
308
309
        // substitute one(!) variable in string which either could result
310
        // again in a string or an array representing multiple values
311
        if (preg_match('/^{([^}]+)}$/', $needle, $matches)) {
312
            return $this->resolveRuntimeReference(
313
                $matches[1],
314
                $formRuntime
315
            );
316
        }
317
318
        // in case string contains more than just one variable or just a static
319
        // value that does not need to be substituted at all, candidates are:
320
        // * "prefix{variable}suffix
321
        // * "{variable-1},{variable-2}"
322
        // * "some static value"
323
        // * mixed cases of the above
324
        return preg_replace_callback(
325
            '/{([^}]+)}/',
326
            function ($matches) use ($formRuntime) {
327
                $value = $this->resolveRuntimeReference(
328
                    $matches[1],
329
                    $formRuntime
330
                );
331
332
                // substitute each match by returning the resolved value
333
                if (!is_array($value)) {
334
                    return $value;
335
                }
336
337
                // now the resolve value is an array that shall substitute
338
                // a variable in a string that probably is not the only one
339
                // or is wrapped with other static string content (see above)
340
                // ... which is just not possible
341
                throw new FinisherException(
342
                    'Cannot convert array to string',
343
                    1519239265
344
                );
345
            },
346
            $needle
347
        );
348
    }
349
350
    /**
351
     * Resolving property by name from submitted form data.
352
     *
353
     * @param string $property
354
     * @param FormRuntime $formRuntime
355
     * @return int|string|array
356
     */
357
    protected function resolveRuntimeReference(string $property, FormRuntime $formRuntime)
358
    {
359
        if ($property === '__currentTimestamp') {
360
            return time();
361
        }
362
363
        // try to resolve the path '{...}' within the FormRuntime
364
        $value = ObjectAccess::getPropertyPath($formRuntime, $property);
365
366
        if (is_object($value)) {
367
            $element = $formRuntime->getFormDefinition()->getElementByIdentifier($property);
368
369
            if (!$element instanceof StringableFormElementInterface) {
370
                throw new FinisherException(
371
                    sprintf('Cannot convert object value of "%s" to string', $property),
372
                    1574362327
373
                );
374
            }
375
376
            $value = $element->valueToString($value);
377
        }
378
379
        if ($value === null) {
380
            // try to resolve the path '{...}' within the FinisherVariableProvider
381
            $value = ObjectAccess::getPropertyPath(
382
                $this->finisherContext->getFinisherVariableProvider(),
383
                $property
384
            );
385
        }
386
387
        if ($value !== null) {
388
            return $value;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value also could return the type TYPO3\CMS\Form\Domain\Fi...ain\Runtime\FormRuntime which is incompatible with the documented return type array|integer|string.
Loading history...
389
        }
390
391
        // in case no value could be resolved
392
        return '{' . $property . '}';
393
    }
394
395
    /**
396
     * Returns whether this finisher is enabled
397
     *
398
     * @return bool
399
     */
400
    public function isEnabled(): bool
401
    {
402
        return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
403
    }
404
405
    /**
406
     * @return TypoScriptFrontendController
407
     */
408
    protected function getTypoScriptFrontendController()
409
    {
410
        return $GLOBALS['TSFE'];
411
    }
412
}
413