Completed
Push — development ( 3e8fd0...082d81 )
by Romain
02:12
created

ConfigurationObjectMapper::parseCompositeType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/*
3
 * 2017 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\Service\DataTransferObject\ConfigurationObjectConversionDTO;
19
use Romm\ConfigurationObject\Service\DataTransferObject\GetTypeConverterDTO;
20
use Romm\ConfigurationObject\Service\Event\ObjectConversionAfterServiceEventInterface;
21
use Romm\ConfigurationObject\Service\Event\ObjectConversionBeforeServiceEventInterface;
22
use Romm\ConfigurationObject\Service\Items\DataPreProcessor\DataPreProcessorService;
23
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesResolver;
24
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesService;
25
use Romm\ConfigurationObject\Service\ServiceFactory;
26
use Romm\ConfigurationObject\Service\ServiceInterface;
27
use Romm\ConfigurationObject\TypeConverter\ArrayConverter;
28
use Romm\ConfigurationObject\TypeConverter\ConfigurationObjectConverter;
29
use TYPO3\CMS\Extbase\Error\Error;
30
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
31
use TYPO3\CMS\Extbase\Property\PropertyMapper;
32
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
33
use TYPO3\CMS\Extbase\Property\TypeConverter\ArrayConverter as ExtbaseArrayConverter;
34
use TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter;
35
use TYPO3\CMS\Extbase\Property\TypeConverterInterface;
36
use TYPO3\CMS\Extbase\Reflection\PropertyReflection;
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
        $typeConverter = $this->getTypeConverter($source, $targetType, $configuration);
110
        $targetType = ltrim($typeConverter->getTargetTypeForSource($source, $targetType), '\\');
111
112
        if (Core::get()->classExists($targetType)) {
113
            $source = $this->handleDataPreProcessor($source, $targetType, $currentPropertyPath);
114
            $targetType = $this->handleMixedType($source, $targetType, $currentPropertyPath);
115
116
            if (MixedTypesResolver::OBJECT_TYPE_NONE === $targetType) {
117
                return null;
118
            }
119
        }
120
121
        $convertedChildProperties = (is_array($source))
122
            ? $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 114 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...
123
            : [];
124
125
        $this->configurationObjectConversionDTO
126
            ->setSource($source)
127
            ->setTargetType($targetType)
0 ignored issues
show
Bug introduced by
It seems like $targetType defined by $this->handleMixedType($..., $currentPropertyPath) on line 114 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...
128
            ->setConvertedChildProperties($convertedChildProperties)
129
            ->setCurrentPropertyPath($currentPropertyPath)
130
            ->setResult(null);
131
        $this->serviceFactory->runServicesFromEvent(ObjectConversionBeforeServiceEventInterface::class, 'objectConversionBefore', $this->configurationObjectConversionDTO);
132
133
        if (null === $this->configurationObjectConversionDTO->getResult()) {
134
            $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties);
0 ignored issues
show
Bug introduced by
It seems like $targetType defined by $this->handleMixedType($..., $currentPropertyPath) on line 114 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...
135
            $this->configurationObjectConversionDTO->setResult($result);
136
        }
137
138
        $this->serviceFactory->runServicesFromEvent(ObjectConversionAfterServiceEventInterface::class, 'objectConversionAfter', $this->configurationObjectConversionDTO);
139
        $result = $this->configurationObjectConversionDTO->getResult();
140
141
        if ($result instanceof Error) {
142
            $this->messages
143
                ->forProperty(implode('.', $currentPropertyPath))
144
                ->addError($result);
145
        }
146
147
        return $result;
148
    }
149
150
    /**
151
     * Will convert all the properties of the given source, depending on the
152
     * target type.
153
     *
154
     * @param array                                 $source
155
     * @param string                                $targetType
156
     * @param TypeConverterInterface                $typeConverter
157
     * @param PropertyMappingConfigurationInterface $configuration
158
     * @param array                                 $currentPropertyPath
159
     * @return array
160
     */
161
    protected function convertChildProperties(array $source, $targetType, TypeConverterInterface $typeConverter, PropertyMappingConfigurationInterface $configuration, array &$currentPropertyPath)
162
    {
163
        $convertedChildProperties = [];
164
        $properties = $source;
165
166
        // If the target is a class, we get its properties, else we assume the source should be converted.
167
        if (Core::get()->classExists($targetType)) {
168
            $properties = $this->getProperties($targetType);
169
        }
170
171
        foreach ($source as $propertyName => $propertyValue) {
172
            if (array_key_exists($propertyName, $properties)) {
173
                $currentPropertyPath[] = $propertyName;
174
                $targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $propertyName, $configuration);
175
                $targetPropertyTypeBis = $this->checkMixedTypeAnnotationForProperty($targetType, $propertyName, $targetPropertyType);
176
                $targetPropertyType = $targetPropertyTypeBis ?: $targetPropertyType;
177
178
                $targetPropertyValue = (null !== $targetPropertyType)
179
                    ? $this->doMapping($propertyValue, $targetPropertyType, $configuration, $currentPropertyPath)
180
                    : $propertyValue;
181
182
                array_pop($currentPropertyPath);
183
184
                if (false === $targetPropertyValue instanceof Error) {
185
                    $convertedChildProperties[$propertyName] = $targetPropertyValue;
186
                }
187
            }
188
        }
189
190
        return $convertedChildProperties;
191
    }
