46d67e4447ae4dad44fa0c1709c9676082121978
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Property / Mapper.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Property;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * This class is a backport of the corresponding class of TYPO3 Flow.
8 * All credits go to the TYPO3 Flow team.
9 * All rights reserved
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 * A copy is found in the textfile GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30 /**
31 * The Property Mapper maps properties from a source onto a given target object, often a
32 * (domain-) model. Which properties are required and how they should be filtered can
33 * be customized.
34 *
35 * During the mapping process, the property values are validated and the result of this
36 * validation can be queried.
37 *
38 * The following code would map the property of the source array to the target:
39 *
40 * $target = new ArrayObject();
41 * $source = new ArrayObject(
42 * array(
43 * 'someProperty' => 'SomeValue'
44 * )
45 * );
46 * $mapper->mapAndValidate(array('someProperty'), $source, $target);
47 *
48 * Now the target object equals the source object.
49 *
50 * @api
51 * @deprecated since Extbase 1.4.0
52 */
53 class Mapper implements \TYPO3\CMS\Core\SingletonInterface {
54
55 /**
56 * Results of the last mapping operation
57 *
58 * @var \TYPO3\CMS\Extbase\Property\MappingResults
59 */
60 protected $mappingResults;
61
62 /**
63 * @var \TYPO3\CMS\Extbase\Validation\ValidatorResolver
64 */
65 protected $validatorResolver;
66
67 /**
68 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
69 */
70 protected $reflectionService;
71
72 /**
73 * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
74 */
75 protected $persistenceManager;
76
77 /**
78 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
79 */
80 protected $objectManager;
81
82 /**
83 * @var \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory
84 */
85 protected $queryFactory;
86
87 /**
88 * @param \TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver
89 * @return void
90 */
91 public function injectValidatorResolver(\TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver) {
92 $this->validatorResolver = $validatorResolver;
93 }
94
95 /**
96 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory $queryFactory
97 * @return void
98 */
99 public function injectQueryFactory(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory $queryFactory) {
100 $this->queryFactory = $queryFactory;
101 }
102
103 /**
104 * @param \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager $persistenceManager
105 * @return void
106 */
107 public function injectPersistenceManager(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager $persistenceManager) {
108 $this->persistenceManager = $persistenceManager;
109 }
110
111 /**
112 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
113 * @return void
114 */
115 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService) {
116 $this->reflectionService = $reflectionService;
117 }
118
119 /**
120 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
121 * @return void
122 */
123 public function injectObjectManager(\TYPO3\CMS\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 \TYPO3\CMS\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(), \TYPO3\CMS\Extbase\Validation\Validator\ObjectValidatorInterface $targetObjectValidator) {
146 $backupProperties = array();
147 $this->map($propertyNames, $source, $backupProperties, $optionalPropertyNames);
148 if ($this->mappingResults->hasErrors()) {
149 return FALSE;
150 }
151 $this->map($propertyNames, $source, $target, $optionalPropertyNames);
152 if ($this->mappingResults->hasErrors()) {
153 return FALSE;
154 }
155 if ($targetObjectValidator->isValid($target) !== TRUE) {
156 $this->addErrorsFromObjectValidator($targetObjectValidator->getErrors());
157 $backupMappingResult = $this->mappingResults;
158 $this->map($propertyNames, $backupProperties, $source, $optionalPropertyNames);
159 $this->mappingResults = $backupMappingResult;
160 }
161 return !$this->mappingResults->hasErrors();
162 }
163
164 /**
165 * Add errors to the mapping result from an object validator (property errors).
166 *
167 * @param array Array of \TYPO3\CMS\Extbase\Validation\PropertyError
168 * @return void
169 */
170 protected function addErrorsFromObjectValidator($errors) {
171 foreach ($errors as $error) {
172 if ($error instanceof \TYPO3\CMS\Extbase\Validation\PropertyError) {
173 $propertyName = $error->getPropertyName();
174 $this->mappingResults->addError($error, $propertyName);
175 }
176 }
177 }
178
179 /**
180 * Maps the given properties to the target object WITHOUT VALIDATING THE RESULT.
181 * If the properties could be set, this method returns TRUE, otherwise FALSE.
182 * Returning TRUE does not mean that the target object is valid and secure!
183 *
184 * Only use this method if you're sure that you don't need validation!
185 *
186 * @param array $propertyNames Names of the properties to map.
187 * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
188 * @param object|array $target The target object
189 * @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.
190 * @throws Exception\InvalidSourceException
191 * @throws Exception\InvalidTargetException
192 * @return boolean TRUE if the properties could be mapped, otherwise FALSE
193 * @see mapAndValidate()
194 * @api
195 */
196 public function map(array $propertyNames, $source, $target, $optionalPropertyNames = array()) {
197 if (!is_object($source) && !is_array($source)) {
198 throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidSourceException('The source object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
199 }
200 if (is_string($target) && strpbrk($target, '_\\') !== FALSE) {
201 return $this->transformToObject($source, $target, '--none--');
202 }
203 if (!is_object($target) && !is_array($target)) {
204 throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException('The target object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
205 }
206 $this->mappingResults = new \TYPO3\CMS\Extbase\Property\MappingResults();
207 if (is_object($target)) {
208 $targetClassSchema = $this->reflectionService->getClassSchema(get_class($target));
209 } else {
210 $targetClassSchema = NULL;
211 }
212 foreach ($propertyNames as $propertyName) {
213 $propertyValue = NULL;
214 if (is_array($source) || $source instanceof \ArrayAccess) {
215 if (isset($source[$propertyName])) {
216 $propertyValue = $source[$propertyName];
217 }
218 } else {
219 $propertyValue = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getProperty($source, $propertyName);
220 }
221 if ($propertyValue === NULL && !in_array($propertyName, $optionalPropertyNames)) {
222 $this->mappingResults->addError(new \TYPO3\CMS\Extbase\Error\Error("Required property '{$propertyName}' does not exist.", 1236785359), $propertyName);
223 } else {
224 if ($targetClassSchema !== NULL && $targetClassSchema->hasProperty($propertyName)) {
225 $propertyMetaData = $targetClassSchema->getProperty($propertyName);
226 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'TYPO3\\CMS\\Extbase\\Persistence\\ObjectStorage', 'Tx_Extbase_Persistence_ObjectStorage'), TRUE) && (strpbrk($propertyMetaData['elementType'], '_\\') !== FALSE || $propertyValue === '')) {
227 $objects = array();
228 if (is_array($propertyValue)) {
229 foreach ($propertyValue as $value) {
230 $transformedObject = $this->transformToObject($value, $propertyMetaData['elementType'], $propertyName);
231 if ($transformedObject !== NULL) {
232 $objects[] = $transformedObject;
233 }
234 }
235 }
236 // make sure we hand out what is expected
237 if ($propertyMetaData['type'] === 'ArrayObject') {
238 $propertyValue = new \ArrayObject($objects);
239 } elseif (in_array($propertyMetaData['type'], array('TYPO3\\CMS\\Extbase\\Persistence\\ObjectStorage', 'Tx_Extbase_Persistence_ObjectStorage'), TRUE)) {
240 $propertyValue = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
241 foreach ($objects as $object) {
242 $propertyValue->attach($object);
243 }
244 } else {
245 $propertyValue = $objects;
246 }
247 } elseif ($propertyMetaData['type'] === 'DateTime' || strpbrk($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 \TYPO3\CMS\Extbase\Error\Error("Property '{$propertyName}' does not exist in target class schema.", 1251813614), $propertyName);
255 }
256 if (is_array($target)) {
257 $target[$propertyName] = $propertyValue;
258 } elseif (\TYPO3\CMS\Extbase\Reflection\ObjectAccess::setProperty($target, $propertyName, $propertyValue) === FALSE) {
259 $this->mappingResults->addError(new \TYPO3\CMS\Extbase\Error\Error("Property '{$propertyName}' could not be set.", 1236783102), $propertyName);
260 }
261 }
262 }
263 return !$this->mappingResults->hasErrors();
264 }
265
266 /**
267 * Transforms strings with UUIDs or arrays with UUIDs/identity properties
268 * into the requested type, if possible.
269 *
270 * @param mixed $propertyValue The value to transform, string or array
271 * @param string $targetType The type to transform to
272 * @param string $propertyName In case of an error we add this to the error message
273 * @throws Exception\InvalidTargetException
274 * @throws \InvalidArgumentException
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 \TYPO3\CMS\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) {
299 throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException('Querying the repository for the specified object was not successful.', 1237305720);
300 }
301 unset($propertyValue['__identity']);
302 if (count($propertyValue) === 0) {
303 $propertyValue = $existingObject;
304 } elseif ($existingObject !== NULL) {
305 $newObject = clone $existingObject;
306 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
307 $propertyValue = $newObject;
308 } else {
309 $propertyValue = NULL;
310 }
311 }
312 } else {
313 $newObject = $this->objectManager->create($targetType);
314 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
315 $propertyValue = $newObject;
316 } else {
317 $propertyValue = NULL;
318 }
319 }
320 } else {
321 throw new \InvalidArgumentException('transformToObject() accepts only numeric values and arrays.', 1251814355);
322 }
323 }
324 return $propertyValue;
325 }
326
327 /**
328 * Returns the results of the last mapping operation.
329 *
330 * @return \TYPO3\CMS\Extbase\Property\MappingResults The mapping results (or NULL if no mapping has been carried out yet)
331 * @api
332 */
333 public function getMappingResults() {
334 return $this->mappingResults;
335 }
336
337 /**
338 * Finds an object from the repository by searching for its technical UID.
339 * TODO This is duplicated code; see Argument class
340 *
341 * @param string $dataType the data type to fetch
342 * @param integer $uid The object's uid
343 * @return object Either the object matching the uid or, if none or more than one object was found, NULL
344 */
345 protected function findObjectByUid($dataType, $uid) {
346 $query = $this->queryFactory->create($dataType);
347 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
348 $query->getQuerySettings()->setRespectStoragePage(FALSE);
349 return $query->matching($query->equals('uid', intval($uid)))->execute()->getFirst();
350 }
351 }
352
353 ?>