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