Passed
Push — master ( 797014...bcb509 )
by
unknown
19:02
created

PropertyMapper::injectObjectManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Extbase\Property;
17
18
use Psr\Container\ContainerInterface;
19
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
20
use TYPO3\CMS\Core\SingletonInterface;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Extbase\Error\Error;
23
use TYPO3\CMS\Extbase\Error\Result;
24
use TYPO3\CMS\Extbase\Object\ObjectManager;
25
use TYPO3\CMS\Extbase\Property\Exception\DuplicateTypeConverterException;
26
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
27
use TYPO3\CMS\Extbase\Property\Exception\InvalidSourceException;
28
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
29
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
30
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
31
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
32
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
33
34
/**
35
 * The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types.
36
 * It is used most prominently to map incoming HTTP arguments to objects.
37
 */
38
class PropertyMapper implements SingletonInterface
39
{
40
    protected ContainerInterface $container;
41
    protected PropertyMappingConfigurationBuilder $configurationBuilder;
42
43
    /**
44
     * A multi-dimensional array which stores the Type Converters available in the system.
45
     * It has the following structure:
46
     * 1. Dimension: Source Type
47
     * 2. Dimension: Target Type
48
     * 3. Dimension: Priority
49
     * Value: Type Converter instance
50
     *
51
     * @var array
52
     */
53
    protected $typeConverters = [];
54
55
    /**
56
     * A list of property mapping messages (errors, warnings) which have occurred on last mapping.
57
     *
58
     * @var \TYPO3\CMS\Extbase\Error\Result
59
     */
60
    protected $messages;
61
62
    public function __construct(
63
        ContainerInterface $container,
64
        PropertyMappingConfigurationBuilder $configurationBuilder
65
    ) {
66
        $this->container = $container;
67
        $this->configurationBuilder = $configurationBuilder;
68
    }
69
70
    /**
71
     * Lifecycle method, called after all dependencies have been injected.
72
     * Here, the typeConverter array gets initialized.
73
     *
74
     * @throws Exception\DuplicateTypeConverterException
75
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
76
     */
77
    public function initializeObject()
78
    {
79
        $this->resetMessages();
80
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'] as $typeConverterClassName) {
81
            if ($this->container->has($typeConverterClassName)) {
82
                $typeConverter = $this->container->get($typeConverterClassName);
83
            } else {
84
                // @deprecated since v11, will be removed in v12.
85
                $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
86
                $typeConverter = $objectManager->get($typeConverterClassName);
87
            }
88
            foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) {
89
                if (isset($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()])) {
90
                    throw new DuplicateTypeConverterException('There exist at least two converters which handle the conversion from "' . $supportedSourceType . '" to "' . $typeConverter->getSupportedTargetType() . '" with priority "' . $typeConverter->getPriority() . '": ' . get_class($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()]) . ' and ' . get_class($typeConverter), 1297951378);
91
                }
92
                $this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()] = $typeConverter;
93
            }
94
        }
95
    }
96
97
    /**
98
     * Map $source to $targetType, and return the result
99
     *
100
     * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
101
     * @param string $targetType The type of the target; can be either a class name or a simple type.
102
     * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration.
103
     * @throws Exception
104
     * @return mixed an instance of $targetType
105
     */
106
    public function convert($source, $targetType, PropertyMappingConfigurationInterface $configuration = null)
107
    {
108
        if ($configuration === null) {
109
            $configuration = $this->configurationBuilder->build();
110
        }
111
        $currentPropertyPath = [];
112
        try {
113
            $result = $this->doMapping($source, $targetType, $configuration, $currentPropertyPath);
114
            if ($result instanceof Error) {
115
                return null;
116
            }
117
118
            return $result;
119
        } catch (TargetNotFoundException $e) {
120
            throw $e;
121
        } catch (\Exception $e) {
122
            throw new Exception('Exception while property mapping at property path "' . implode('.', $currentPropertyPath) . '": ' . $e->getMessage(), 1297759968, $e);
123
        }
124
    }
125
126
    /**
127
     * Get the messages of the last Property Mapping.
128
     *
129
     * @return \TYPO3\CMS\Extbase\Error\Result
130
     */
131
    public function getMessages()
132
    {
133
        return $this->messages;
134
    }
135
136
    /**
137
     * Resets the messages of the last Property Mapping.
138
     */
139
    public function resetMessages(): void
140
    {
141
        $this->messages = new Result();
142
    }
