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