/
ObjectConverter.php
257 lines (231 loc) · 13.2 KB
/
ObjectConverter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Property\TypeConverter;
use Psr\Container\ContainerInterface;
use Symfony\Component\PropertyInfo\Type;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
/**
* This converter transforms arrays to simple objects (POPO) by setting properties.
*/
class ObjectConverter extends AbstractTypeConverter
{
/**
* @var int
*/
public const CONFIGURATION_TARGET_TYPE = 3;
/**
* @var int
*/
public const CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED = 4;
protected ContainerInterface $container;
/**
* @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
*/
protected $reflectionService;
public function injectReflectionService(ReflectionService $reflectionService): void
{
$this->reflectionService = $reflectionService;
}
public function injectContainer(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Convert all properties in the source array
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getSourceChildPropertiesToBeConverted($source): array
{
if (isset($source['__type'])) {
unset($source['__type']);
}
return $source;
}
/**
* The type of a property is determined by the reflection service.
*
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration): string
{
$configuredTargetType = $configuration->getConfigurationFor($propertyName)->getConfigurationValue(\TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_TARGET_TYPE);
if ($configuredTargetType !== null) {
return $configuredTargetType;
}
$classSchema = $this->reflectionService->getClassSchema($targetType);
// @todo: infer property type from property instead of from setter and make setter optional
// {@link https://forge.typo3.org/issues/100136}
$methodName = 'set' . ucfirst($propertyName);
if ($classSchema->hasMethod($methodName)) {
$methodParameters = $classSchema->getMethod($methodName)->getParameters();
$methodParameter = current($methodParameters);
if ($methodParameter->getType() === null) {
throw new InvalidTargetException('Setter for property "' . $propertyName . '" had no type hint or documentation in target object of type "' . $targetType . '".', 1303379158);
}
$property = $classSchema->getProperty($propertyName);
$primaryCollectionValueType = $property->getPrimaryCollectionValueType();
if ($primaryCollectionValueType instanceof Type) {
return $methodParameter->getType() . '<' . ($primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType()) . '>';
}
return $methodParameter->getType();
}
try {
$parameterType = $classSchema->getMethod('__construct')->getParameter($propertyName)->getType();
} catch (NoSuchMethodException $e) {
$exceptionMessage = sprintf('Type of child property "%s" of class "%s" could not be '
. 'derived from constructor arguments as said class does not have a constructor '
. 'defined.', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1582385098);
} catch (NoSuchMethodParameterException $e) {
$exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
. 'derived from constructor arguments as the constructor of said class does not '
. 'have a parameter with property name "%1$s".', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1303379126);
}
if ($parameterType === null) {
$exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
. 'derived from constructor argument "%1$s". This usually happens if the argument '
. 'misses a type hint.', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1582385619);
}
return $parameterType;
}
/**
* Convert an object from $source to an object.
*
* @param mixed $source
* @return object|null the target type
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom($source, string $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null): ?object
{
$object = $this->buildObject($convertedChildProperties, $targetType);
foreach ($convertedChildProperties as $propertyName => $propertyValue) {
$result = ObjectAccess::setProperty($object, $propertyName, $propertyValue);
if ($result === false) {
$exceptionMessage = sprintf(
'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.',
$propertyName,
get_debug_type($propertyValue),
$targetType
);
throw new InvalidTargetException($exceptionMessage, 1304538165);
}
}
return $object;
}
/**
* Determines the target type based on the source's (optional) __type key and by evaluating possible
* XCLASS overrides of the target type.
*
* @param mixed $source
* @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException
* @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException
* @throws \InvalidArgumentException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getTargetTypeForSource($source, string $originalTargetType, PropertyMappingConfigurationInterface $configuration = null): string
{
$targetType = $originalTargetType;
if (is_array($source) && array_key_exists('__type', $source)) {
$targetType = $source['__type'];
if ($configuration === null) {
// 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.
throw new \InvalidArgumentException('A property mapping configuration must be given, not NULL.', 1326277369);
}
if ($configuration->getConfigurationValue(\TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED) !== true) {
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);
}
if ($targetType !== $originalTargetType && is_a($targetType, $originalTargetType, true) === false) {
throw new InvalidDataTypeException('The given type "' . $targetType . '" is not a subtype of "' . $originalTargetType . '".', 1317048056);
}
}
// Respect XCLASSed object target type
return GeneralUtility::getClassName($targetType);
}
/**
* Builds a new instance of $objectType with the given $possibleConstructorArgumentValues. If
* constructor argument values are missing from the given array the method looks for a default
* value in the constructor signature. Furthermore, the constructor arguments are removed from
* $possibleConstructorArgumentValues: They are considered "handled" by __construct and will
* not be mapped calling setters later.
*
* @return object The created instance
* @throws InvalidTargetException if a required constructor argument is missing
*/
protected function buildObject(array &$possibleConstructorArgumentValues, string $objectType): object
{
// The ObjectConverter typically kicks in, if request arguments are to be mapped to
// a domain model. An example is ext:belog:Domain/Model/Demand.
// Domain models are data objects and should thus be fetched via makeInstance(), should
// not be registered as service, and should thus not be DI aware.
// Additionally, all to-be-mapped arguments are hand over as "possible constructor arguments" here,
// and extbase is able to use single arguments as constructor arguments to domain models,
// if a __construct() with an argument having the same name as a to-be-mapped argument exists.
// This is the reason that &$possibleConstructorArgumentValues is hand over as reference here:
// If an argument can be hand over as constructor argument, it is considered "already mapped" and
// is not manually mapped calling setters later.
// To be as backwards compatible as possible, the following logic is applied:
// * If the class is registered as service (container->has()=true), and if there are no
// $possibleConstructorArgumentValues, instantiate the class via container->get(). Easy
// scenario - the target class is DI aware and will get dependencies injected. A different target
// class can be specified using service configuration if needed.
// * If the class is registered as service, and if there are $possibleConstructorArgumentValues,
// the class is instantiated via container->get(). $possibleConstructorArgumentValues are *not* hand
// over to the constructor. The target class can then use constructor injection and inject* methods
// for DI. A different target class can be specified using service configuration if needed. Mapping
// of arguments is done using setters by follow-up code.
// * If the class is *not* registered as service, makeInstance() is used for object retrieval.
// * If there are no $possibleConstructorArgumentValues, makeInstance() is used right away.
// * If there are $possibleConstructorArgumentValues and __construct() does not exist, makeInstance()
// is used without constructor arguments. Mapping of argument values via setters is done by follow-up code.
// * If there are $possibleConstructorArgumentValues and if __construct() exists, extbase reflection
// is used to map single arguments to constructor arguments with the same name and
// makeInstance() is used to instantiate the class. Mapping remaining arguments is done by follow-up code.
if ($this->container->has($objectType)) {
// @todo: consider dropping container->get() to prevent domain models being treated as services in >=v12.
return $this->container->get($objectType);
}
if (empty($possibleConstructorArgumentValues) || !method_exists($objectType, '__construct')) {
return GeneralUtility::makeInstance($objectType);
}
$classSchema = $this->reflectionService->getClassSchema($objectType);
$constructor = $classSchema->getMethod('__construct');
$constructorArguments = [];
foreach ($constructor->getParameters() as $parameterName => $parameter) {
if (array_key_exists($parameterName, $possibleConstructorArgumentValues)) {
$constructorArguments[] = $possibleConstructorArgumentValues[$parameterName];
unset($possibleConstructorArgumentValues[$parameterName]);
} elseif ($parameter->isOptional()) {
$constructorArguments[] = $parameter->getDefaultValue();
} else {
throw new InvalidTargetException('Missing constructor argument "' . $parameterName . '" for object of type "' . $objectType . '".', 1268734872);
}
}
return GeneralUtility::makeInstance(...[$objectType, ...$constructorArguments]);
}
}