Bug #37257 » PropertyMapper.php

Carsten Bleicker, 2012-05-24 18:20

 
1
<?php
2
namespace TYPO3\FLOW3\Property;
3

    
4
/*                                                                        *
5
 * This script belongs to the FLOW3 framework.                            *
6
 *                                                                        *
7
 * It is free software; you can redistribute it and/or modify it under    *
8
 * the terms of the GNU Lesser General Public License, either version 3   *
9
 * of the License, or (at your option) any later version.                 *
10
 *                                                                        *
11
 * The TYPO3 project - inspiring people to share!                         *
12
 *                                                                        */
13

    
14
use TYPO3\FLOW3\Annotations as FLOW3;
15

    
16
/**
17
 * The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types.
18
 * It is used most prominently to map incoming HTTP arguments to objects.
19
 *
20
 * @FLOW3\Scope("singleton")
21
 * @api
22
 */
23
class PropertyMapper {
24

    
25
	/**
26
	 * @var \TYPO3\FLOW3\Object\ObjectManagerInterface
27
	 */
28
	protected $objectManager;
29

    
30
	/**
31
	 * @var \TYPO3\FLOW3\Reflection\ReflectionService
32
	 */
33
	protected $reflectionService;
34

    
35
	/**
36
	 * @var \TYPO3\FLOW3\Property\PropertyMappingConfigurationBuilder
37
	 */
38
	protected $configurationBuilder;
39

    
40
	/**
41
	 * A multi-dimensional array which stores the Type Converters available in the system.
42
	 * It has the following structure:
43
	 * 1. Dimension: Source Type
44
	 * 2. Dimension: Target Type
45
	 * 3. Dimension: Priority
46
	 * Value: Type Converter instance
47
	 *
48
	 * @var array
49
	 */
50
	protected $typeConverters = array();
51

    
52
	/**
53
	 * A list of property mapping messages (errors, warnings) which have occured on last mapping.
54
	 * @var \TYPO3\FLOW3\Error\Result
55
	 */
56
	protected $messages;
57

    
58
	/**
59
	 * @param \TYPO3\FLOW3\Object\ObjectManagerInterface $objectManager
60
	 * @return void
61
	 */
62
	public function injectObjectManager(\TYPO3\FLOW3\Object\ObjectManagerInterface $objectManager) {
63
		$this->objectManager = $objectManager;
64
	}
65

    
66
	/**
67
	 * @param \TYPO3\FLOW3\Reflection\ReflectionService $reflectionService
68
	 * @return void
69
	 */
70
	public function injectReflectionService(\TYPO3\FLOW3\Reflection\ReflectionService $reflectionService) {
71
		$this->reflectionService = $reflectionService;
72
	}
73

    
74
	/**
75
	 * @param \TYPO3\FLOW3\Property\PropertyMappingConfigurationBuilder $propertyMappingConfigurationBuilder
76
	 * @return void
77
	 */
78
	public function injectPropertyMappingConfigurationBuilder(\TYPO3\FLOW3\Property\PropertyMappingConfigurationBuilder $propertyMappingConfigurationBuilder) {
79
		$this->configurationBuilder = $propertyMappingConfigurationBuilder;
80
	}
81

    
82
	/**
83
	 * Lifecycle method, called after all dependencies have been injected.
84
	 * Here, the typeConverter array gets initialized.
85
	 *
86
	 * @return void
87
	 * @throws \TYPO3\FLOW3\Property\Exception\DuplicateTypeConverterException
88
	 */
89
	public function initializeObject() {
90
		foreach($this->reflectionService->getAllImplementationClassNamesForInterface('TYPO3\FLOW3\Property\TypeConverterInterface') as $typeConverterClassName) {
91
			$typeConverter = $this->objectManager->get($typeConverterClassName);
92
			foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) {
93
				if (isset($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()])) {
94
					throw new \TYPO3\FLOW3\Property\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);
95
				}
96
				$this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()] = $typeConverter;
97
			}
98
		}
99
	}
100

    
101
	/**
102
	 * Map $source to $targetType, and return the result
103
	 *
104
	 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
105
	 * @param string $targetType The type of the target; can be either a class name or a simple type.
106
	 * @param \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration.
107
	 * @return mixed an instance of $targetType
108
	 * @throws \TYPO3\FLOW3\Property\Exception
109
	 * @api
110
	 */
