ConfigurationObjectMapper::parseCompositeType()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
/*
3
 * 2018 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 Configuration Object 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\ConfigurationObject;
15
16
use Romm\ConfigurationObject\Core\Core;
17
use Romm\ConfigurationObject\Core\Service\ReflectionService;
18
use Romm\ConfigurationObject\Legacy\Reflection\PropertyReflection;
19
use Romm\ConfigurationObject\Service\DataTransferObject\ConfigurationObjectConversionDTO;
20
use Romm\ConfigurationObject\Service\DataTransferObject\GetTypeConverterDTO;
21
use Romm\ConfigurationObject\Service\Event\ObjectConversionAfterServiceEventInterface;
22
use Romm\ConfigurationObject\Service\Event\ObjectConversionBeforeServiceEventInterface;
23
use Romm\ConfigurationObject\Service\Items\DataPreProcessor\DataPreProcessorService;
24
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesResolver;
25
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesService;
26
use Romm\ConfigurationObject\Service\ServiceFactory;
27
use Romm\ConfigurationObject\Service\ServiceInterface;
28
use Romm\ConfigurationObject\TypeConverter\ArrayConverter;
29
use Romm\ConfigurationObject\TypeConverter\ConfigurationObjectConverter;
30
use TYPO3\CMS\Extbase\Error\Error;
31
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
32
use TYPO3\CMS\Extbase\Property\PropertyMapper;
33
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
34
use TYPO3\CMS\Extbase\Property\TypeConverter\ArrayConverter as ExtbaseArrayConverter;
35
use TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter;
36
use TYPO3\CMS\Extbase\Property\TypeConverterInterface;
37
38
/**
39
 * Custom mapper used for configuration objects.
40
 *
41
 * The mapper will recursively go through all the object properties, and use a
42
 * correct type converter (fetched from the property reflection) to fill the
43
 * property with the given value.
44
 *
45
 * Note that this class inherits from the default Extbase `PropertyMapper`,
46
 * because existing functionality is still used.
47
 */
48
class ConfigurationObjectMapper extends PropertyMapper
49
{
50
    /**
51
     * Contains the initial called target type.
52
     *
53
     * @var string
54
     */
55
    protected $rootTargetType;
56
57
    /**
58
     * @var ServiceFactory
59
     */
60
    protected $serviceFactory;
61
62
    /**
63
     * @var ConfigurationObjectConversionDTO
64
     */
65
    protected $configurationObjectConversionDTO;
66
67
    /**
68
     * @var GetTypeConverterDTO
69
     */
70
    protected $getTypeConverterDTO;
71
72
    /**
73
     * @var array
74
     */
75
    protected $existingClassList = [];
76
77
    /**
78
     * @var array
79
     */
80
    protected $typeProperties = [];
81
82
    /**
83
     * @inheritdoc
84
     */
85
    public function convert($source, $targetType, PropertyMappingConfigurationInterface $configuration = null)
86
    {
87
        $this->rootTargetType = $targetType;
88
        $this->serviceFactory = ConfigurationObjectFactory::getInstance()
89
            ->getConfigurationObjectServiceFactory($targetType);
90
91
        $this->configurationObjectConversionDTO = new ConfigurationObjectConversionDTO($this->rootTargetType, $this->serviceFactory);
92
        $this->getTypeConverterDTO = new GetTypeConverterDTO($this->rootTargetType, $this->serviceFactory);
93
94
        $result = call_user_func_array(['parent', 'convert'], func_get_args());
95
96
        unset($this->configurationObjectConversionDTO);
97
        unset($this->getTypeConverterDTO);
98
99
        return $result;
100
    }
101
102
    /**
103
     * Will recursively fill all the properties of the configuration object.
104
     *
105
     * @inheritdoc
106
     */
107
    protected function doMapping($source, $targetType, PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath)
108
    {
109
        if ($source === null) {
110
            return null;
111
        }
112
113
        $typeConverter = $this->getTypeConverter($source, $targetType, $configuration);
114
        $targetType = ltrim($typeConverter->getTargetTypeForSource($source, $targetType), '\\');
115
116
        if (Core::get()->classExists($targetType)) {
117
            $targetType = $this->handleMixedType($source, $targetType, $currentPropertyPath);
118
            $source = $this->handleDataPreProcessor($source, $targetType, $currentPropertyPath);
119
120
            if (MixedTypesResolver::OBJECT_TYPE_NONE === $targetType) {
121
                return null;
122
            }
123
        }
124
125
        $convertedChildProperties = (is_array($source))
126
            ? $this->convertChildProperties($source, $targetType, $typeConverter, $configuration, $currentPropertyPath)
0 ignored issues
show
Bug introduced by
It seems like $targetType defined by $this->handleMixedType($..., $currentPropertyPath) on line 117 can also be of type object<Romm\Configuratio...urationObjectInterface>; however, Romm\ConfigurationObject...onvertChildProperties() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
127
            : [];
128
129
        $this->configurationObjectConversionDTO
130
            ->setSource($source)
131
            ->setTargetType($targetType)
0 ignored issues
show
Bug introduced by
It seems like $targetType defined by $this->handleMixedType($..., $currentPropertyPath) on line 117 can also be of type object<Romm\Configuratio...urationObjectInterface>; however, Romm\ConfigurationObject...ionDTO::setTargetType() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
132
            ->setConvertedChildProperties($convertedChildProperties)
133
            ->setCurrentPropertyPath($currentPropertyPath)
134
            ->setResult(null);
135
        $this->serviceFactory->runServicesFromEvent(ObjectConversionBeforeServiceEventInterface::class, 'objectConversionBefore', $this->configurationObjectConversionDTO);
136
137
        if (null === $this->configurationObjectConversionDTO->getResult()) {
138
            $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties);
0 ignored issues
show
Bug introduced by
It seems like $targetType defined by $this->handleMixedType($..., $currentPropertyPath) on line 117 can also be of type object<Romm\Configuratio...urationObjectInterface>; however, TYPO3\CMS\Extbase\Proper...nterface::convertFrom() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
139
            $this->configurationObjectConversionDTO->setResult($result);
140
        }
141
142
        $this->serviceFactory->runServicesFromEvent(ObjectConversionAfterServiceEventInterface::class, 'objectConversionAfter', $this->configurationObjectConversionDTO);
143
        $result = $this->configurationObjectConversionDTO->getResult();
144
145
        if ($result instanceof Error) {
146
            $this->messages
147
                ->forProperty(implode('.', $currentPropertyPath))
148
                ->addError($result);
149
        }
150
151
        return $result;
152
    }
