Passed
Push — master ( 82b6fa...37388f )
by
unknown
14:03
created

findEligibleConverterWithHighestPriority()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 3
dl 0
loc 15
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 TYPO3\CMS\Core\Core\ClassLoadingInformation;
19
use TYPO3\CMS\Core\SingletonInterface;
20
use TYPO3\CMS\Extbase\Error\Error;
21
use TYPO3\CMS\Extbase\Error\Result;
22
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
23
use TYPO3\CMS\Extbase\Property\Exception\DuplicateTypeConverterException;
24
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
25
use TYPO3\CMS\Extbase\Property\Exception\InvalidSourceException;
26
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
27
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
28
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
29
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
30
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
31
32
/**
33
 * The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types.
34
 * It is used most prominently to map incoming HTTP arguments to objects.
35
 */
36
class PropertyMapper implements SingletonInterface
37
{
38
    /**
39
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
40
     */
41
    protected $objectManager;
42
43
    /**
44
     * @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder
45
     */
46
    protected $configurationBuilder;
47
48
    /**
49
     * A multi-dimensional array which stores the Type Converters available in the system.
50
     * It has the following structure:
51
     * 1. Dimension: Source Type
52
     * 2. Dimension: Target Type
53
     * 3. Dimension: Priority
54
     * Value: Type Converter instance
55
     *
56
     * @var array
57
     */
58
    protected $typeConverters = [];
59
60
    /**
61
     * A list of property mapping messages (errors, warnings) which have occurred on last mapping.
62
     *
63
     * @var \TYPO3\CMS\Extbase\Error\Result
64
     */
65
    protected $messages;
66
67
    /**
68
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
69
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
70
     */
71
    public function injectObjectManager(ObjectManagerInterface $objectManager)
72
    {
73
        $this->objectManager = $objectManager;
74
    }
75
76
    /**
77
     * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder $configurationBuilder
78
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
79
     */
80
    public function injectConfigurationBuilder(PropertyMappingConfigurationBuilder $configurationBuilder)
81
    {
82
        $this->configurationBuilder = $configurationBuilder;
83
    }
84
85
    /**
86
     * Lifecycle method, called after all dependencies have been injected.
87
     * Here, the typeConverter array gets initialized.
88
     *
89
     * @throws Exception\DuplicateTypeConverterException
90
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
91
     */
92
    public function initializeObject()
93
    {
94
        $this->resetMessages();
95
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'] as $typeConverterClassName) {
96
            $typeConverter = $this->objectManager->get($typeConverterClassName);
97
            foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) {
98
                if (isset($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()])) {
99
                    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);
100
                }
101
                $this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()] = $typeConverter;
102
            }
103
        }
104
    }
105
106
    /**
107
     * Map $source to $targetType, and return the result
108
     *
109
     * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
110
     * @param string $targetType The type of the target; can be either a class name or a simple type.
111
     * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration.
112
     * @throws Exception
113
     * @return mixed an instance of $targetType
114
     */
115
    public function convert($source, $targetType, PropertyMappingConfigurationInterface $configuration = null)
116
    {
117
        if ($configuration === null) {
118
            $configuration = $this->configurationBuilder->build();
119
        }
120
        $currentPropertyPath = [];
121
        try {
122
            $result = $this->doMapping($source, $targetType, $configuration, $currentPropertyPath);
123
            if ($result instanceof Error) {
124
                return null;
125
            }
126
127
            return $result;
128
        } catch (TargetNotFoundException $e) {
129
            throw $e;
130
        } catch (\Exception $e) {
131
            throw new Exception('Exception while property mapping at property path "' . implode('.', $currentPropertyPath) . '": ' . $e->getMessage(), 1297759968, $e);
132
        }
133
    }
134
135
    /**
136
     * Get the messages of the last Property Mapping.
137
     *
138
     * @return \TYPO3\CMS\Extbase\Error\Result
139
     */
140
    public function getMessages()
141
    {
142
        return $this->messages;
143
    }
144
145
    /**
146
     * Resets the messages of the last Property Mapping.
147
     */
148
    public function resetMessages(): void
149
    {
150
        $this->messages = new Result();
151
    }
152
153
    /**
154
     * Internal function which actually does the property mapping.
155
     *
156
     * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
157
     * @param string $targetType The type of the target; can be either a class name or a simple type.
158
     * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping.
159
     * @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown.
160
     * @throws Exception\TypeConverterException
161
     * @throws Exception\InvalidPropertyMappingConfigurationException
162
     * @return mixed an instance of $targetType
163
     */
164
    protected function doMapping($source, $targetType, PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath)
165
    {
166
        if (is_object($source)) {
167
            $targetType = $this->parseCompositeType($targetType);
168
            if ($source instanceof $targetType) {
169
                return $source;
170
            }
171
        }
172
173
        if ($source === null) {
174
            $source = '';
175
        }
176
177
        $typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
178
        $targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration);
179
180
        if (!is_object($typeConverter) || !$typeConverter instanceof TypeConverterInterface) {
181
            // todo: this Exception is never thrown as findTypeConverter returns an object or throws an Exception.
182
            throw new TypeConverterException(
183
                'Type converter for "' . $source . '" -> "' . $targetType . '" not found.',
184
                1476045062
185
            );
186
        }
187
188
        $convertedChildProperties = [];
189
        foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) {
190
            $targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName);
191
            if ($configuration->shouldSkip($targetPropertyName)) {
192
                continue;
193
            }
194
195
            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

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