Bug #37257 » PropertyMapper.php

Carsten Bleicker, 2012-05-24 17:23

 
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
			array_pop($currentPropertyPath);
170
			
171
				// THIS IF SHOULD CHECK BY CLASS REFLECTION FOR @ORM\Column(nullable=true)
172
				// If this returns true, also NULL Should be added to convertedChildProperties
173
			if ($targetPropertyValue !== NULL) {
174
				$convertedChildProperties[$targetPropertyName] = $targetPropertyValue;
175
			}
176
		}
177
		$result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration);
178

    
179
		if ($result instanceof \TYPO3\FLOW3\Error\Error) {
180
			$this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result);
181
			$result = NULL;
182
		}
183

    
184
		return $result;
185
	}
186

    
187
	/**
188
	 * Determine the type converter to be used. If no converter has been found, an exception is raised.
189
	 *
190
	 * @param mixed $source
191
	 * @param string $targetType
192
	 * @param \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration
193
	 * @return \TYPO3\FLOW3\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType.
194
	 * @throws \TYPO3\FLOW3\Property\Exception\TypeConverterException
195
	 * @throws \TYPO3\FLOW3\Property\Exception\InvalidTargetException
196
	 */
197
	protected function findTypeConverter($source, $targetType, \TYPO3\FLOW3\Property\PropertyMappingConfigurationInterface $configuration) {
198
		if ($configuration->getTypeConverter() !== NULL) return $configuration->getTypeConverter();
199

    
200
		$sourceType = $this->determineSourceType($source);
201

    
202
		if (!is_string($targetType)) {
203
			throw new \TYPO3\FLOW3\Property\Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727);
204
		}
205
		if (strpos($targetType, '<') !== FALSE) {
206
			$targetType = substr($targetType, 0, strpos($targetType, '<'));
207
		}
208
		$converter = NULL;
209

    
210
		if (\TYPO3\FLOW3\Utility\TypeHandling::isSimpleType($targetType)) {
211
			if (isset($this->typeConverters[$sourceType][$targetType])) {
212
				$converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType], $source, $targetType);
213
			}
214
		} else {
215
			$converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetType);
216
		}
217

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

    
222
		return $converter;
223
	}
224

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

    
239
		if (!isset($this->typeConverters[$sourceType])) {
240
			return NULL;
241
		}
242

    
243
		$convertersForSource = $this->typeConverters[$sourceType];
244
		if (isset($convertersForSource[$targetClass])) {
245
			$converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass], $source, $targetClass);
246
			if ($converter !== NULL) {
247
				return $converter;
248
			}
249
		}
250

    
251
		foreach (class_parents($targetClass) as $parentClass) {
252
			if (!isset($convertersForSource[$parentClass])) continue;
253

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

    
260
		$converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass));
261
		$converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetClass);
262

    
263
		if ($converter !== NULL) {
264
			return $converter;
265
		}
266
		if (isset($convertersForSource['object'])) {
267
			return $this->findEligibleConverterWithHighestPriority($convertersForSource['object'], $source, $targetClass);
268
		} else {
269
			return NULL;
270
		}
271
	}
272

    
273
	/**
274
	 * @param mixed $converters
275
	 * @param mixed $source
276
	 * @param string $targetType
277
	 * @return mixed Either the matching object converter or NULL
278
	 */
279
	protected function findEligibleConverterWithHighestPriority($converters, $source, $targetType) {
280
		if (!is_array($converters)) return NULL;
281
		krsort($converters);
282
		reset($converters);
283
		foreach ($converters as $converter) {
284
			if ($converter->canConvertFrom($source, $targetType)) {
285
				return $converter;
286
			}
287
		}
288
		return NULL;
289
	}
290

    
291
	/**
292
	 * @param array $convertersForSource
293
	 * @param array $interfaceNames
294
	 * @return array
295
	 * @throws \TYPO3\FLOW3\Property\Exception\DuplicateTypeConverterException
296
	 */
297
	protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames) {
298
		$convertersForInterface = array();
299
		foreach ($interfaceNames as $implementedInterface) {
300
			if (isset($convertersForSource[$implementedInterface])) {
301
				foreach ($convertersForSource[$implementedInterface] as $priority => $converter) {
302
					if (isset($convertersForInterface[$priority])) {
303
						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);
304
					}
305
					$convertersForInterface[$priority] = $converter;
306
				}
307
			}
308
		}
309
		return $convertersForInterface;
310
	}
311

    
312
	/**
313
	 * Determine the type of the source data, or throw an exception if source was an unsupported format.
314
	 *
315
	 * @param mixed $source
316
	 * @return string the type of $source
317
	 * @throws \TYPO3\FLOW3\Property\Exception\InvalidSourceException
318
	 */
319
	protected function determineSourceType($source) {
320
		if (is_string($source)) {
321
			return 'string';
322
		} elseif (is_array($source)) {
323
			return 'array';
324
		} elseif (is_float($source)) {
325
			return 'float';
326
		} elseif (is_integer($source)) {
327
			return 'integer';
328
		} elseif (is_bool($source)) {
329
			return 'boolean';
330
		} else {
331
			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);
332
		}
333
	}
334
}
335
?>
(2-2/3)