111
	public function convert($source, $targetType, \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration = NULL) {
112
		if ($configuration === NULL) {
113
			$configuration = $this->configurationBuilder->build();
114
		}
115

    
116
		$currentPropertyPath = array();
117
		$this->messages = new \TYPO3\FLOW3\Error\Result();
118
		try {
119
			return $this->doMapping($source, $targetType, $configuration, $currentPropertyPath);
120
		} catch (\Exception $e) {
121
			throw new \TYPO3\FLOW3\Property\Exception('Exception while property mapping for target type "' . $targetType . '", at property path "' . implode('.', $currentPropertyPath) . '": ' . $e->getMessage(), 1297759968, $e);
122
		}
123
	}
124

    
125
	/**
126
	 * Get the messages of the last Property Mapping
127
	 *
128
	 * @return \TYPO3\FLOW3\Error\Result
129
	 * @api
130
	 */
131
	public function getMessages() {
132
		return $this->messages;
133
	}
134

    
135
	/**
136
	 * Internal function which actually does the property mapping.
137
	 *
138
	 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
139
	 * @param string $targetType The type of the target; can be either a class name or a simple type.
140
	 * @param \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration Configuration for the property mapping.
141
	 * @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown.
142
	 * @return mixed an instance of $targetType
143
	 * @throws \TYPO3\FLOW3\Property\Exception\TypeConverterException
144
	 */
145
	protected function doMapping($source, $targetType, \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath) {
146
		if ($source === NULL) {
147
			$source = '';
148
		}
149

    
150
		$typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
151

    
152
		if (!is_object($typeConverter) || !($typeConverter instanceof \TYPO3\FLOW3\Property\TypeConverterInterface)) {
153
			throw new \TYPO3\FLOW3\Property\Exception\TypeConverterException('Type converter for "' . $source . '" -> "' . $targetType . '" not found.');
154
		}
155

    
156
		$convertedChildProperties = array();
157
		foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) {
158
			$targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName);
159
			if (!$configuration->shouldMap($targetPropertyName)) {
160
				throw new \TYPO3\FLOW3\Property\Exception\InvalidPropertyMappingConfigurationException('It is not allowed to map property "' . $targetPropertyName . '". You need to use $propertyMappingConfiguration->allowProperties(\'' . $targetPropertyName . '\') to enable mapping of this property.', 1335969887);
161
			}
162

    
163
			$targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $targetPropertyName, $configuration);
164

    
165
			$subConfiguration = $configuration->getConfigurationFor($targetPropertyName);
166

    
167
			$currentPropertyPath[] = $targetPropertyName;
168
			$targetPropertyValue = $this->doMapping($sourcePropertyValue, $targetPropertyType, $subConfiguration, $currentPropertyPath);
169
			
170
			array_pop($currentPropertyPath);
171
			
172
				// Get Column Annotation
173
			$propertyAnnotations = $this->reflectionService->getPropertyAnnotation($targetType, $targetPropertyName, 'Doctrine\ORM\Mapping\Column');
174
			
175
				// Allow NULL Decision
176
			if($propertyAnnotations instanceof \Doctrine\ORM\Mapping\Column)
177
				$allowNull = $propertyAnnotations->nullable;
178
			else
179
				$allowNull = FALSE;
180
			
181
			if ( $targetPropertyValue !== NULL || $allowNull === TRUE ) {
182
				$convertedChildProperties[$targetPropertyName] = $targetPropertyValue;
183
			}
184
			
185
		}
186
		$result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration);
187

    
188
		if ($result instanceof \TYPO3\FLOW3\Error\Error) {
189
			$this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result);
190
			$result = NULL;
191
		}
192

    
193
		return $result;
194
	}
195

    
196
	/**
197
	 * Determine the type converter to be used. If no converter has been found, an exception is raised.
198
	 *
199
	 * @param mixed $source
200
	 * @param string $targetType
201
	 * @param \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration
202
	 * @return \TYPO3\FLOW3\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType.
203
	 * @throws \TYPO3\FLOW3\Property\Exception\TypeConverterException
204
	 * @throws \TYPO3\FLOW3\Property\Exception\InvalidTargetException
205
	 */
206
	protected function findTypeConverter($source, $targetType, \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration) {
207
		if ($configuration->getTypeConverter() !== NULL) return $configuration->getTypeConverter();
208

    
209
		$sourceType = $this->determineSourceType($source);
210

    
211
		if (!is_string($targetType)) {
212
			throw new \TYPO3\FLOW3\Property\Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727);
213
		}
214
		if (strpos($targetType, '<') !== FALSE) {
215
			$targetType = substr($targetType, 0, strpos($targetType, '<'));
216
		}
217
		$converter = NULL;
218

    
219
		if (\TYPO3\FLOW3\Utility\TypeHandling::isSimpleType($targetType)) {
220
			if (isset($this->typeConverters[$sourceType][$targetType])) {
221
				$converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType], $source, $targetType);