192
193
    /**
194
     * @param string $targetType
195
     * @param string $propertyName
196
     * @param string $propertyType
197
     * @return null|string
198
     */
199
    protected function checkMixedTypeAnnotationForProperty($targetType, $propertyName, $propertyType)
200
    {
201
        $result = null;
202
203
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_MIXED_TYPES)) {
204
            /** @var MixedTypesService $mixedTypesService */
205
            $mixedTypesService = $this->serviceFactory->get(ServiceInterface::SERVICE_MIXED_TYPES);
206
207
            // Is the property composite?
208
            $isComposite = $this->parseCompositeType($propertyType) !== $propertyType;
209
210
            $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...
211
        }
212
213
        return $result;
214
    }
215
216
    /**
217
     * Will check if the target type class inherits of `MixedTypeInterface`. If
218
     * so, it means the real type of the target must be fetched through the
219
     * function `getInstanceClassName()`.
220
     *
221
     * @param mixed $source
222
     * @param mixed $targetType
223
     * @param array $currentPropertyPath
224
     * @return ConfigurationObjectInterface
225
     */
226
    protected function handleMixedType($source, $targetType, $currentPropertyPath)
227
    {
228
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_MIXED_TYPES)) {
229
            /** @var MixedTypesService $mixedTypesService */
230
            $mixedTypesService = $this->serviceFactory->get(ServiceInterface::SERVICE_MIXED_TYPES);
231
232
            if ($mixedTypesService->classIsMixedTypeResolver($targetType)) {
233
                $resolver = $mixedTypesService->getMixedTypesResolver($source, $targetType);
234
                $targetType = $resolver->getObjectType();
235
                $resolverResult = $resolver->getResult();
236
237
                if ($resolverResult->hasErrors()) {
238
                    $targetType = MixedTypesResolver::OBJECT_TYPE_NONE;
239
                    $this->messages->forProperty(implode('.', $currentPropertyPath))->merge($resolverResult);
240
                }
241
            }
242
        }
243
244
        return $targetType;
245
    }
246
247
    /**
248
     * Will check if the target type is a class, then call functions which will
249
     * check the interfaces of the class.
250
     *
251
     * @param mixed $source
252
     * @param mixed $targetType
253
     * @param array $currentPropertyPath
254
     * @return array
255
     */
256
    protected function handleDataPreProcessor($source, $targetType, $currentPropertyPath)
257
    {
258
        if ($this->serviceFactory->has(ServiceInterface::SERVICE_DATA_PRE_PROCESSOR)) {
259
            /** @var DataPreProcessorService $dataProcessorService */
260
            $dataProcessorService = $this->serviceFactory->get(ServiceInterface::SERVICE_DATA_PRE_PROCESSOR);
261
262
            $processor = $dataProcessorService->getDataPreProcessor($source, $targetType);
263
            $source = $processor->getData();
264
            $processorResult = $processor->getResult();
265
266
            if ($processorResult->hasErrors()) {
267
                $this->messages->forProperty(implode('.', $currentPropertyPath))->merge($processorResult);
268
            }
269
        }
270
271
        return $source;
272
    }
273
274
    /**
275
     * This function will fetch the type converter which will convert the source
276
     * to the requested target type.
277
     *
278
     * @param mixed $source
279
     * @param mixed $targetType
280
     * @param mixed $configuration
281
     * @return TypeConverterInterface
282
     * @throws TypeConverterException
283
     */
284
    protected function getTypeConverter($source, $targetType, $configuration)
285
    {
286
        $compositeType = $this->parseCompositeType($targetType);
287
288
        if (in_array($compositeType, ['\\ArrayObject', 'array'])) {
289
            $typeConverter = $this->objectManager->get(ArrayConverter::class);
290
        } else {
291
            $typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
292
293
            if ($typeConverter instanceof ExtbaseArrayConverter) {
294
                $typeConverter = $this->objectManager->get(ArrayConverter::class);
295
            } elseif ($typeConverter instanceof ObjectConverter) {
296
                $typeConverter = $this->getObjectConverter();
297
            }
298
        }
299
300
        if (!is_object($typeConverter) || !$typeConverter instanceof TypeConverterInterface) {
301
            throw new TypeConverterException('Type converter for "' . $source . '" -> "' . $targetType . '" not found.');
302
        }
303
304
        return $typeConverter;
305
    }
306
307
    /**
308
     * @param string $compositeType
309
     * @return string
310
     */
311
    public function parseCompositeType($compositeType)
312
    {
313
        if ('[]' === substr($compositeType, -2)) {
314
            return '\\ArrayObject';
315
        } else {
316
            return parent::parseCompositeType($compositeType);
317
        }
318
    }
319
320
    /**
321
     * Internal function that fetches the properties of a class.
322
     *
323
     * @param $targetType
324
     * @return array
325
     */
326
    protected function getProperties($targetType)
327
    {
328
        $properties = ReflectionService::get()->getClassReflection($targetType)->getProperties();
329
        $propertiesKeys = array_map(
330
            function (PropertyReflection $propertyReflection) {
331
                return $propertyReflection->getName();
332
            },
333
            $properties
334
        );
335
336
        return array_combine($propertiesKeys, $properties);
337
    }
338
339
    /**
340
     * @return ConfigurationObjectConverter
341
     */
342
    protected function getObjectConverter()
343
    {
344
        return $this->objectManager->get(ConfigurationObjectConverter::class);
345
    }
346
}
347