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