[+BUGFIX] (Property) Fixed only adding objects to ObjectStorage
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Property / Mapper.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
6 * All rights reserved
7 *
8 * This class is a backport of the corresponding class of FLOW3.
9 * All credits go to the v5 team.
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27
28 /**
29 * The Property Mapper maps properties from a source onto a given target object, often a
30 * (domain-) model. Which properties are required and how they should be filtered can
31 * be customized.
32 *
33 * During the mapping process, the property values are validated and the result of this
34 * validation can be queried.
35 *
36 * The following code would map the property of the source array to the target:
37 *
38 * $target = new ArrayObject();
39 * $source = new ArrayObject(
40 * array(
41 * 'someProperty' => 'SomeValue'
42 * )
43 * );
44 * $mapper->mapAndValidate(array('someProperty'), $source, $target);
45 *
46 * Now the target object equals the source object.
47 *
48 * @package Extbase
49 * @subpackage Property
50 * @version $Id$
51 * @api
52 */
53 class Tx_Extbase_Property_Mapper implements t3lib_Singleton {
54
55 /**
56 * Results of the last mapping operation
57 * @var Tx_Extbase_Property_MappingResults
58 */
59 protected $mappingResults;
60
61 /**
62 * @var Tx_Extbase_Validation_ValidatorResolver
63 */
64 protected $validatorResolver;
65
66 /**
67 * @var Tx_Extbase_Reflection_Service
68 */
69 protected $reflectionService;
70
71 /**
72 * @var Tx_Extbase_Persistence_ManagerInterface
73 */
74 protected $persistenceManager;
75
76 /**
77 * @var Tx_Extbase_Object_ObjectManagerInterface
78 */
79 protected $objectManager;
80
81 /**
82 * @var Tx_Extbase_Persistence_QueryFactory
83 */
84 protected $queryFactory;
85
86 /**
87 * @param Tx_Extbase_Validation_ValidatorResolver $validatorResolver
88 * @return void
89 */
90 public function injectValidatorResolver(Tx_Extbase_Validation_ValidatorResolver $validatorResolver) {
91 $this->validatorResolver = $validatorResolver;
92 }
93
94 /**
95 *
96 * @param Tx_Extbase_Persistence_QueryFactory $queryFactory
97 * @return void
98 */
99 public function injectQueryFactory(Tx_Extbase_Persistence_QueryFactory $queryFactory) {
100 $this->queryFactory = $queryFactory;
101 }
102
103 /**
104 * @param Tx_Extbase_Persistence_Manager $persistenceManager
105 * @return void
106 */
107 public function injectPersistenceManager(Tx_Extbase_Persistence_Manager $persistenceManager) {
108 $this->persistenceManager = $persistenceManager;
109 }
110
111 /**
112 * @param Tx_Extbase_Reflection_Service $reflectionService
113 * @return void
114 */
115 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
116 $this->reflectionService = $reflectionService;
117 }
118
119 /**
120 * @param Tx_Extbase_Object_ObjectManagerInterface $objectManager
121 * @return void
122 */
123 public function injectObjectManager(Tx_Extbase_Object_ObjectManagerInterface $objectManager) {
124 $this->objectManager = $objectManager;
125 }
126
127 /**
128 * Maps the given properties to the target object and validates the properties according to the defined
129 * validators. If the result object is not valid, the operation will be undone (the target object remains
130 * unchanged) and this method returns FALSE.
131 *
132 * If in doubt, always prefer this method over the map() method because skipping validation can easily become
133 * a security issue.
134 *
135 * @param array $propertyNames Names of the properties to map.
136 * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
137 * @param object $target The target object
138 * @param Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator A validator used for validating the target object
139 * @param array $optionalPropertyNames Names of optional properties. If a property is specified here and it doesn't exist in the source, no error is issued.
140 * @return boolean TRUE if the mapped properties are valid, otherwise FALSE
141 * @see getMappingResults()
142 * @see map()
143 * @api
144 */
145 public function mapAndValidate(array $propertyNames, $source, $target, $optionalPropertyNames = array(), Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator) {
146 $backupProperties = array();
147
148 $this->map($propertyNames, $source, $backupProperties, $optionalPropertyNames);
149 if ($this->mappingResults->hasErrors()) return FALSE;
150
151 $this->map($propertyNames, $source, $target, $optionalPropertyNames);
152 if ($this->mappingResults->hasErrors()) return FALSE;
153
154 if ($targetObjectValidator->isValid($target) !== TRUE) {
155 $this->addErrorsFromObjectValidator($targetObjectValidator->getErrors());
156 $backupMappingResult = $this->mappingResults;
157 $this->map($propertyNames, $backupProperties, $source, $optionalPropertyNames);
158 $this->mappingResults = $backupMappingResult;
159 }
160 return (!$this->mappingResults->hasErrors());
161 }
162
163 /**
164 * Add errors to the mapping result from an object validator (property errors).
165 *
166 * @param array Array of Tx_Extbase_Validation_PropertyError
167 * @return void
168 */
169 protected function addErrorsFromObjectValidator($errors) {
170 foreach ($errors as $error) {
171 if ($error instanceof Tx_Extbase_Validation_PropertyError) {
172 $propertyName = $error->getPropertyName();
173 $this->mappingResults->addError($error, $propertyName);
174 }
175 }
176 }
177
178 /**
179 * Maps the given properties to the target object WITHOUT VALIDATING THE RESULT.
180 * If the properties could be set, this method returns TRUE, otherwise FALSE.
181 * Returning TRUE does not mean that the target object is valid and secure!
182 *
183 * Only use this method if you're sure that you don't need validation!
184 *
185 * @param array $propertyNames Names of the properties to map.
186 * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
187 * @param object $target The target object
188 * @param array $optionalPropertyNames Names of optional properties. If a property is specified here and it doesn't exist in the source, no error is issued.
189 * @return boolean TRUE if the properties could be mapped, otherwise FALSE
190 * @see mapAndValidate()
191 * @api
192 */
193 public function map(array $propertyNames, $source, $target, $optionalPropertyNames = array()) {
194 if (!is_object($source) && !is_array($source)) throw new Tx_Extbase_Property_Exception_InvalidSource('The source object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
195
196 if (is_string($target) && strpos($target, '_') !== FALSE) {
197 return $this->transformToObject($source, $target, '--none--');
198 }
199
200 if (!is_object($target) && !is_array($target)) throw new Tx_Extbase_Property_Exception_InvalidTarget('The target object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
201
202 $this->mappingResults = new Tx_Extbase_Property_MappingResults();
203 if (is_object($target)) {
204 $targetClassSchema = $this->reflectionService->getClassSchema(get_class($target));
205 } else {
206 $targetClassSchema = NULL;
207 }
208
209 foreach ($propertyNames as $propertyName) {
210 $propertyValue = NULL;
211 if (is_array($source) || $source instanceof ArrayAccess) {
212 if (isset($source[$propertyName])) {
213 $propertyValue = $source[$propertyName];
214 }
215 } else {
216 $propertyValue = Tx_Extbase_Reflection_ObjectAccess::getProperty($source, $propertyName);
217 }
218
219 if ($propertyValue === NULL && !in_array($propertyName, $optionalPropertyNames)) {
220 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Required property '$propertyName' does not exist." , 1236785359), $propertyName);
221 } else {
222 if ($targetClassSchema !== NULL && $targetClassSchema->hasProperty($propertyName)) {
223 $propertyMetaData = $targetClassSchema->getProperty($propertyName);
224
225 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'Tx_Extbase_Persistence_ObjectStorage')) && (strpos($propertyMetaData['elementType'], '_') !== FALSE || $propertyValue === '')) {
226 $objects = array();
227 if (is_array($propertyValue)) {
228 foreach ($propertyValue as $value) {
229 $transformedObject = $this->transformToObject($value, $propertyMetaData['elementType'], $propertyName);
230 if ($transformedObject !== NULL) {
231 $objects[] = $transformedObject;
232 }
233 }
234 }
235
236 // make sure we hand out what is expected
237 if ($propertyMetaData['type'] === 'ArrayObject') {
238 $propertyValue = new ArrayObject($objects);
239 } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
240 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
241 foreach ($objects as $object) {
242 $propertyValue->attach($object);
243 }
244 } else {
245 $propertyValue = $objects;
246 }
247 } elseif ($propertyMetaData['type'] === 'DateTime' || strpos($propertyMetaData['type'], '_') !== FALSE) {
248 $propertyValue = $this->transformToObject($propertyValue, $propertyMetaData['type'], $propertyName);
249 if ($propertyValue === NULL) {
250 continue;
251 }
252 }
253 } elseif ($targetClassSchema !== NULL) {
254 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' does not exist in target class schema." , 1251813614), $propertyName);
255 }
256
257 if (is_array($target)) {
258 $target[$propertyName] = $propertyValue;
259 } elseif (Tx_Extbase_Reflection_ObjectAccess::setProperty($target, $propertyName, $propertyValue) === FALSE) {
260 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' could not be set." , 1236783102), $propertyName);
261 }
262 }
263 }
264
265 return !$this->mappingResults->hasErrors();
266 }
267
268 /**
269 * Transforms strings with UUIDs or arrays with UUIDs/identity properties
270 * into the requested type, if possible.
271 *
272 * @param mixed $propertyValue The value to transform, string or array
273 * @param string $targetType The type to transform to
274 * @param string $propertyName In case of an error we add this to the error message
275 * @return object The object, when no transformation was possible this may return NULL as well
276 */
277 protected function transformToObject($propertyValue, $targetType, $propertyName) {
278 if ($targetType === 'DateTime' || is_subclass_of($targetType, 'DateTime')) {
279 // TODO replace this with converter implementation of FLOW3
280 if ($propertyValue === '') {
281 $propertyValue = NULL;
282 } else {
283 try {
284 $propertyValue = $this->objectManager->create($targetType, $propertyValue);
285 } catch (Exception $e) {
286 $propertyValue = NULL;
287 }
288 }
289 } else {
290 if (is_numeric($propertyValue)) {
291 $propertyValue = $this->findObjectByUid($targetType, $propertyValue);
292 if ($propertyValue === FALSE) {
293 $this->mappingResults->addError(new Tx_Extbase_Error_Error('Querying the repository for the specified object with UUID ' . $propertyValue . ' was not successful.' , 1249379517), $propertyName);
294 }
295 } elseif (is_array($propertyValue)) {
296 if (isset($propertyValue['__identity'])) {
297 $existingObject = $this->findObjectByUid($targetType, $propertyValue['__identity']);
298 if ($existingObject === FALSE) throw new Tx_Extbase_Property_Exception_TargetNotFound('Querying the repository for the specified object was not successful.', 1237305720);
299 unset($propertyValue['__identity']);
300 if (count($propertyValue) === 0) {
301 $propertyValue = $existingObject;
302 } elseif ($existingObject !== NULL) {
303 $newObject = clone $existingObject;
304 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
305 $propertyValue = $newObject;
306 } else {
307 $propertyValue = NULL;
308 }
309 }
310 } else {
311 $newObject = $this->objectManager->create($targetType);
312 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
313 $propertyValue = $newObject;
314 } else {
315 $propertyValue = NULL;
316 }
317 }
318 } else {
319 throw new InvalidArgumentException('transformToObject() accepts only numeric values and arrays.', 1251814355);
320 }
321 }
322
323 return $propertyValue;
324 }
325
326 /**
327 * Returns the results of the last mapping operation.
328 *
329 * @return Tx_Extbase_Property_MappingResults The mapping results (or NULL if no mapping has been carried out yet)
330 * @api
331 */
332 public function getMappingResults() {
333 return $this->mappingResults;
334 }
335
336 /**
337 * Finds an object from the repository by searching for its technical UID.
338 *
339 * @param string $dataType the data type to fetch
340 * @param int $uid The object's uid
341 * @return object Either the object matching the uid or, if none or more than one object was found, NULL
342 */
343 // TODO This is duplicated code; see Argument class
344 protected function findObjectByUid($dataType, $uid) {
345 $query = $this->queryFactory->create($dataType);
346 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
347 $query->getQuerySettings()->setRespectStoragePage(FALSE);
348 return $query->matching(
349 $query->equals('uid', intval($uid)))
350 ->execute()
351 ->getFirst();
352 }
353 }
354
355 ?>