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