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

ObjectConverter::buildObject()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 28
rs 8.8333
c 0
b 0
f 0
cc 7
nc 6
nop 2
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
namespace TYPO3\CMS\Extbase\Property\TypeConverter;
19
20
use Psr\Container\ContainerInterface;
21
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
22
use TYPO3\CMS\Extbase\Object\Container\Container;
23
use TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException;
24
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
25
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
26
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
27
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
28
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException;
29
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
30
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
31
32
/**
33
 * This converter transforms arrays to simple objects (POPO) by setting properties.
34
 */
35
class ObjectConverter extends AbstractTypeConverter
36
{
37
    /**
38
     * @var int
39
     */
40
    const CONFIGURATION_TARGET_TYPE = 3;
41
42
    /**
43
     * @var int
44
     */
45
    const CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED = 4;
46
47
    /**
48
     * @var array
49
     */
50
    protected $sourceTypes = ['array'];
51
52
    /**
53
     * @var string
54
     */
55
    protected $targetType = 'object';
56
57
    /**
58
     * @var int
59
     */
60
    protected $priority = 10;
61
62
    /**
63
     * @var \TYPO3\CMS\Extbase\Object\Container\Container
64
     * @deprecated since v11, will be removed in v12.
65
     */
66
    protected $objectContainer;
67
68
    protected ContainerInterface $container;
69
70
    /**
71
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
72
     */
73
    protected $reflectionService;
74
75
    /**
76
     * @param \TYPO3\CMS\Extbase\Object\Container\Container $objectContainer
77
     * @deprecated since v11, will be removed in v12.
78
     */
79
    public function injectObjectContainer(Container $objectContainer): void
80
    {
81
        $this->objectContainer = $objectContainer;
82
    }
83
84
    /**
85
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
86
     */
87
    public function injectReflectionService(ReflectionService $reflectionService): void
88
    {
89
        $this->reflectionService = $reflectionService;
90
    }
91
92
    public function injectContainer(ContainerInterface $container)
93
    {
94
        $this->container = $container;
95
    }
96
97
    /**
98
     * Only convert non-persistent types
99
     *
100
     * @param mixed $source
101
     * @param string $targetType
102
     * @return bool
103
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
104
     */
105
    public function canConvertFrom($source, string $targetType): bool
106
    {
107
        return !is_subclass_of($targetType, AbstractDomainObject::class);
108
    }
109
110
    /**
111
     * Convert all properties in the source array
112
     *
113
     * @param mixed $source
114
     * @return array
115
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
116
     */
117
    public function getSourceChildPropertiesToBeConverted($source): array
118
    {
119
        if (isset($source['__type'])) {
120
            unset($source['__type']);
121
        }
122
        return $source;
123
    }
124
125
    /**
126
     * The type of a property is determined by the reflection service.
127
     *
128
     * @param string $targetType
129
     * @param string $propertyName
130
     * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
131
     * @return string
132
     * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException
133
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
134
     */
135
    public function getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration): string
136
    {
137
        $configuredTargetType = $configuration->getConfigurationFor($propertyName)->getConfigurationValue(\TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_TARGET_TYPE);
138
        if ($configuredTargetType !== null) {
139
            return $configuredTargetType;
140
        }
141
142
        // @deprecated since v11, will be removed in v12: ContainerInterface resolves class names. v12: drop next line.
143
        $specificTargetType = $this->objectContainer->getImplementationClassName($targetType);
144
        $classSchema = $this->reflectionService->getClassSchema($specificTargetType);
145
146
        $methodName = 'set' . ucfirst($propertyName);
147
        if ($classSchema->hasMethod($methodName)) {
148
            $methodParameters = $classSchema->getMethod($methodName)->getParameters() ?? [];
149
            $methodParameter = current($methodParameters);
150
            if ($methodParameter->getType() === null) {
151
                throw new InvalidTargetException('Setter for property "' . $propertyName . '" had no type hint or documentation in target object of type "' . $specificTargetType . '".', 1303379158);
152
            }
153
            return $methodParameter->getType();
154
        }
155
        try {
156
            $parameterType = $classSchema->getMethod('__construct')->getParameter($propertyName)->getType();
157
        } catch (NoSuchMethodException $e) {
158
            $exceptionMessage = sprintf('Type of child property "%s" of class "%s" could not be '
159
                . 'derived from constructor arguments as said class does not have a constructor '
160
                . 'defined.', $propertyName, $specificTargetType);
161
            throw new InvalidTargetException($exceptionMessage, 1582385098);
162
        } catch (NoSuchMethodParameterException $e) {
163
            $exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
164
                . 'derived from constructor arguments as the constructor of said class does not '
165
                . 'have a parameter with property name "%1$s".', $propertyName, $specificTargetType);
166
            throw new InvalidTargetException($exceptionMessage, 1303379126);
167
        }
168
169
        if ($parameterType === null) {
170
            $exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
171
                . 'derived from constructor argument "%1$s". This usually happens if the argument '
172
                . 'misses a type hint.', $propertyName, $specificTargetType);