222
			}
223
		} else {
224
			$converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetType);
225
		}
226

    
227
		if ($converter === NULL) {
228
			throw new \TYPO3\FLOW3\Property\Exception\TypeConverterException('No converter found which can be used to convert from "' . $sourceType . '" to "' . $targetType . '".');
229
		}
230

    
231
		return $converter;
232
	}
233

    
234
	/**
235
	 * Tries to find a suitable type converter for the given source and target type.
236
	 *
237
	 * @param string $source The actual source value
238
	 * @param string $sourceType Type of the source to convert from
239
	 * @param string $targetClass Name of the target class to find a type converter for
240
	 * @return mixed Either the matching object converter or NULL
241
	 * @throws \TYPO3\FLOW3\Property\Exception\InvalidTargetException
242
	 */
243
	protected function findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetClass) {
244
		if (!class_exists($targetClass) && !interface_exists($targetClass)) {
245
			throw new \TYPO3\FLOW3\Property\Exception\InvalidTargetException('Could not find a suitable type converter for "' . $targetClass . '" because no such class or interface exists.', 1297948764);
246
		}
247

    
248
		if (!isset($this->typeConverters[$sourceType])) {
249
			return NULL;
250
		}
251

    
252
		$convertersForSource = $this->typeConverters[$sourceType];
253
		if (isset($convertersForSource[$targetClass])) {
254
			$converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass], $source, $targetClass);
255
			if ($converter !== NULL) {
256
				return $converter;
257
			}
258
		}
259

    
260
		foreach (class_parents($targetClass) as $parentClass) {
261
			if (!isset($convertersForSource[$parentClass])) continue;
262

    
263
			$converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$parentClass], $source, $targetClass);
264
			if ($converter !== NULL) {
265
				return $converter;
266
			}
267
		}
268

    
269
		$converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass));
270
		$converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetClass);
271

    
272
		if ($converter !== NULL) {
273
			return $converter;
274
		}
275
		if (isset($convertersForSource['object'])) {
276
			return $this->findEligibleConverterWithHighestPriority($convertersForSource['object'], $source, $targetClass);
277
		} else {
278
			return NULL;
279
		}
280
	}
281

    
282
	/**
283
	 * @param mixed $converters
284
	 * @param mixed $source
285
	 * @param string $targetType
286
	 * @return mixed Either the matching object converter or NULL
287
	 */
288
	protected function findEligibleConverterWithHighestPriority($converters, $source, $targetType) {
289
		if (!is_array($converters)) return NULL;
290
		krsort($converters);
291
		reset($converters);
292
		foreach ($converters as $converter) {
293
			if ($converter->canConvertFrom($source, $targetType)) {
294
				return $converter;
295
			}
296
		}
297
		return NULL;
298
	}
299

    
300
	/**
301
	 * @param array $convertersForSource
302
	 * @param array $interfaceNames
303
	 * @return array
304
	 * @throws \TYPO3\FLOW3\Property\Exception\DuplicateTypeConverterException
305
	 */
306
	protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames) {
307
		$convertersForInterface = array();
308
		foreach ($interfaceNames as $implementedInterface) {
309
			if (isset($convertersForSource[$implementedInterface])) {
310
				foreach ($convertersForSource[$implementedInterface] as $priority => $converter) {
311
					if (isset($convertersForInterface[$priority])) {
312
						throw new \TYPO3\FLOW3\Property\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);
313
					}
314
					$convertersForInterface[$priority] = $converter;
315
				}
316
			}
317
		}
318
		return $convertersForInterface;
319
	}
320

    
321
	/**
322
	 * Determine the type of the source data, or throw an exception if source was an unsupported format.
323
	 *
324
	 * @param mixed $source
325
	 * @return string the type of $source
326
	 * @throws \TYPO3\FLOW3\Property\Exception\InvalidSourceException
327
	 */
328
	protected function determineSourceType($source) {
329
		if (is_string($source)) {
330
			return 'string';
331
		} elseif (is_array($source)) {
332
			return 'array';
333
		} elseif (is_float($source)) {
334
			return 'float';
335
		} elseif (is_integer($source)) {
336
			return 'integer';
337
		} elseif (is_bool($source)) {
338
			return 'boolean';
339
		} else {
340
			throw new \TYPO3\FLOW3\Property\Exception\InvalidSourceException('The source is not of type string, array, float, integer or boolean, but of type "' . gettype($source) . '"', 1297773150);
341
		}
342
	}
343
}
344
?>
(3-3/3)