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