143
144
    /**
145
     * Internal function which actually does the property mapping.
146
     *
147
     * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
148
     * @param string $targetType The type of the target; can be either a class name or a simple type.
149
     * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping.
150
     * @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown.
151
     * @throws Exception\TypeConverterException
152
     * @throws Exception\InvalidPropertyMappingConfigurationException
153
     * @return mixed an instance of $targetType
154
     */
155
    protected function doMapping($source, $targetType, PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath)
156
    {
157
        if (is_object($source)) {
158
            $targetType = $this->parseCompositeType($targetType);
159
            if ($source instanceof $targetType) {
160
                return $source;
161
            }
162
        }
163
164
        if ($source === null) {
165
            $source = '';
166
        }
167
168
        $typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
169
        $targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration);
170
171
        if (!is_object($typeConverter) || !$typeConverter instanceof TypeConverterInterface) {
172
            // todo: this Exception is never thrown as findTypeConverter returns an object or throws an Exception.
173
            throw new TypeConverterException(
174
                'Type converter for "' . $source . '" -> "' . $targetType . '" not found.',
175
                1476045062
176
            );
177
        }
178
179
        $convertedChildProperties = [];
180
        foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) {
181
            $targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName);
182
            if ($configuration->shouldSkip($targetPropertyName)) {
183
                continue;
184
            }
185
186
            if (!$configuration->shouldMap($targetPropertyName)) {
0 ignored issues
show
Bug introduced by
The method shouldMap() does not exist on TYPO3\CMS\Extbase\Proper...gConfigurationInterface. Did you maybe mean shouldSkip()? ( Ignorable by Annotation )

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

186
            if (!$configuration->/** @scrutinizer ignore-call */ shouldMap($targetPropertyName)) {

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...
187
                if ($configuration->shouldSkipUnknownProperties()) {
188
                    continue;
189
                }
190
                throw new InvalidPropertyMappingConfigurationException('It is not allowed to map property "' . $targetPropertyName . '". You need to use $propertyMappingConfiguration->allowProperties(\'' . $targetPropertyName . '\') to enable mapping of this property.', 1355155913);
191
            }
192
193
            $targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $targetPropertyName, $configuration);
194
195
            $subConfiguration = $configuration->getConfigurationFor($targetPropertyName);
196
197
            $currentPropertyPath[] = $targetPropertyName;
198
            $targetPropertyValue = $this->doMapping($sourcePropertyValue, $targetPropertyType, $subConfiguration, $currentPropertyPath);
199
            array_pop($currentPropertyPath);
200
            if (!($targetPropertyValue instanceof Error)) {
201
                $convertedChildProperties[$targetPropertyName] = $targetPropertyValue;
202
            }
203
        }
204
        $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration);
205
206
        if ($result instanceof Error) {
207
            $this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result);
208
        }
209
210
        return $result;
211
    }
212
213
    /**
214
     * Determine the type converter to be used. If no converter has been found, an exception is raised.
215
     *
216
     * @param mixed $source
217
     * @param string $targetType
218
     * @param PropertyMappingConfigurationInterface $configuration
219
     * @throws Exception\TypeConverterException
220
     * @throws Exception\InvalidTargetException
221
     * @return \TYPO3\CMS\Extbase\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType.
222
     */
223
    protected function findTypeConverter($source, $targetType, PropertyMappingConfigurationInterface $configuration)
224
    {
225
        if ($configuration->getTypeConverter() !== null) {
226
            return $configuration->getTypeConverter();
227
        }
228
229
        $sourceType = $this->determineSourceType($source);
230
231
        if (!is_string($targetType)) {
0 ignored issues
show
introduced by
The condition is_string($targetType) is always true.
Loading history...
232
            throw new InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727);
233
        }
234
235
        $targetType = $this->parseCompositeType($targetType);
236
        // This is needed to correctly convert old class names to new ones
237
        // This compatibility layer will be removed with 7.0
238
        $targetType = ClassLoadingInformation::getClassNameForAlias($targetType);
239
240
        $targetType = TypeHandlingUtility::normalizeType($targetType);
241
242
        $converter = null;
243
244
        if (TypeHandlingUtility::isSimpleType($targetType)) {
245
            if (isset($this->typeConverters[$sourceType][$targetType])) {
246
                $converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType], $source, $targetType);
247
            }
248
        } else {
249
            $converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetType);
250
        }
251
252
        if ($converter === null) {
253
            throw new TypeConverterException(
254
                'No converter found which can be used to convert from "' . $sourceType . '" to "' . $targetType . '".',
255
                1476044883
256
            );
257
        }
258
259
        return $converter;
260
    }
