‪TYPO3CMS  ‪main
ObjectConverter.php
Go to the documentation of this file.
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 
19 
20 use Psr\Container\ContainerInterface;
21 use Symfony\Component\PropertyInfo\Type;
31 
36 {
40  public const ‪CONFIGURATION_TARGET_TYPE = 3;
41 
46 
47  protected ContainerInterface ‪$container;
48 
52  protected ‪$reflectionService;
53 
55  {
56  $this->reflectionService = ‪$reflectionService;
57  }
58 
59  public function ‪injectContainer(ContainerInterface ‪$container)
60  {
61  $this->container = ‪$container;
62  }
63 
70  public function ‪getSourceChildPropertiesToBeConverted($source): array
71  {
72  if (isset($source['__type'])) {
73  unset($source['__type']);
74  }
75  return $source;
76  }
77 
84  public function ‪getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration): string
85  {
86  $configuredTargetType = $configuration->getConfigurationFor($propertyName)->getConfigurationValue(\‪TYPO3\CMS\‪Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_TARGET_TYPE);
87  if ($configuredTargetType !== null) {
88  return $configuredTargetType;
89  }
90 
91  $classSchema = $this->reflectionService->getClassSchema($targetType);
92 
93  // @todo: infer property type from property instead of from setter and make setter optional
94  // {@link https://forge.typo3.org/issues/100136}
95 
96  $methodName = 'set' . ucfirst($propertyName);
97  if ($classSchema->hasMethod($methodName)) {
98  $methodParameters = $classSchema->getMethod($methodName)->getParameters();
99  $methodParameter = current($methodParameters);
100  if ($methodParameter->getType() === null) {
101  throw new InvalidTargetException('Setter for property "' . $propertyName . '" had no type hint or documentation in target object of type "' . $targetType . '".', 1303379158);
102  }
103  $property = $classSchema->getProperty($propertyName);
104  $primaryCollectionValueType = $property->getPrimaryCollectionValueType();
105  if ($primaryCollectionValueType instanceof Type) {
106  return $methodParameter->getType() . '<' . ($primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType()) . '>';
107  }
108  return $methodParameter->getType();
109  }
110  try {
111  $parameterType = $classSchema->getMethod('__construct')->getParameter($propertyName)->getType();
112  } catch (NoSuchMethodException $e) {
113  $exceptionMessage = sprintf('Type of child property "%s" of class "%s" could not be '
114  . 'derived from constructor arguments as said class does not have a constructor '
115  . 'defined.', $propertyName, $targetType);
116  throw new InvalidTargetException($exceptionMessage, 1582385098);
117  } catch (NoSuchMethodParameterException $e) {
118  $exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
119  . 'derived from constructor arguments as the constructor of said class does not '
120  . 'have a parameter with property name "%1$s".', $propertyName, $targetType);
121  throw new InvalidTargetException($exceptionMessage, 1303379126);
122  }
123 
124  if ($parameterType === null) {
125  $exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
126  . 'derived from constructor argument "%1$s". This usually happens if the argument '
127  . 'misses a type hint.', $propertyName, $targetType);
128  throw new InvalidTargetException($exceptionMessage, 1582385619);
129  }
130  return $parameterType;
131  }
132 
141  public function ‪convertFrom($source, string $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null): ?object
142  {
143  $object = $this->‪buildObject($convertedChildProperties, $targetType);
144  foreach ($convertedChildProperties as $propertyName => $propertyValue) {
145  $result = ‪ObjectAccess::setProperty($object, $propertyName, $propertyValue);
146  if ($result === false) {
147  $exceptionMessage = sprintf(
148  '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.',
149  $propertyName,
150  get_debug_type($propertyValue),
151  $targetType
152  );
153  throw new InvalidTargetException($exceptionMessage, 1304538165);
154  }
155  }
156 
157  return $object;
158  }
159 
170  public function ‪getTargetTypeForSource($source, string $originalTargetType, PropertyMappingConfigurationInterface $configuration = null): string
171  {
172  $targetType = $originalTargetType;
173 
174  if (is_array($source) && array_key_exists('__type', $source)) {
175  $targetType = $source['__type'];
176 
177  if ($configuration === null) {
178  // 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.
179  throw new \InvalidArgumentException('A property mapping configuration must be given, not NULL.', 1326277369);
180  }
181  if ($configuration->getConfigurationValue(\‪TYPO3\CMS\‪Extbase\Property\TypeConverter\ObjectConverter::class, self::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED) !== true) {
182  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);
183  }
184 
185  if ($targetType !== $originalTargetType && is_a($targetType, $originalTargetType, true) === false) {
186  throw new InvalidDataTypeException('The given type "' . $targetType . '" is not a subtype of "' . $originalTargetType . '".', 1317048056);
187  }
188  }
189 
190  // Respect XCLASSed object target type
191  return GeneralUtility::getClassName($targetType);
192  }
193 
204  protected function ‪buildObject(array &$possibleConstructorArgumentValues, string $objectType): object
205  {
206  // The ObjectConverter typically kicks in, if request arguments are to be mapped to
207  // a domain model. An example is ext:belog:Domain/Model/Demand.
208  // Domain models are data objects and should thus be fetched via makeInstance(), should
209  // not be registered as service, and should thus not be DI aware.
210  // Additionally, all to-be-mapped arguments are hand over as "possible constructor arguments" here,
211  // and extbase is able to use single arguments as constructor arguments to domain models,
212  // if a __construct() with an argument having the same name as a to-be-mapped argument exists.
213  // This is the reason that &$possibleConstructorArgumentValues is hand over as reference here:
214  // If an argument can be hand over as constructor argument, it is considered "already mapped" and
215  // is not manually mapped calling setters later.
216  // To be as backwards compatible as possible, the following logic is applied:
217  // * If the class is registered as service (container->has()=true), and if there are no
218  // $possibleConstructorArgumentValues, instantiate the class via container->get(). Easy
219  // scenario - the target class is DI aware and will get dependencies injected. A different target
220  // class can be specified using service configuration if needed.
221  // * If the class is registered as service, and if there are $possibleConstructorArgumentValues,
222  // the class is instantiated via container->get(). $possibleConstructorArgumentValues are *not* hand
223  // over to the constructor. The target class can then use constructor injection and inject* methods
224  // for DI. A different target class can be specified using service configuration if needed. Mapping
225  // of arguments is done using setters by follow-up code.
226  // * If the class is *not* registered as service, makeInstance() is used for object retrieval.
227  // * If there are no $possibleConstructorArgumentValues, makeInstance() is used right away.
228  // * If there are $possibleConstructorArgumentValues and __construct() does not exist, makeInstance()
229  // is used without constructor arguments. Mapping of argument values via setters is done by follow-up code.
230  // * If there are $possibleConstructorArgumentValues and if __construct() exists, extbase reflection
231  // is used to map single arguments to constructor arguments with the same name and
232  // makeInstance() is used to instantiate the class. Mapping remaining arguments is done by follow-up code.
233  if ($this->container->has($objectType)) {
234  // @todo: consider dropping container->get() to prevent domain models being treated as services in >=v12.
235  return $this->container->get($objectType);
236  }
237 
238  if (empty($possibleConstructorArgumentValues) || !method_exists($objectType, '__construct')) {
239  return GeneralUtility::makeInstance($objectType);
240  }
241 
242  $classSchema = $this->reflectionService->getClassSchema($objectType);
243  $constructor = $classSchema->getMethod('__construct');
244  $constructorArguments = [];
245  foreach ($constructor->getParameters() as $parameterName => $parameter) {
246  if (array_key_exists($parameterName, $possibleConstructorArgumentValues)) {
247  $constructorArguments[] = $possibleConstructorArgumentValues[$parameterName];
248  unset($possibleConstructorArgumentValues[$parameterName]);
249  } elseif ($parameter->isOptional()) {
250  $constructorArguments[] = $parameter->getDefaultValue();
251  } else {
252  throw new InvalidTargetException('Missing constructor argument "' . $parameterName . '" for object of type "' . $objectType . '".', 1268734872);
253  }
254  }
255  return GeneralUtility::makeInstance(...[$objectType, ...$constructorArguments]);
256  }
257 }
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\CONFIGURATION_TARGET_TYPE
‪const CONFIGURATION_TARGET_TYPE
Definition: ObjectConverter.php:40
‪TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface\getConfigurationValue
‪mixed getConfigurationValue($typeConverterClassName, $key)
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\buildObject
‪object buildObject(array &$possibleConstructorArgumentValues, string $objectType)
Definition: ObjectConverter.php:203
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\injectReflectionService
‪injectReflectionService(ReflectionService $reflectionService)
Definition: ObjectConverter.php:53
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\$reflectionService
‪TYPO3 CMS Extbase Reflection ReflectionService $reflectionService
Definition: ObjectConverter.php:51
‪TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface\getConfigurationFor
‪TYPO3 CMS Extbase Property PropertyMappingConfigurationInterface getConfigurationFor($propertyName)
‪TYPO3\CMS\Extbase\Annotation
Definition: IgnoreValidation.php:18
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess\setProperty
‪static bool setProperty(object|array &$subject, string $propertyName, mixed $propertyValue)
Definition: ObjectAccess.php:158
‪TYPO3
‪TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException
Definition: NoSuchMethodParameterException.php:24
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\getSourceChildPropertiesToBeConverted
‪getSourceChildPropertiesToBeConverted($source)
Definition: ObjectConverter.php:69
‪TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface
Definition: PropertyMappingConfigurationInterface.php:22
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED
‪const CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED
Definition: ObjectConverter.php:45
‪TYPO3\CMS\Extbase\Reflection\ObjectAccess
Definition: ObjectAccess.php:39
‪TYPO3\CMS\Extbase\Reflection\ReflectionService
Definition: ReflectionService.php:28
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\getTypeOfChildProperty
‪getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration)
Definition: ObjectConverter.php:83
‪TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException
Definition: NoSuchMethodException.php:24
‪TYPO3\CMS\Extbase\Property\TypeConverter
Definition: AbstractFileFolderConverter.php:18
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter
Definition: ObjectConverter.php:36
‪TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter
Definition: AbstractTypeConverter.php:29
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\injectContainer
‪injectContainer(ContainerInterface $container)
Definition: ObjectConverter.php:58
‪TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException
Definition: InvalidPropertyMappingConfigurationException.php:25
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\$container
‪ContainerInterface $container
Definition: ObjectConverter.php:47
‪TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException
Definition: InvalidTargetException.php:25
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\convertFrom
‪object null convertFrom($source, string $targetType, array $convertedChildProperties=[], PropertyMappingConfigurationInterface $configuration=null)
Definition: ObjectConverter.php:140
‪TYPO3\CMS\Extbase\Property\TypeConverter\ObjectConverter\getTargetTypeForSource
‪getTargetTypeForSource($source, string $originalTargetType, PropertyMappingConfigurationInterface $configuration=null)
Definition: ObjectConverter.php:169
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException
Definition: InvalidDataTypeException.php:25