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