173
            throw new InvalidTargetException($exceptionMessage, 1582385619);
174
        }
175
        return $parameterType;
176
    }
177
178
    /**
179
     * Convert an object from $source to an object.
180
     *
181
     * @param mixed $source
182
     * @param string $targetType
183
     * @param array $convertedChildProperties
184
     * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
185
     * @return object|null the target type
186
     * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException
187
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
188
     */
189
    public function convertFrom($source, string $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null): ?object
190
    {
191
        $object = $this->buildObject($convertedChildProperties, $targetType);
192
        foreach ($convertedChildProperties as $propertyName => $propertyValue) {
193
            $result = ObjectAccess::setProperty($object, $propertyName, $propertyValue);
194
            if ($result === false) {
195
                $exceptionMessage = sprintf(
196
                    'Property "%s" having a value of type "%s" could not be set in target object of type "%s". Make sure that the property is accessible properly, for example via an appropriate setter method.',
197
                    $propertyName,
198
                    (is_object($propertyValue) ? get_class($propertyValue) : gettype($propertyValue)),
199
                    $targetType
200
                );
201
                throw new InvalidTargetException($exceptionMessage, 1304538165);
202
            }
203
        }
204
205
        return $object;
206
    }
207
208
    /**
209
     * Determines the target type based on the source's (optional) __type key.
210
     *
211
     * @param mixed $source
212
     * @param string $originalTargetType
213
     * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
214
     * @return string
215
     * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException
216
     * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException
217
     * @throws \InvalidArgumentException
218
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
219
     */
220
    public function getTargetTypeForSource($source, string $originalTargetType, PropertyMappingConfigurationInterface $configuration = null): string
221
    {
222
        $targetType = $originalTargetType;
223
224
        if (is_array($source) && array_key_exists('__type', $source)) {
225
            $targetType = $source['__type'];
226
227
            if ($configuration === null) {
228
                // todo: this is impossible to achieve since this methods is always called via (convert -> doMapping -> getTargetTypeForSource) and convert and doMapping create configuration objects if missing.
229
                throw new \InvalidArgumentException('A property mapping configuration must be given, not NULL.', 1326277369);
230
            }
231
            if ($configuration->getConfigurationValue(\TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED) !== true) {
232
                throw new InvalidPropertyMappingConfigurationException('Override of target type not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED" to TRUE.', 1317050430);
233
            }
234
235
            if ($targetType !== $originalTargetType && is_a($targetType, $originalTargetType, true) === false) {
236
                throw new InvalidDataTypeException('The given type "' . $targetType . '" is not a subtype of "' . $originalTargetType . '".', 1317048056);
237
            }
238
        }
239
240
        return $targetType;
241
    }
242
243
    /**
244
     * Builds a new instance of $objectType with the given $possibleConstructorArgumentValues. If
245
     * constructor argument values are missing from the given array the method
246
     * looks for a default value in the constructor signature. Furthermore, the constructor arguments are removed from $possibleConstructorArgumentValues
247
     *
248
     * @param array $possibleConstructorArgumentValues
249
     * @param string $objectType
250
     * @return object The created instance
251
     * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException if a required constructor argument is missing
252
     */
253
    protected function buildObject(array &$possibleConstructorArgumentValues, string $objectType): object
254
    {
255
        if (empty($possibleConstructorArgumentValues) && $this->container->has($objectType)) {
256
            return $this->container->get($objectType);
257
        }
258
259
        // @deprecated since v11, will be removed in v12: ContainerInterface resolves class names. v12: Drop everything below.
260
        $specificObjectType = $this->objectContainer->getImplementationClassName($objectType);
261
        $classSchema = $this->reflectionService->getClassSchema($specificObjectType);
262
263
        if ($classSchema->hasConstructor()) {
264
            $constructor = $classSchema->getMethod('__construct');
265
            $constructorArguments = [];
266
            foreach ($constructor->getParameters() as $parameterName => $parameter) {
267
                if (array_key_exists($parameterName, $possibleConstructorArgumentValues)) {
268
                    $constructorArguments[] = $possibleConstructorArgumentValues[$parameterName];
269
                    unset($possibleConstructorArgumentValues[$parameterName]);
270
                } elseif ($parameter->isOptional()) {
271
                    $constructorArguments[] = $parameter->getDefaultValue();
272
                } else {
273
                    throw new InvalidTargetException('Missing constructor argument "' . $parameterName . '" for object of type "' . $objectType . '".', 1268734872);
274
                }
275
            }
276
            // @deprecated since v11, will be removed in v12
277
            return $this->objectManager->get(...[$objectType, ...$constructorArguments]);
0 ignored issues
show
Bug introduced by
It seems like array($objectType, $constructorArguments) can also be of type array and array; however, parameter $objectName of TYPO3\CMS\Extbase\Object...ManagerInterface::get() 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

277
            return $this->objectManager->get(/** @scrutinizer ignore-type */ ...[$objectType, ...$constructorArguments]);
Loading history...
278
        }
279
        // @deprecated since v11, will be removed in v12
280
        return $this->objectManager->get($objectType);
281
    }
282
}
283