[CLEANUP] Rework/simplify copyright header and remove @package
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Property / PropertyMapper.php
1 <?php
2 namespace 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
17 use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
18
19 /**
20 * The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types.
21 * It is used most prominently to map incoming HTTP arguments to objects.
22 *
23 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
24 * @api
25 */
26 class PropertyMapper implements \TYPO3\CMS\Core\SingletonInterface {
27
28 /**
29 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
30 * @inject
31 */
32 protected $objectManager;
33
34 /**
35 * @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder
36 * @inject
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 occurred on last mapping.
54 *
55 * @var \TYPO3\CMS\Extbase\Error\Result
56 */
57 protected $messages;
58
59 /**
60 * Lifecycle method, called after all dependencies have been injected.
61 * Here, the typeConverter array gets initialized.
62 *
63 * @throws Exception\DuplicateTypeConverterException
64 * @return void
65 */
66 public function initializeObject() {
67 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'] as $typeConverterClassName) {
68 $typeConverter = $this->objectManager->get($typeConverterClassName);
69 foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) {
70 if (isset($this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()])) {
71 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);
72 }
73 $this->typeConverters[$supportedSourceType][$typeConverter->getSupportedTargetType()][$typeConverter->getPriority()] = $typeConverter;
74 }
75 }
76 }
77
78 /**
79 * Map $source to $targetType, and return the result
80 *
81 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
82 * @param string $targetType The type of the target; can be either a class name or a simple type.
83 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration.
84 * @throws Exception
85 * @return mixed an instance of $targetType
86 * @api
87 */
88 public function convert($source, $targetType, \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration = NULL) {
89 if ($configuration === NULL) {
90 $configuration = $this->configurationBuilder->build();
91 }
92 $currentPropertyPath = array();
93 $this->messages = new \TYPO3\CMS\Extbase\Error\Result();
94 try {
95 $result = $this->doMapping($source, $targetType, $configuration, $currentPropertyPath);
96 if ($result instanceof \TYPO3\CMS\Extbase\Error\Error) {
97 return NULL;
98 }
99
100 return $result;
101 } catch (\Exception $e) {
102 throw new \TYPO3\CMS\Extbase\Property\Exception('Exception while property mapping at property path "' . implode('.', $currentPropertyPath) . '":' . $e->getMessage(), 1297759968, $e);
103 }
104 }
105
106 /**
107 * Get the messages of the last Property Mapping
108 *
109 * @return \TYPO3\CMS\Extbase\Error\Result
110 * @api
111 */
112 public function getMessages() {
113 return $this->messages;
114 }
115
116 /**
117 * Internal function which actually does the property mapping.
118 *
119 * @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
120 * @param string $targetType The type of the target; can be either a class name or a simple type.
121 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration Configuration for the property mapping.
122 * @param array &$currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown.
123 * @throws Exception\TypeConverterException
124 * @return mixed an instance of $targetType
125 */
126 protected function doMapping($source, $targetType, \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration, &$currentPropertyPath) {
127 if (is_object($source)) {
128 $targetType = $this->parseCompositeType($targetType);
129 if ($source instanceof $targetType) {
130 return $source;
131 }
132 }
133
134 if ($source === NULL) {
135 $source = '';
136 }
137
138 $typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
139 $targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration);
140
141 if (!is_object($typeConverter) || !$typeConverter instanceof \TYPO3\CMS\Extbase\Property\TypeConverterInterface) {
142 throw new \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException('Type converter for "' . $source . '" -> "' . $targetType . '" not found.');
143 }
144
145 $convertedChildProperties = array();
146 foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) {
147 $targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName);
148 if ($configuration->shouldSkip($targetPropertyName)) {
149 continue;
150 }
151
152 if (!$configuration->shouldMap($targetPropertyName)) {
153 if ($configuration->shouldSkipUnknownProperties()) {
154 continue;
155 }
156 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);
157 }
158
159 $targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $targetPropertyName, $configuration);
160
161 $subConfiguration = $configuration->getConfigurationFor($targetPropertyName);
162
163 $currentPropertyPath[] = $targetPropertyName;
164 $targetPropertyValue = $this->doMapping($sourcePropertyValue, $targetPropertyType, $subConfiguration, $currentPropertyPath);
165 array_pop($currentPropertyPath);
166 if (!($targetPropertyValue instanceof \TYPO3\CMS\Extbase\Error\Error)) {
167 $convertedChildProperties[$targetPropertyName] = $targetPropertyValue;
168 }
169 }
170 $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration);
171
172 if ($result instanceof \TYPO3\CMS\Extbase\Error\Error) {
173 $this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result);
174 }
175
176 return $result;
177 }
178
179 /**
180 * Determine the type converter to be used. If no converter has been found, an exception is raised.
181 *
182 * @param mixed $source
183 * @param string $targetType
184 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
185 * @throws Exception\TypeConverterException
186 * @throws Exception\InvalidTargetException
187 * @return \TYPO3\CMS\Extbase\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType.
188 */
189 protected function findTypeConverter($source, $targetType, \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration) {
190 if ($configuration->getTypeConverter() !== NULL) {
191 return $configuration->getTypeConverter();
192 }
193
194 $sourceType = $this->determineSourceType($source);
195
196 if (!is_string($targetType)) {
197 throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727);
198 }
199
200 $targetType = $this->parseCompositeType($targetType);
201 // This is needed to correctly convert old class names to new ones
202 // This compatibility layer will be removed with 7.0
203 $targetType = \TYPO3\CMS\Core\Core\ClassLoader::getClassNameForAlias($targetType);
204
205 $converter = NULL;
206
207 if (\TYPO3\CMS\Extbase\Utility\TypeHandlingUtility::isSimpleType($targetType)) {
208 if (isset($this->typeConverters[$sourceType][$targetType])) {
209 $converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType], $source, $targetType);
210 }
211 } else {
212 $converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetType);
213 }
214
215 if ($converter === NULL) {
216 throw new \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException('No converter found which can be used to convert from "' . $sourceType . '" to "' . $targetType . '".');
217 }
218
219 return $converter;
220 }
221
222 /**
223 * Tries to find a suitable type converter for the given source and target type.
224 *
225 * @param string $source The actual source value
226 * @param string $sourceType Type of the source to convert from
227 * @param string $targetClass Name of the target class to find a type converter for
228 * @return mixed Either the matching object converter or NULL
229 * @throws Exception\InvalidTargetException
230 */
231 protected function findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $targetClass) {
232 if (!class_exists($targetClass) && !interface_exists($targetClass)) {
233 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);
234 }
235
236 if (!isset($this->typeConverters[$sourceType])) {
237 return NULL;
238 }
239
240 $convertersForSource = $this->typeConverters[$sourceType];
241 if (isset($convertersForSource[$targetClass])) {
242 $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass], $source, $targetClass);
243 if ($converter !== NULL) {
244 return $converter;
245 }
246 }
247
248 foreach (class_parents($targetClass) as $parentClass) {
249 if (!isset($convertersForSource[$parentClass])) {
250 continue;
251 }
252
253 $converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$parentClass], $source, $targetClass);
254 if ($converter !== NULL) {
255 return $converter;
256 }
257 }
258
259 $converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass));
260 $converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetClass);
261
262 if ($converter !== NULL) {
263 return $converter;
264 }
265 if (isset($convertersForSource['object'])) {
266 return $this->findEligibleConverterWithHighestPriority($convertersForSource['object'], $source, $targetClass);
267 } else {
268 return NULL;
269 }
270 }
271
272 /**
273 * @param mixed $converters
274 * @param mixed $source
275 * @param string $targetType
276 * @return mixed Either the matching object converter or NULL
277 */
278 protected function findEligibleConverterWithHighestPriority($converters, $source, $targetType) {
279 if (!is_array($converters)) {
280 return NULL;
281 }
282 krsort($converters);
283 reset($converters);
284 foreach ($converters as $converter) {
285 if ($converter->canConvertFrom($source, $targetType)) {
286 return $converter;
287 }
288 }
289 return NULL;
290 }
291
292 /**
293 * @param array $convertersForSource
294 * @param array $interfaceNames
295 * @return array
296 * @throws Exception\DuplicateTypeConverterException
297 */
298 protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames) {
299 $convertersForInterface = array();
300 foreach ($interfaceNames as $implementedInterface) {
301 if (isset($convertersForSource[$implementedInterface])) {
302 foreach ($convertersForSource[$implementedInterface] as $priority => $converter) {
303 if (isset($convertersForInterface[$priority])) {
304 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);
305 }
306 $convertersForInterface[$priority] = $converter;
307 }
308 }
309 }
310 return $convertersForInterface;
311 }
312
313 /**
314 * Determine the type of the source data, or throw an exception if source was an unsupported format.
315 *
316 * @param mixed $source
317 * @throws Exception\InvalidSourceException
318 * @return string the type of $source
319 */
320 protected function determineSourceType($source) {
321 if (is_string($source)) {
322 return 'string';
323 } elseif (is_array($source)) {
324 return 'array';
325 } elseif (is_float($source)) {
326 return 'float';
327 } elseif (is_integer($source)) {
328 return 'integer';
329 } elseif (is_bool($source)) {
330 return 'boolean';
331 } else {
332 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);
333 }
334 }
335
336 /**
337 * Parse a composite type like \Foo\Collection<\Bar\Entity> into
338 * \Foo\Collection
339 *
340 * @param string $compositeType
341 * @return string
342 */
343 public function parseCompositeType($compositeType) {
344 if (strpos($compositeType, '<') !== FALSE) {
345 $compositeType = substr($compositeType, 0, strpos($compositeType, '<'));
346 }
347 return $compositeType;
348 }
349
350 }