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