[+FEATURE] Extbase (Persistence): Implemented a second Lazy Loading strategy called...
[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 */
52 class Tx_Extbase_Property_Mapper {
53
54 /**
55 * Results of the last mapping operation
56 * @var Tx_Extbase_Property_MappingResults
57 */
58 protected $mappingResults;
59
60 /**
61 * @var Tx_Extbase_Validation_ValidatorResolver
62 */
63 protected $validatorResolver;
64
65 /**
66 * @var Tx_Extbase_Reflection_Service
67 */
68 protected $reflectionService;
69
70 /**
71 * @var Tx_Extbase_Persistence_ManagerInterface
72 */
73 protected $persistenceManager;
74
75 /**
76 * @var Tx_Extbase_Persistence_QueryFactory
77 */
78 protected $queryFactory;
79
80 /**
81 * Constructs the Property Mapper.
82 */
83 public function __construct() {
84 $objectManager = t3lib_div::makeInstance('Tx_Extbase_Object_Manager');
85 $this->validatorResolver = t3lib_div::makeInstance('Tx_Extbase_Validation_ValidatorResolver');
86 $this->validatorResolver->injectObjectManager($objectManager);
87 $this->persistenceManager = Tx_Extbase_Dispatcher::getPersistenceManager();
88 $this->queryFactory = t3lib_div::makeInstance('Tx_Extbase_Persistence_QueryFactory');
89 }
90
91 /**
92 * Injects the Reflection Service
93 *
94 * @param Tx_Extbase_Reflection_Service
95 * @return void
96 */
97 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
98 $this->reflectionService = $reflectionService;
99 }
100
101 /**
102 * Maps the given properties to the target object and validates the properties according to the defined
103 * validators. If the result object is not valid, the operation will be undone (the target object remains
104 * unchanged) and this method returns FALSE.
105 *
106 * If in doubt, always prefer this method over the map() method because skipping validation can easily become
107 * a security issue.
108 *
109 * @param array $propertyNames Names of the properties to map.
110 * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
111 * @param object $target The target object
112 * @param Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator A validator used for validating the target object
113 * @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.
114 * @return boolean TRUE if the mapped properties are valid, otherwise FALSE
115 * @see getMappingResults()
116 * @see map()
117 * @api
118 */
119 public function mapAndValidate(array $propertyNames, $source, $target, $optionalPropertyNames = array(), Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator) {
120 $backupProperties = array();
121
122 $this->map($propertyNames, $source, $backupProperties, $optionalPropertyNames);
123 if ($this->mappingResults->hasErrors()) return FALSE;
124
125 $this->map($propertyNames, $source, $target, $optionalPropertyNames);
126 if ($this->mappingResults->hasErrors()) return FALSE;
127
128 if ($targetObjectValidator->isValid($target) !== TRUE) {
129 $this->addErrorsFromObjectValidator($targetObjectValidator->getErrors());
130 $backupMappingResult = $this->mappingResults;
131 $this->map($propertyNames, $backupProperties, $source, $optionalPropertyNames);
132 $this->mappingResults = $backupMappingResult;
133 }
134 return (!$this->mappingResults->hasErrors());
135 }
136
137 /**
138 * Add errors to the mapping result from an object validator (property errors).
139 *
140 * @param array Array of Tx_Extbase_Validation_PropertyError
141 * @return void
142 */
143 protected function addErrorsFromObjectValidator($errors) {
144 foreach ($errors as $error) {
145 if ($error instanceof Tx_Extbase_Validation_PropertyError) {
146 $propertyName = $error->getPropertyName();
147 $this->mappingResults->addError($error, $propertyName);
148 }
149 }
150 }
151
152 /**
153 * Maps the given properties to the target object WITHOUT VALIDATING THE RESULT.
154 * If the properties could be set, this method returns TRUE, otherwise FALSE.
155 * Returning TRUE does not mean that the target object is valid and secure!
156 *
157 * Only use this method if you're sure that you don't need validation!
158 *
159 * @param array $propertyNames Names of the properties to map.
160 * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
161 * @param object $target The target object
162 * @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.
163 * @return boolean TRUE if the properties could be mapped, otherwise FALSE
164 * @see mapAndValidate()
165 * @api
166 */
167 public function map(array $propertyNames, $source, $target, $optionalPropertyNames = array()) {
168 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);
169
170 if (is_string($target) && strpos($target, '_') !== FALSE) {
171 return $this->transformToObject($source, $target, '--none--');
172 }
173
174 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);
175
176 $this->mappingResults = new Tx_Extbase_Property_MappingResults();
177 if (is_object($target)) {
178 $targetClassSchema = $this->reflectionService->getClassSchema(get_class($target));
179 } else {
180 $targetClassSchema = NULL;
181 }
182
183 foreach ($propertyNames as $propertyName) {
184 $propertyValue = NULL;
185 if (is_array($source) || $source instanceof ArrayAccess) {
186 if (isset($source[$propertyName])) {
187 $propertyValue = $source[$propertyName];
188 }
189 } else {
190 $propertyValue = Tx_Extbase_Reflection_ObjectAccess::getProperty($source, $propertyName);
191 }
192
193 if ($propertyValue === NULL && !in_array($propertyName, $optionalPropertyNames)) {
194 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Required property '$propertyName' does not exist." , 1236785359), $propertyName);
195 } else {
196 if ($targetClassSchema !== NULL && $targetClassSchema->hasProperty($propertyName)) {
197 $propertyMetaData = $targetClassSchema->getProperty($propertyName);
198
199 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'Tx_Extbase_Persistence_ObjectStorage')) && strpos($propertyMetaData['elementType'], '_') !== FALSE) {
200 $objects = array();
201 foreach ($propertyValue as $value) {
202 $objects[] = $this->transformToObject($value, $propertyMetaData['elementType'], $propertyName);
203 }
204
205 // make sure we hand out what is expected
206 if ($propertyMetaData['type'] === 'ArrayObject') {
207 $propertyValue = new ArrayObject($objects);
208 } elseif ($propertyMetaData['type']=== 'Tx_Extbase_Persistence_ObjectStorage') {
209 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
210 foreach ($objects as $object) {
211 $propertyValue->attach($object);
212 }
213 } else {
214 $propertyValue = $objects;
215 }
216 } elseif (strpos($propertyMetaData['type'], '_') !== FALSE) {
217 $propertyValue = $this->transformToObject($propertyValue, $propertyMetaData['type'], $propertyName);
218 }
219 } elseif ($targetClassSchema !== NULL) {
220 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' does not exist in target class schema." , 1251813614), $propertyName);
221 }
222
223 if (is_array($target)) {
224 $target[$propertyName] = $propertyValue;
225 } elseif (Tx_Extbase_Reflection_ObjectAccess::setProperty($target, $propertyName, $propertyValue) === FALSE) {
226 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' could not be set." , 1236783102), $propertyName);
227 }
228 }
229 }
230
231 return !$this->mappingResults->hasErrors();
232 }
233
234 /**
235 * Transforms strings with UUIDs or arrays with UUIDs/identity properties
236 * into the requested type, if possible.
237 *
238 * @param mixed $propertyValue The value to transform, string or array
239 * @param string $targetType The type to transform to
240 * @param string $propertyName In case of an error we add this to the error message
241 * @return object
242 */
243 protected function transformToObject($propertyValue, $targetType, $propertyName) {
244 if (is_numeric($propertyValue)) {
245 $propertyValue = $this->findObjectByUid($targetType, $propertyValue);
246 if ($propertyValue === FALSE) {
247 $this->mappingResults->addError(new Tx_Extbase_Error_Error('Querying the repository for the specified object with UUID ' . $propertyValue . ' was not successful.' , 1249379517), $propertyName);
248 }
249 } elseif (is_array($propertyValue)) {
250 if (isset($propertyValue['__identity'])) {
251 $existingObject = $this->findObjectByUid($targetType, $propertyValue['__identity']);
252 if ($existingObject === FALSE) throw new Tx_Extbase_Property_Exception_TargetNotFound('Querying the repository for the specified object was not successful.', 1237305720);
253 unset($propertyValue['__identity']);
254 if (count($propertyValue) === 0) {
255 $propertyValue = $existingObject;
256 } elseif ($existingObject !== NULL) {
257 $newObject = clone $existingObject;
258 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
259 $propertyValue = $newObject;
260 }
261 }
262 } else {
263 $newObject = new $targetType;
264 if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
265 $propertyValue = $newObject;
266 }
267 }
268 } else {
269 throw new InvalidArgumentException('transformToObject() accepts only numeric values and arrays.', 1251814355);
270 }
271
272 return $propertyValue;
273 }
274
275 /**
276 * Returns the results of the last mapping operation.
277 *
278 * @return Tx_Extbase_Property_MappingResults The mapping results (or NULL if no mapping has been carried out yet)
279 * @api
280 */
281 public function getMappingResults() {
282 return $this->mappingResults;
283 }
284
285 /**
286 * Finds an object from the repository by searching for its technical UID.
287 *
288 * @param string $dataType the data type to fetch
289 * @param int $uid The object's uid
290 * @return mixed Either the object matching the uid or, if none or more than one object was found, FALSE
291 */
292 protected function findObjectByUid($dataType, $uid) {
293 $query = $this->queryFactory->create($dataType);
294 $result = $query->matching($query->withUid($uid))->execute();
295 $object = NULL;
296 if (count($result) > 0) {
297 $object = current($result);
298 }
299 return $object;
300 }
301 }
302
303 ?>