4f88f961eef8a3d72e71132327a2d83d7d8bf9dc
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Mapper / DataMapper.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25 /**
26 * A mapper to map database tables configured in $TCA on domain objects.
27 *
28 * @package Extbase
29 * @subpackage Persistence\Mapper
30 * @version $ID:$
31 */
32 class Tx_Extbase_Persistence_Mapper_DataMapper implements t3lib_Singleton {
33
34 /**
35 * @var Tx_Extbase_Persistence_IdentityMap
36 */
37 protected $identityMap;
38
39 /**
40 * @var Tx_Extbase_Reflection_Service
41 */
42 protected $reflectionService;
43
44 /**
45 * @var Tx_Extbase_Persistence_QOM_QueryObjectModelFactory
46 */
47 protected $qomFactory;
48
49 /**
50 * @var Tx_Extbase_Persistence_Session
51 */
52 protected $persistenceSession;
53
54 /**
55 * A reference to the page select object providing methods to perform language and work space overlays
56 *
57 * @var t3lib_pageSelect
58 **/
59 protected $pageSelectObject;
60
61 /**
62 * Cached data maps
63 *
64 * @var array
65 **/
66 protected $dataMaps = array();
67
68 /**
69 * @var Tx_Extbase_Persistence_Mapper_DataMapFactory
70 */
71 protected $dataMapFactory;
72
73 /**
74 * @var Tx_Extbase_Persistence_QueryFactoryInterface
75 */
76 protected $queryFactory;
77
78 /**
79 * The TYPO3 reference index object
80 *
81 * @var t3lib_refindex
82 **/
83 protected $referenceIndex;
84
85 /**
86 * @var Tx_Extbase_Object_ObjectManagerInterface
87 */
88 protected $objectManager;
89
90 /**
91 * Injects the identity map
92 *
93 * @param Tx_Extbase_Persistence_IdentityMap $identityMap
94 * @return void
95 */
96 public function injectIdentityMap(Tx_Extbase_Persistence_IdentityMap $identityMap) {
97 $this->identityMap = $identityMap;
98 }
99
100 /**
101 * Injects the persistence session
102 *
103 * @param Tx_Extbase_Persistence_Session $persistenceSession
104 * @return void
105 */
106 public function injectSession(Tx_Extbase_Persistence_Session $persistenceSession) {
107 $this->persistenceSession = $persistenceSession;
108 }
109
110 /**
111 * Injects the Reflection Service
112 *
113 * @param Tx_Extbase_Reflection_Service
114 * @return void
115 */
116 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
117 $this->reflectionService = $reflectionService;
118 }
119
120 /**
121 * Injects the DataMap Factory
122 *
123 * @param Tx_Extbase_Persistence_Mapper_DataMapFactory
124 * @return void
125 */
126 public function injectDataMapFactory(Tx_Extbase_Persistence_Mapper_DataMapFactory $dataMapFactory) {
127 $this->dataMapFactory = $dataMapFactory;
128 }
129
130 /**
131 * Injects the Query Factory
132 *
133 * @param Tx_Extbase_Persistence_QueryFactoryInterface $queryFactory
134 */
135 public function injectQueryFactory(Tx_Extbase_Persistence_QueryFactoryInterface $queryFactory) {
136 $this->queryFactory = $queryFactory;
137 }
138
139 /**
140 * Sets the query object model factory
141 *
142 * @param Tx_Extbase_Persistence_QOM_QueryObjectModelFactory $qomFactory
143 * @return void
144 */
145 public function injectQomFactory(Tx_Extbase_Persistence_QOM_QueryObjectModelFactory $qomFactory) {
146 $this->qomFactory = $qomFactory;
147 }
148
149 /**
150 * @param Tx_Extbase_Object_ObjectManagerInterface $objectManager
151 * @return void
152 */
153 public function injectObjectManager(Tx_Extbase_Object_ObjectManagerInterface $objectManager) {
154 $this->objectManager = $objectManager;
155 }
156
157 /**
158 * Maps the given rows on objects
159 *
160 * @param string $className The name of the class
161 * @param array $rows An array of arrays with field_name => value pairs
162 * @return array An array of objects of the given class
163 */
164 public function map($className, array $rows) {
165 $objects = array();
166 foreach ($rows as $row) {
167 $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row);
168 }
169 return $objects;
170 }
171
172 /**
173 * Returns the target type for the given row.
174 *
175 * @param string $className The name of the class
176 * @param array $row A single array with field_name => value pairs
177 * @return string The target type (a class name)
178 */
179 public function getTargetType($className, array $row) {
180 $dataMap = $this->getDataMap($className);
181 $targetType = $className;
182 if ($dataMap->getRecordTypeColumnName() !== NULL) {
183 foreach ($dataMap->getSubclasses() as $subclassName) {
184 $recordSubtype = $this->getDataMap($subclassName)->getRecordType();
185 if ($row[$dataMap->getRecordTypeColumnName()] === $recordSubtype) {
186 $targetType = $subclassName;
187 break;
188 }
189 }
190 }
191 return $targetType;
192 }
193
194 /**
195 * Maps a single row on an object of the given class
196 *
197 * @param string $className The name of the target class
198 * @param array $row A single array with field_name => value pairs
199 * @return object An object of the given class
200 */
201 protected function mapSingleRow($className, array $row) {
202 if ($this->identityMap->hasIdentifier($row['uid'], $className)) {
203 $object = $this->identityMap->getObjectByIdentifier($row['uid'], $className);
204 } else {
205 $object = $this->createEmptyObject($className);
206 $this->identityMap->registerObject($object, $row['uid']);
207 $this->thawProperties($object, $row);
208 $object->_memorizeCleanState();
209 $this->persistenceSession->registerReconstitutedObject($object);
210 }
211 return $object;
212 }
213
214 /**
215 * Creates a skeleton of the specified object
216 *
217 * @param string $className Name of the class to create a skeleton for
218 * @return object The object skeleton
219 */
220 protected function createEmptyObject($className) {
221 // Note: The class_implements() function also invokes autoload to assure that the interfaces
222 // and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
223 if (!in_array('Tx_Extbase_DomainObject_DomainObjectInterface', class_implements($className))) throw new Tx_Extbase_Object_Exception_CannotReconstituteObject('Cannot create empty instance of the class "' . $className . '" because it does not implement the Tx_Extbase_DomainObject_DomainObjectInterface.', 1234386924);
224 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
225 return $object;
226 }
227
228 /**
229 * Sets the given properties on the object.
230 *
231 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to set properties on
232 * @param array $row
233 * @return void
234 */
235 protected function thawProperties(Tx_Extbase_DomainObject_DomainObjectInterface $object, array $row) {
236 $className = get_class($object);
237 $dataMap = $this->getDataMap($className);
238 $object->_setProperty('uid', intval($row['uid']));
239 $object->_setProperty('_localizedUid', intval($row['uid']));
240 if ($dataMap->getLanguageIdColumnName() !== NULL) {
241 $object->_setProperty('_languageUid', intval($row[$dataMap->getLanguageIdColumnName()]));
242 if (isset($row['_LOCALIZED_UID'])) {
243 $object->_setProperty('_localizedUid', intval($row['_LOCALIZED_UID']));
244 }
245 }
246 $properties = $object->_getProperties();
247 foreach ($properties as $propertyName => $propertyValue) {
248 if (!$dataMap->isPersistableProperty($propertyName)) continue;
249 $columnMap = $dataMap->getColumnMap($propertyName);
250 $columnName = $columnMap->getColumnName();
251 $propertyData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
252 $propertyValue = NULL;
253 if ($row[$columnName] !== NULL) {
254 switch ($propertyData['type']) {
255 case 'integer':
256 $propertyValue = (int) $row[$columnName];
257 break;
258 case 'float':
259 $propertyValue = (float) $row[$columnName];
260 break;
261 case 'boolean':
262 $propertyValue = (boolean) $row[$columnName];
263 break;
264 case 'string':
265 $propertyValue = (string) $row[$columnName];
266 break;
267 case 'array':
268 // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet!
269 break;
270 case 'SplObjectStorage':
271 case 'Tx_Extbase_Persistence_ObjectStorage':
272 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
273 break;
274 default:
275 if (($propertyData['type'] === 'DateTime') || in_array('DateTime', class_parents($propertyData['type']))) {
276 $propertyValue = $this->mapDateTime($row[$columnName]);
277 } else {
278 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
279 // $propertyValue = $this->mapToObject($row[$columnName]); // Not supported, yet!
280 }
281 break;
282 }
283 }
284
285 if ($propertyValue !== NULL) {
286 $object->_setProperty($propertyName, $propertyValue);
287 }
288 }
289 }
290
291 /**
292 * Creates a DateTime from an unix timestamp. If the input is empty
293 * NULL is returned.
294 *
295 * @param integer $timestamp
296 * @return DateTime
297 */
298 protected function mapDateTime($timestamp) {
299 if (empty($timestamp)) { // 0 -> NULL !!!
300 return NULL;
301 } else {
302 return new DateTime(date('c', $timestamp));
303 }
304 }
305
306 /**
307 * Fetches a collection of objects related to a property of a parent object
308 *
309 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
310 * @param string $propertyName The name of the proxied property in it's parent
311 * @param mixed $fieldValue The raw field value.
312 * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
313 * @param bool $performLanguageOverlay A flag indication if the related objects should be localized
314 * @return mixed The result
315 */
316 public function fetchRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = TRUE) {
317 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
318 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
319 if ($enableLazyLoading === TRUE && $propertyMetaData['lazy']) {
320 if ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
321 $result = $this->objectManager->create('Tx_Extbase_Persistence_LazyObjectStorage', $parentObject, $propertyName, $fieldValue);
322 } else {
323 if (empty($fieldValue)) {
324 $result = NULL;
325 } else {
326 $result = $this->objectManager->create('Tx_Extbase_Persistence_LazyLoadingProxy', $parentObject, $propertyName, $fieldValue);
327 }
328 }
329 } else {
330 $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
331 }
332 return $result;
333 }
334
335 /**
336 * Fetches the related objects from the storage backend.
337 *
338 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
339 * @param string $propertyName The name of the proxied property in it's parent
340 * @param mixed $fieldValue The raw field value.
341 * @param bool $performLanguageOverlay A flag indication if the related objects should be localized
342 * @return void
343 */
344 protected function fetchRelatedEager(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
345 if ($fieldValue === '') return array();
346 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
347 return $query->execute();
348 }
349
350 /**
351 * Builds and returns the prepared query, ready to be executed.
352 *
353 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
354 * @param string $propertyName
355 * @param string $fieldValue
356 * @return void
357 */
358 protected function getPreparedQuery(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
359 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
360 $type = $this->getType(get_class($parentObject), $propertyName);
361
362 $query = $this->queryFactory->create($type);
363 $query->getQuerySettings()->setRespectStoragePage(FALSE);
364 if ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
365 if ($columnMap->getChildSortByFieldName() !== NULL) {
366 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
367 }
368 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
369 $query->setSource($this->getSource($parentObject, $propertyName));
370 if ($columnMap->getChildSortByFieldName() !== NULL) {
371 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
372 }
373 }
374 $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->getRelationTableMatchFields()));
375 return $query;
376 }
377
378 /**
379 * Builds and returns the constraint for multi value properties.
380 *
381 * @param Tx_Extbase_Persistence_QueryInterface $query
382 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
383 * @param string $propertyName
384 * @param string $fieldValue
385 * @param array $relationTableMatchFields
386 * @return Tx_Extbase_Persistence_QOM_ConstraintInterface $constraint
387 */
388 protected function getConstraint(Tx_Extbase_Persistence_QueryInterface $query, Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = array()) {
389 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
390 if ($columnMap->getParentKeyFieldName() !== NULL) {
391 $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
392 if($columnMap->getParentTableFieldName() !== NULL) {
393 $constraint = $query->logicalAnd(
394 $constraint,
395 $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
396 );
397 }
398 } else {
399 $constraint = $query->in('uid', t3lib_div::intExplode(',', $fieldValue));
400 }
401 if (count($relationTableMatchFields) > 0) {
402 foreach($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
403 $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
404 }
405 }
406 return $constraint;
407 }
408
409 /**
410 * Builds and returns the source to build a join for a m:n relation.
411 *
412 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
413 * @param string $propertyName
414 * @return Tx_Extbase_Persistence_QOM_SourceInterface $source
415 */
416 protected function getSource(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName) {
417 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
418 $left = $this->qomFactory->selector(NULL, $columnMap->getRelationTableName());
419 $childClassName = $this->getType(get_class($parentObject), $propertyName);
420 $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName());
421 $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid');
422 $source = $this->qomFactory->join(
423 $left,
424 $right,
425 Tx_Extbase_Persistence_QueryInterface::JCR_JOIN_TYPE_INNER,
426 $joinCondition
427 );
428 return $source;
429 }
430
431 /**
432 * Returns the given result as property value of the specified property type.
433 *
434 * @param mixed $result The result could be an object or an ObjectStorage
435 * @param array $propertyMetaData The property meta data
436 * @param array $result The result
437 * @return void
438 */
439 public function mapResultToPropertyValue(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $result) {
440 if ($result instanceof Tx_Extbase_Persistence_LoadingStrategyInterface) {
441 $propertyValue = $result;
442 } else {
443 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
444 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
445 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'SplObjectStorage', 'Tx_Extbase_Persistence_ObjectStorage'))) {
446 $elementType = $this->getType(get_class($parentObject), $propertyName);
447 $objects = array();
448 foreach ($result as $value) {
449 $objects[] = $value;
450 }
451
452 if ($propertyMetaData['type'] === 'ArrayObject') {
453 $propertyValue = new ArrayObject($objects);
454 } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
455 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
456 foreach ($objects as $object) {
457 $propertyValue->attach($object);
458 }
459 $propertyValue->_memorizeCleanState();
460 } else {
461 $propertyValue = $objects;
462 }
463 } elseif (strpos($propertyMetaData['type'], '_') !== FALSE) {
464 if (is_array($result)) {
465
466 if (current($result) !== FALSE) {
467 $propertyValue = current($result);
468 } else {
469 $propertyValue = NULL;
470 }
471 } else {
472 $propertyValue = $result;
473 }
474 }
475 }
476 return $propertyValue;
477 }
478
479 /**
480 * Counts the number of related objects assigned to a property of a parent object
481 *
482 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
483 * @param string $propertyName The name of the proxied property in it's parent
484 * @param mixed $fieldValue The raw field value.
485 */
486 public function countRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
487 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
488 return $query->count();
489 }
490
491 /**
492 * Delegates the call to the Data Map.
493 * Returns TRUE if the property is persistable (configured in $TCA)
494 *
495 * @param string $className The property name
496 * @param string $propertyName The property name
497 * @return boolean TRUE if the property is persistable (configured in $TCA)
498 */
499 public function isPersistableProperty($className, $propertyName) {
500 $dataMap = $this->getDataMap($className);
501 return $dataMap->isPersistableProperty($propertyName);
502 }
503
504 /**
505 * Returns a data map for a given class name
506 *
507 * @param string $className The class name you want to fetch the Data Map for
508 * @return Tx_Extbase_Persistence_Mapper_DataMap The data map
509 */
510 public function getDataMap($className) {
511 if (!is_string($className) || strlen($className) === 0) throw new Tx_Extbase_Persistence_Exception('No class name was given to retrieve the Data Map for.', 1251315965);
512 if (!isset($this->dataMaps[$className])) {
513 $this->dataMaps[$className] = $this->dataMapFactory->buildDataMap($className);
514 }
515 return $this->dataMaps[$className];
516 }
517
518 /**
519 * Returns the selector (table) name for a given class name.
520 *
521 * @param string $className
522 * @return string The selector name
523 */
524 public function convertClassNameToTableName($className = NULL) {
525 if ($className !== NULL) {
526 $tableName = $this->getDataMap($className)->getTableName();
527 } else {
528 $tableName = strtolower($className);
529 }
530 return $tableName;
531 }
532
533 /**
534 * Returns the column name for a given property name of the specified class.
535 *
536 * @param string $className
537 * @param string $propertyName
538 * @return string The column name
539 */
540 public function convertPropertyNameToColumnName($propertyName, $className = NULL) {
541 if (!empty($className)) {
542 $dataMap = $this->getDataMap($className);
543 if ($dataMap !== NULL) {
544 $columnMap = $dataMap->getColumnMap($propertyName);
545 if ($columnMap !== NULL) {
546 return $columnMap->getColumnName();
547 }
548 }
549 }
550 return Tx_Extbase_Utility_Extension::convertCamelCaseToLowerCaseUnderscored($propertyName);
551 }
552
553 /**
554 * Returns the type of a child object.
555 *
556 * @param string $parentClassName The class name of the object this proxy is part of
557 * @param string $propertyName The name of the proxied property in it's parent
558 * @return string The class name of the child object
559 */
560 public function getType($parentClassName, $propertyName) {
561 $propertyMetaData = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
562 if (!empty($propertyMetaData['elementType'])) {
563 $type = $propertyMetaData['elementType'];
564 } elseif (!empty($propertyMetaData['type'])) {
565 $type = $propertyMetaData['type'];
566 } else {
567 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the child object type.', 1251315967);
568 }
569 return $type;
570 }
571
572 }
573 ?>