1<?php 2namespace TYPO3\CMS\Extbase\Property; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException; 18use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter; 19use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility; 20 21/** 22 * The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types. 23 * It is used most prominently to map incoming HTTP arguments to objects. 24 */ 25class PropertyMapper implements \TYPO3\CMS\Core\SingletonInterface 26{ 27 /** 28 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface 29 */ 30 protected $objectManager; 31 32 /** 33 * @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder 34 */ 35 protected $configurationBuilder; 36 37 /** 38 * A multi-dimensional array which stores the Type Converters available in the system. 39 * It has the following structure: 40 * 1. Dimension: Source Type 41 * 2. Dimension: Target Type 42 * 3. Dimension: Priority 43 * Value: Type Converter instance 44 * 45 * @var array 46 */ 47 protected $typeConverters = []; 48 49 /** 50 * A list of property mapping messages (errors, warnings) which have occurred on last mapping. 51 * 52 * @var \TYPO3\CMS\Extbase\Error\Result 53 */ 54 protected $messages; 55 56 /** 57 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager 58 * @internal only to be used within Extbase, not part of TYPO3 Core API. 59 */ 60 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager) 61 { 62 $this->objectManager = $objectManager; 63 } 64 65 /** 66 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder $configurationBuilder 67 * @internal only to be used within Extbase, not part of TYPO3 Core API. 68 */ 69 public function injectConfigurationBuilder(\TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder $configurationBuilder) 70 { 71 $this->configurationBuilder = $configurationBuilder; 72 } 73 74 /** 75 * Lifecycle method, called after all dependencies have been injected. 76 * Here, the typeConverter array gets initialized. 77 * 78 * @throws Exception\DuplicateTypeConverterException 79 * @internal only to be used within Extbase, not part of TYPO3 Core API. 80 */ 81 public function initializeObject() 82 { 83 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'] as $typeConverterClassName) { 84 $typeConverter = $this->objectManager->get($typeConverterClassName); 85 foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) { 86 if (isset($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()])) { 87 throw new Exception\DuplicateTypeConverterException('There exist at least two converters which handle the conversion from "' . $supportedSourceType . '" to "' . $typeConverter->getSupportedTargetType() . '" with priority "' . $typeConverter->getPriority() . '": ' . get_class($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()]) . ' and ' . get_class($typeConverter), 1297951378); 88 } 89 $this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()] = $typeConverter; 90 } 91 } 92 } 93 94 /** 95 * Map $source to $targetType, and return the result 96 * 97 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed! 98 * @param string $targetType The type of the target; can be either a class name or a simple type. 99 * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration. 100 * @throws Exception 101 * @return mixed an instance of $targetType 102 */ 103 public function convert($source, $targetType, PropertyMappingConfigurationInterface $configuration = null) 104 { 105 if ($configuration === null) { 106 $configuration = $this->configurationBuilder->build(); 107 } 108 $currentPropertyPath = []; 109 $this->messages = new \TYPO3\CMS\Extbase\Error\Result(); 110 try { 111 $result = $this->doMapping($source, $targetType, $configuration, $currentPropertyPath); 112 if ($result instanceof \TYPO3\CMS\Extbase\Error\Error) { 113 return null; 114 } 115 116 return $result; 117 } catch (TargetNotFoundException $e) { 118 throw $e; 119 } catch (\Exception $e) { 120 throw new Exception('Exception while property mapping at property path "' . implode('.', $currentPropertyPath) . '": ' . $e->getMessage(), 1297759968, $e); 121 } 122 } 123 124 /** 125 * Get the messages of the last Property Mapping 126 * 127 * @return \TYPO3\CMS\Extbase\Error\Result 128 */ 129 public function getMessages() 130 { 131 return $this->messages; 132 } 133 134 /** 135 * Internal function which actually does the property mapping. 136 * 137 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed! 138 * @param string $targetType The type of the target; can be either a class name or a simple type. 139 * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. 140 * @param array &$currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown. 141 * @throws Exception\TypeConverterException 142 * @throws Exception\InvalidPropertyMappingConfigurationException 143 * @return mixed an instance of $targetType 144 */ 145 protected function doMapping($source, $targetType, PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath) 146 { 147 if (is_object($source)) { 148 $targetType = $this->parseCompositeType($targetType); 149 if ($source instanceof $targetType) { 150 return $source; 151 } 152 } 153 154 if ($source === null) { 155 $source = ''; 156 } 157 158 $typeConverter = $this->findTypeConverter($source, $targetType, $configuration); 159 $targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration); 160 161 if (!is_object($typeConverter) || !$typeConverter instanceof \TYPO3\CMS\Extbase\Property\TypeConverterInterface) { 162 throw new Exception\TypeConverterException( 163 'Type converter for "' . $source . '" -> "' . $targetType . '" not found.', 164 1476045062 165 ); 166 } 167 168 $convertedChildProperties = []; 169 foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) { 170 $targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName); 171 if ($configuration->shouldSkip($targetPropertyName)) { 172 continue; 173 } 174 175 if (!$configuration->shouldMap($targetPropertyName)) { 176 if ($configuration->shouldSkipUnknownProperties()) { 177 continue; 178 } 179 throw new Exception\InvalidPropertyMappingConfigurationException('It is not allowed to map property "' . $targetPropertyName . '". You need to use $propertyMappingConfiguration->allowProperties(\'' . $targetPropertyName . '\') to enable mapping of this property.', 1355155913); 180 } 181 182 $targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $targetPropertyName, $configuration); 183 184 $subConfiguration = $configuration->getConfigurationFor($targetPropertyName); 185 186 $currentPropertyPath[] = $targetPropertyName; 187 $targetPropertyValue = $this->doMapping($sourcePropertyValue, $targetPropertyType, $subConfiguration, $currentPropertyPath); 188 array_pop($currentPropertyPath); 189 if (!($targetPropertyValue instanceof \TYPO3\CMS\Extbase\Error\Error)) { 190 $convertedChildProperties[$targetPropertyName] = $targetPropertyValue; 191 } 192 } 193 $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration); 194 195 if ($result instanceof \TYPO3\CMS\Extbase\Error\Error) { 196 $this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result); 197 } 198 199 return $result; 200 } 201 202 /** 203 * Determine the type converter to be used. If no converter has been found, an exception is raised. 204 * 205 * @param mixed $source 206 * @param string $targetType 207 * @param PropertyMappingConfigurationInterface $configuration 208 * @throws Exception\TypeConverterException 209 * @throws Exception\InvalidTargetException 210 * @return \TYPO3\CMS\Extbase\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType. 211 */ 212 protected function findTypeConverter($source, $targetType, PropertyMappingConfigurationInterface $configuration) 213 { 214 if ($configuration->getTypeConverter() !== null) { 215 return $configuration->getTypeConverter(); 216 } 217 218 $sourceType = $this->determineSourceType($source); 219 220 if (!is_string($targetType)) { 221 throw new Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727); 222 } 223 224 $targetType = $this->parseCompositeType($targetType); 225 // This is needed to correctly convert old class names to new ones 226 // This compatibility layer will be removed with 7.0 227 $targetType = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($targetType); 228 229 $targetType = TypeHandlingUtility::normalizeType($targetType); 230 231 $converter = null; 232 233 if (TypeHandlingUtility::isSimpleType($targetType)) { 234 if (isset($this->typeConverters[$sourceType][$targetType])) { 235 $converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType], $source, $targetType); 236 } 237 } else { 238 $converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetType); 239 } 240 241 if ($converter === null) { 242 throw new Exception\TypeConverterException( 243 'No converter found which can be used to convert from "' . $sourceType . '" to "' . $targetType . '".', 244 1476044883 245 ); 246 } 247 248 return $converter; 249 } 250 251 /** 252 * Tries to find a suitable type converter for the given source and target type. 253 * 254 * @param string $source The actual source value 255 * @param string $sourceType Type of the source to convert from 256 * @param string $targetClass Name of the target class to find a type converter for 257 * @return mixed Either the matching object converter or NULL 258 * @throws Exception\InvalidTargetException 259 */ 260 protected function findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetClass) 261 { 262 if (!class_exists($targetClass) && !interface_exists($targetClass)) { 263 throw new Exception\InvalidTargetException('Could not find a suitable type converter for "' . $targetClass . '" because no such class or interface exists.', 1297948764); 264 } 265 266 if (!isset($this->typeConverters[$sourceType])) { 267 return null; 268 } 269 270 $convertersForSource = $this->typeConverters[$sourceType]; 271 if (isset($convertersForSource[$targetClass])) { 272 $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass], $source, $targetClass); 273 if ($converter !== null) { 274 return $converter; 275 } 276 } 277 278 foreach (class_parents($targetClass) as $parentClass) { 279 if (!isset($convertersForSource[$parentClass])) { 280 continue; 281 } 282 283 $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$parentClass], $source, $targetClass); 284 if ($converter !== null) { 285 return $converter; 286 } 287 } 288 289 $converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass)); 290 $converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetClass); 291 292 if ($converter !== null) { 293 return $converter; 294 } 295 if (isset($convertersForSource['object'])) { 296 return $this->findEligibleConverterWithHighestPriority($convertersForSource['object'], $source, $targetClass); 297 } 298 return null; 299 } 300 301 /** 302 * @param mixed $converters 303 * @param mixed $source 304 * @param string $targetType 305 * @return mixed Either the matching object converter or NULL 306 */ 307 protected function findEligibleConverterWithHighestPriority($converters, $source, $targetType) 308 { 309 if (!is_array($converters)) { 310 return null; 311 } 312 krsort($converters, SORT_NUMERIC); 313 reset($converters); 314 /** @var AbstractTypeConverter $converter */ 315 foreach ($converters as $converter) { 316 if ($converter->canConvertFrom($source, $targetType)) { 317 return $converter; 318 } 319 } 320 return null; 321 } 322 323 /** 324 * @param array $convertersForSource 325 * @param array $interfaceNames 326 * @return array 327 * @throws Exception\DuplicateTypeConverterException 328 */ 329 protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames) 330 { 331 $convertersForInterface = []; 332 foreach ($interfaceNames as $implementedInterface) { 333 if (isset($convertersForSource[$implementedInterface])) { 334 foreach ($convertersForSource[$implementedInterface] as $priority => $converter) { 335 if (isset($convertersForInterface[$priority])) { 336 throw new Exception\DuplicateTypeConverterException('There exist at least two converters which handle the conversion to an interface with priority "' . $priority . '". ' . get_class($convertersForInterface[$priority]) . ' and ' . get_class($converter), 1297951338); 337 } 338 $convertersForInterface[$priority] = $converter; 339 } 340 } 341 } 342 return $convertersForInterface; 343 } 344 345 /** 346 * Determine the type of the source data, or throw an exception if source was an unsupported format. 347 * 348 * @param mixed $source 349 * @throws Exception\InvalidSourceException 350 * @return string the type of $source 351 */ 352 protected function determineSourceType($source) 353 { 354 if (is_string($source)) { 355 return 'string'; 356 } 357 if (is_array($source)) { 358 return 'array'; 359 } 360 if (is_float($source)) { 361 return 'float'; 362 } 363 if (is_int($source)) { 364 return 'integer'; 365 } 366 if (is_bool($source)) { 367 return 'boolean'; 368 } 369 throw new Exception\InvalidSourceException('The source is not of type string, array, float, integer or boolean, but of type "' . gettype($source) . '"', 1297773150); 370 } 371 372 /** 373 * Parse a composite type like \Foo\Collection<\Bar\Entity> into 374 * \Foo\Collection 375 * 376 * @param string $compositeType 377 * @return string 378 * @internal only to be used within Extbase, not part of TYPO3 Core API. 379 */ 380 public function parseCompositeType($compositeType) 381 { 382 if (strpos($compositeType, '<') !== false) { 383 $compositeType = substr($compositeType, 0, strpos($compositeType, '<')); 384 } 385 return $compositeType; 386 } 387} 388