261
262
    /**
263
     * Tries to find a suitable type converter for the given source and target type.
264
     *
265
     * @param string $source The actual source value
266
     * @param string $sourceType Type of the source to convert from
267
     * @param string $targetClass Name of the target class to find a type converter for
268
     * @return mixed Either the matching object converter or NULL
269
     * @throws Exception\InvalidTargetException
270
     */
271
    protected function findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetClass)
272
    {
273
        if (!class_exists($targetClass) && !interface_exists($targetClass)) {
274
            throw new InvalidTargetException('Could not find a suitable type converter for "' . $targetClass . '" because no such class or interface exists.', 1297948764);
275
        }
276
277
        if (!isset($this->typeConverters[$sourceType])) {
278
            return null;
279
        }
280
281
        $convertersForSource = $this->typeConverters[$sourceType];
282
        if (isset($convertersForSource[$targetClass])) {
283
            $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass], $source, $targetClass);
284
            if ($converter !== null) {
285
                return $converter;
286
            }
287
        }
288
289
        foreach (class_parents($targetClass) as $parentClass) {
290
            if (!isset($convertersForSource[$parentClass])) {
291
                continue;
292
            }
293
294
            $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$parentClass], $source, $targetClass);
295
            if ($converter !== null) {
296
                return $converter;
297
            }
298
        }
299
300
        $converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass) ?: []);
301
        $converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetClass);
302
303
        if ($converter !== null) {
304
            return $converter;
305
        }
306
        if (isset($convertersForSource['object'])) {
307
            return $this->findEligibleConverterWithHighestPriority($convertersForSource['object'], $source, $targetClass);
308
        }
309
310
        // todo: this case is impossible because at this point there must be an ObjectConverter
311
        // todo: which allowed the processing up to this point.
312
        return null;
313
    }
314
315
    /**
316
     * @param mixed $converters
317
     * @param mixed $source
318
     * @param string $targetType
319
     * @return mixed Either the matching object converter or NULL
320
     */
321
    protected function findEligibleConverterWithHighestPriority($converters, $source, $targetType)
322
    {
323
        if (!is_array($converters)) {
324
            // todo: this case is impossible as initializeObject always defines an array.
325
            return null;
326
        }
327
        krsort($converters, SORT_NUMERIC);
328
        reset($converters);
329
        /** @var AbstractTypeConverter $converter */
330
        foreach ($converters as $converter) {
331
            if ($converter->canConvertFrom($source, $targetType)) {
332
                return $converter;
333
            }
334
        }
335
        return null;
336
    }
337
338
    /**
339
     * @param array $convertersForSource
340
     * @param array $interfaceNames
341
     * @return array
342
     * @throws Exception\DuplicateTypeConverterException
343
     */
344
    protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames)
345
    {
346
        $convertersForInterface = [];
347
        foreach ($interfaceNames as $implementedInterface) {
348
            if (isset($convertersForSource[$implementedInterface])) {
349
                foreach ($convertersForSource[$implementedInterface] as $priority => $converter) {
350
                    if (isset($convertersForInterface[$priority])) {
351
                        throw new DuplicateTypeConverterException('There exist at least two converters which handle the conversion to an interface with priority "' . $priority . '". ' . get_class($convertersForInterface[$priority]) . ' and ' . get_class($converter), 1297951338);
352
                    }
353
                    $convertersForInterface[$priority] = $converter;
354
                }
355
            }
356
        }
357
        return $convertersForInterface;
358
    }
359
360
    /**
361
     * Determine the type of the source data, or throw an exception if source was an unsupported format.
362
     *
363
     * @param mixed $source
364
     * @throws Exception\InvalidSourceException
365
     * @return string the type of $source
366
     */
367
    protected function determineSourceType($source)
368
    {
369
        if (is_string($source)) {
370
            return 'string';
371
        }
372
        if (is_array($source)) {
373
            return 'array';
374
        }
375
        if (is_float($source)) {
376
            return 'float';
377
        }
378
        if (is_int($source)) {
379
            return 'integer';
380
        }
381
        if (is_bool($source)) {
382
            return 'boolean';
383
        }
384
        throw new InvalidSourceException('The source is not of type string, array, float, integer or boolean, but of type "' . gettype($source) . '"', 1297773150);
385
    }
386
387
    /**
388
     * Parse a composite type like \Foo\Collection<\Bar\Entity> into
389
     * \Foo\Collection
390
     *
391
     * @param string $compositeType
392
     * @return string
393
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
394
     */
395
    public function parseCompositeType($compositeType)
396
    {
397
        if (strpos($compositeType, '<') !== false) {
398
            $compositeType = substr($compositeType, 0, strpos($compositeType, '<'));
399
        }
400
        return $compositeType;
401
    }
402
}
403