153
154
    /**
155
     * Will convert all the properties of the given source, depending on the
156
     * target type.
157
     *
158
     * @param array                                 $source
159
     * @param string                                $targetType
160
     * @param TypeConverterInterface                $typeConverter
161
     * @param PropertyMappingConfigurationInterface $configuration
162
     * @param array                                 $currentPropertyPath
163
     * @return array
164
     */
165
    protected function convertChildProperties(array $source, $targetType, TypeConverterInterface $typeConverter, PropertyMappingConfigurationInterface $configuration, array &$currentPropertyPath)
166
    {
167
        $convertedChildProperties = [];
168
        $properties = $source;
169
170
        // If the target is a class, we get its properties, else we assume the source should be converted.
171
        if (Core::get()->classExists($targetType)) {
172
            $properties = $this->getProperties($targetType);
173
        }
174
175
        foreach ($source as $propertyName => $propertyValue) {
176
            if (array_key_exists($propertyName, $properties)) {
177
                $currentPropertyPath[] = $propertyName;
178
                $targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $propertyName, $configuration);
179
                $targetPropertyTypeBis = $this->checkMixedTypeAnnotationForProperty($targetType, $propertyName, $targetPropertyType);
180
                $targetPropertyType = $targetPropertyTypeBis ?: $targetPropertyType;
181
182
                $targetPropertyValue = (null !== $targetPropertyType)
183
                    ? $this->doMapping($propertyValue, $targetPropertyType, $configuration, $currentPropertyPath)
184
                    : $propertyValue;
185
186
                array_pop($currentPropertyPath);
187
188
                if (false === $targetPropertyValue instanceof Error) {
189
                    $convertedChildProperties[$propertyName] = $targetPropertyValue;
190
                }
191
            }
192
        }
193
194
        return $convertedChildProperties;
195
    }
196
197
    /**
198
     * @param string $targetType
199
     * @param string $propertyName
200
     * @param string $propertyType
201
     * @return null|string
202
     */
203
    protected function checkMixedTypeAnnotationForProperty($targetType, $propertyName, $propertyType)
204
    {
205
        $result = null;
206
207
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_MIXED_TYPES)) {
208
            /** @var MixedTypesService $mixedTypesService */
209
            $mixedTypesService = $this->serviceFactory->get(ServiceInterface::SERVICE_MIXED_TYPES);
210
211
            // Is the property composite?
212
            $isComposite = $this->parseCompositeType($propertyType) !== $propertyType;
213
214
            $result = $mixedTypesService->checkMixedTypeAnnotationForProperty($targetType, $propertyName, $isComposite);
0 ignored issues
show
Documentation introduced by
$isComposite is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
215
        }
216
217
        return $result;
218
    }
219
220
    /**
221
     * Will check if the target type class inherits of `MixedTypeInterface`. If
222
     * so, it means the real type of the target must be fetched through the
223
     * function `getInstanceClassName()`.
224
     *
225
     * @param mixed $source
226
     * @param mixed $targetType
227
     * @param array $currentPropertyPath
228
     * @return ConfigurationObjectInterface
229
     */
230
    protected function handleMixedType($source, $targetType, $currentPropertyPath)
231
    {
232
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_MIXED_TYPES)) {
233
            /** @var MixedTypesService $mixedTypesService */
234
            $mixedTypesService = $this->serviceFactory->get(ServiceInterface::SERVICE_MIXED_TYPES);
235
236
            if ($mixedTypesService->classIsMixedTypeResolver($targetType)) {
237
                $resolver = $mixedTypesService->getMixedTypesResolver($source, $targetType);
238
                $targetType = $resolver->getObjectType();
239
                $resolverResult = $resolver->getResult();
240
241
                if ($resolverResult->hasErrors()) {
242
                    $targetType = MixedTypesResolver::OBJECT_TYPE_NONE;
243
                    $this->messages->forProperty(implode('.', $currentPropertyPath))->merge($resolverResult);
244
                }
245
            }
246
        }
247
248
        return $targetType;
249
    }
250
251
    /**
252
     * Will check if the target type is a class, then call functions which will
253
     * check the interfaces of the class.
254
     *
255
     * @param mixed $source
256
     * @param mixed $targetType
257
     * @param array $currentPropertyPath
258
     * @return array
259
     */
260
    protected function handleDataPreProcessor($source, $targetType, $currentPropertyPath)
261
    {
262
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_DATA_PRE_PROCESSOR)) {
263
            /** @var DataPreProcessorService $dataProcessorService */
264
            $dataProcessorService = $this->serviceFactory->get(ServiceInterface::SERVICE_DATA_PRE_PROCESSOR);
265
266
            $processor = $dataProcessorService->getDataPreProcessor($source, $targetType);
267
            $source = $processor->getData();
268
            $processorResult = $processor->getResult();
269
270
            if ($processorResult->hasErrors()) {
271
                $this->messages->forProperty(implode('.', $currentPropertyPath))->merge($processorResult);
272
            }
273
        }
274
275
        return $source;
276
    }
277
278
    /**
279
     * This function will fetch the type converter which will convert the source
280
     * to the requested target type.
281
     *
282
     * @param mixed $source
283
     * @param mixed $targetType
284
     * @param mixed $configuration
285
     * @return TypeConverterInterface
286
     * @throws TypeConverterException
287
     */
288
    protected function getTypeConverter($source, $targetType, $configuration)
289
    {
290
        $compositeType = $this->parseCompositeType($targetType);
291
292
        if (in_array($compositeType, ['\\ArrayObject', 'array'])) {
293
            $typeConverter = $this->objectManager->get(ArrayConverter::class);
294
        } else {
295
            $typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
296
297
            if ($typeConverter instanceof ExtbaseArrayConverter) {
298
                $typeConverter = $this->objectManager->get(ArrayConverter::class);
299
            } elseif ($typeConverter instanceof ObjectConverter) {
300
                $typeConverter = $this->getObjectConverter();
301
            }
302
        }
303
304
        if (!is_object($typeConverter) || !$typeConverter instanceof TypeConverterInterface) {
305
            throw new TypeConverterException('Type converter for "' . $source . '" -> "' . $targetType . '" not found.');
306
        }
307
308
        return $typeConverter;
309
    }
310
311
    /**
312
     * @param string $compositeType
313
     * @return string
314
     */
315
    public function parseCompositeType($compositeType)
316
    {
317
        if ('[]' === substr($compositeType, -2)) {
318
            return '\\ArrayObject';
319
        } else {
320
            return parent::parseCompositeType($compositeType);
321
        }
322
    }
323
324
    /**
325
     * Internal function that fetches the properties of a class.
326
     *
327
     * @param $targetType
328
     * @return array
329
     */
330
    protected function getProperties($targetType)
331
    {
332
        $properties = ReflectionService::get()->getClassReflection($targetType)->getProperties();
333
        $propertiesKeys = array_map(
334
            function (PropertyReflection $propertyReflection) {
335
                return $propertyReflection->getName();
336
            },
337
            $properties
338
        );
339
340
        return array_combine($propertiesKeys, $properties);
341
    }
342
343
    /**
344
     * @return ConfigurationObjectConverter
345
     */
346
    protected function getObjectConverter()
347
    {
348
        return $this->objectManager->get(ConfigurationObjectConverter::class);
349
    }
350
}
351