2c1d9024a57cdbe1e4c8730cf9fd19ecc1a11c7f
[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 * Constructs a new mapper
87 *
88 */
89 public function __construct() {
90 $this->queryFactory = t3lib_div::makeInstance('Tx_Extbase_Persistence_QueryFactory');
91 $this->dataMapFactory = t3lib_div::makeInstance('Tx_Extbase_Persistence_Mapper_DataMapFactory');
92 }
93
94 /**
95 * Injects the identity map
96 *
97 * @param Tx_Extbase_Persistence_IdentityMap $identityMap
98 * @return void
99 */
100 public function injectIdentityMap(Tx_Extbase_Persistence_IdentityMap $identityMap) {
101 $this->identityMap = $identityMap;
102 }
103
104 /**
105 * Injects the persistence session
106 *
107 * @param Tx_Extbase_Persistence_Session $persistenceSession
108 * @return void
109 */
110 public function injectSession(Tx_Extbase_Persistence_Session $persistenceSession) {
111 $this->persistenceSession = $persistenceSession;
112 }
113
114 /**
115 * Injects the Reflection Service
116 *
117 * @param Tx_Extbase_Reflection_Service
118 * @return void
119 */
120 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
121 $this->reflectionService = $reflectionService;
122 }
123
124 /**
125 * Sets the query object model factory
126 *
127 * @param Tx_Extbase_Persistence_QOM_QueryObjectModelFactory $qomFactory
128 * @return void
129 */
130 public function setQomFactory(Tx_Extbase_Persistence_QOM_QueryObjectModelFactory $qomFactory) {
131 $this->qomFactory = $qomFactory;
132 }
133
134 /**
135 * Maps the given rows on objects
136 *
137 * @param string $className The name of the class
138 * @param array $rows An array of arrays with field_name => value pairs
139 * @return array An array of objects of the given class
140 */
141 public function map($className, array $rows) {
142 $objects = array();
143 foreach ($rows as $row) {
144 $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row);
145 }
146 return $objects;
147 }
148
149 /**
150 * Returns the target type for the given row.
151 *
152 * @param string $className The name of the class
153 * @param array $row A single array with field_name => value pairs
154 * @return string The target type (a class name)
155 */
156 public function getTargetType($className, array $row) {
157 $dataMap = $this->getDataMap($className);
158 $targetType = $className;
159 if ($dataMap->getRecordTypeColumnName() !== NULL) {
160 foreach ($dataMap->getSubclasses() as $subclassName) {
161 $recordSubtype = $this->getDataMap($subclassName)->getRecordType();
162 if ($row[$dataMap->getRecordTypeColumnName()] === $recordSubtype) {
163 $targetType = $subclassName;
164 break;
165 }
166 }
167 }
168 return $targetType;
169 }
170
171 /**
172 * Maps a single row on an object of the given class
173 *
174 * @param string $className The name of the target class
175 * @param array $row A single array with field_name => value pairs
176 * @return object An object of the given class
177 */
178 protected function mapSingleRow($className, array $row) {
179 if ($this->identityMap->hasIdentifier($row['uid'], $className)) {
180 $object = $this->identityMap->getObjectByIdentifier($row['uid'], $className);
181 } else {
182 $object = $this->createEmptyObject($className);
183 $this->identityMap->registerObject($object, $row['uid']);
184 $this->thawProperties($object, $row);
185 $object->_memorizeCleanState();
186 $this->persistenceSession->registerReconstitutedObject($object);
187 }
188 return $object;
189 }
190
191 /**
192 * Creates a skeleton of the specified object
193 *
194 * @param string $className Name of the class to create a skeleton for
195 * @return object The object skeleton
196 */
197 protected function createEmptyObject($className) {
198 // Note: The class_implements() function also invokes autoload to assure that the interfaces
199 // and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
200 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);
201 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
202 return $object;
203 }
204
205 /**
206 * Sets the given properties on the object.
207 *
208 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to set properties on
209 * @param Tx_Extbase_Persistence_RowInterface $row
210 * @return void
211 */
212 protected function thawProperties(Tx_Extbase_DomainObject_DomainObjectInterface $object, array $row) {
213 $className = get_class($object);
214 $dataMap = $this->getDataMap($className);
215 $object->_setProperty('uid', intval($row['uid']));
216 $object->_setProperty('_localizedUid', intval($row['uid']));
217 if ($dataMap->getLanguageIdColumnName() !== NULL) {
218 $object->_setProperty('_languageUid', intval($row[$dataMap->getLanguageIdColumnName()]));
219 if (isset($row['_LOCALIZED_UID'])) {
220 $object->_setProperty('_localizedUid', intval($row['_LOCALIZED_UID']));
221 }
222 }
223 $properties = $object->_getProperties();
224 foreach ($properties as $propertyName => $propertyValue) {
225 if (!$dataMap->isPersistableProperty($propertyName)) continue;
226 $columnMap = $dataMap->getColumnMap($propertyName);
227 $columnName = $columnMap->getColumnName();
228 $propertyData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
229 $propertyValue = NULL;
230 if ($row[$columnName] !== NULL) {
231 switch ($propertyData['type']) {
232 case 'integer':
233 $propertyValue = (int) $row[$columnName];
234 break;
235 case 'float':
236 $propertyValue = (float) $row[$columnName];
237 break;
238 case 'boolean':
239 $propertyValue = (boolean) $row[$columnName];
240 break;
241 case 'string':
242 $propertyValue = (string) $row[$columnName];
243 break;
244 case 'array':
245 // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet!
246 break;
247 case 'SplObjectStorage':
248 case 'Tx_Extbase_Persistence_ObjectStorage':
249 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
250 break;
251 default:
252 if (($propertyData['type'] === 'DateTime') || in_array('DateTime', class_parents($propertyData['type']))) {
253 $propertyValue = $this->mapDateTime($row[$columnName]);
254 } else {
255 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
256 // $propertyValue = $this->mapToObject($row[$columnName]); // Not supported, yet!
257 }
258 break;
259 }
260 }
261
262 if ($propertyValue !== NULL) {
263 $object->_setProperty($propertyName, $propertyValue);
264 }
265 }
266 }
267
268 /**
269 * Creates a DateTime from an unix timestamp. If the input is empty
270 * NULL is returned.
271 *
272 * @param integer $timestamp
273 * @return DateTime
274 */
275 protected function mapDateTime($timestamp) {
276 if (empty($timestamp)) { // 0 -> NULL !!!
277 return NULL;
278 } else {
279 return new DateTime(date('c', $timestamp));
280 }
281 }
282
283 /**
284 * Fetches a collection of objects related to a property of a parent object
285 *
286 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
287 * @param string $propertyName The name of the proxied property in it's parent
288 * @param mixed $fieldValue The raw field value.
289 * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
290 * @param bool $performLanguageOverlay A flag indication if the related objects should be localized
291 * @return mixed The result
292 */
293 public function fetchRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = TRUE) {
294 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
295 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
296 if ($enableLazyLoading === TRUE && $propertyMetaData['lazy']) {
297 if ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
298 $result = t3lib_div::makeInstance('Tx_Extbase_Persistence_LazyObjectStorage', $parentObject, $propertyName, $fieldValue);
299 } else {
300 if (empty($fieldValue)) {
301 $result = NULL;
302 } else {
303 $result = t3lib_div::makeInstance('Tx_Extbase_Persistence_LazyLoadingProxy', $parentObject, $propertyName, $fieldValue);
304 }
305 }
306 } else {
307 $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
308 }
309 return $result;
310 }
311
312 /**
313 * Fetches the related objects from the storage backend.
314 *
315 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
316 * @param string $propertyName The name of the proxied property in it's parent
317 * @param mixed $fieldValue The raw field value.
318 * @param bool $performLanguageOverlay A flag indication if the related objects should be localized
319 * @return void
320 */
321 protected function fetchRelatedEager(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
322 if ($fieldValue === '') return array();
323 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
324 return $query->execute();
325 }
326
327 /**
328 * Builds and returns the prepared query, ready to be executed.
329 *
330 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
331 * @param string $propertyName
332 * @param string $fieldValue
333 * @return void
334 */
335 protected function getPreparedQuery(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
336 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
337 $queryFactory = t3lib_div::makeInstance('Tx_Extbase_Persistence_QueryFactory');
338 if ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_ONE) {
339 $query = $queryFactory->create($this->getType(get_class($parentObject), $propertyName));
340 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
341 $query = $queryFactory->create($this->getElementType(get_class($parentObject), $propertyName));
342 $query->getQuerySettings()->setRespectStoragePage(FALSE);
343 if ($columnMap->getChildSortByFieldName() !== NULL) {
344 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
345 }
346 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
347 $query = $queryFactory->create($this->getElementType(get_class($parentObject), $propertyName));
348 $query->getQuerySettings()->setRespectStoragePage(FALSE);
349 $query->setSource($this->getSource($parentObject, $propertyName));
350 if ($columnMap->getChildSortByFieldName() !== NULL) {
351 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
352 }
353 } else {
354 throw new Tx_Extbase_Persistence_Exception('Could not determine type of relation for the property "' . $propertyName . '". This is mainly caused by a missing type declaration above the property definition.', 1252502725);
355 }
356 $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->getRelationTableMatchFields()));
357 return $query;
358 }
359
360 /**
361 * Builds and returns the constraint for multi value properties.
362 *
363 * @param Tx_Extbase_Persistence_QueryInterface $query
364 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
365 * @param string $propertyName
366 * @param string $fieldValue
367 * @param array $relationTableMatchFields
368 * @return Tx_Extbase_Persistence_QOM_ConstraintInterface $constraint
369 */
370 protected function getConstraint(Tx_Extbase_Persistence_QueryInterface $query, Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = array()) {
371 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
372 if ($columnMap->getParentKeyFieldName() !== NULL) {
373 $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
374 if($columnMap->getParentTableFieldName() !== NULL) {
375 $constraint = $query->logicalAnd(
376 $constraint,
377 $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
378 );
379 }
380 } else {
381 $constraint = $query->in('uid', t3lib_div::intExplode(',', $fieldValue));
382 }
383 if (count($relationTableMatchFields) > 0) {
384 foreach($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
385 $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
386 }
387 }
388 return $constraint;
389 }
390
391 /**
392 * Builds and returns the source to build a join for a m:n relation.
393 *
394 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
395 * @param string $propertyName
396 * @return Tx_Extbase_Persistence_QOM_SourceInterface $source
397 */
398 protected function getSource(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName) {
399 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
400 $left = $this->qomFactory->selector(NULL, $columnMap->getRelationTableName());
401 $childClassName = $this->getElementType(get_class($parentObject), $propertyName);
402 $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName());
403 $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid');
404 $source = $this->qomFactory->join(
405 $left,
406 $right,
407 Tx_Extbase_Persistence_QueryInterface::JCR_JOIN_TYPE_INNER,
408 $joinCondition
409 );
410 return $source;
411 }
412
413 /**
414 * Returns the given result as property value of the specified property type.
415 *
416 * @param mixed $result The result could be an object or an ObjectStorage
417 * @param array $propertyMetaData The property meta data
418 * @return void
419 */
420 public function mapResultToPropertyValue(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $result) {
421 if ($result instanceof Tx_Extbase_Persistence_LoadingStrategyInterface) {
422 $propertyValue = $result;
423 } else {
424 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
425 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
426 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'Tx_Extbase_Persistence_ObjectStorage'))) {
427 $elementType = $this->getElementType(get_class($parentObject), $propertyName);
428 $objects = array();
429 foreach ($result as $value) {
430 $objects[] = $value;
431 }
432
433 if ($propertyMetaData['type'] === 'ArrayObject') {
434 $propertyValue = new ArrayObject($objects);
435 } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
436 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
437 foreach ($objects as $object) {
438 $propertyValue->attach($object);
439 }
440 } else {
441 $propertyValue = $objects;
442 }
443 } elseif (strpos($propertyMetaData['type'], '_') !== FALSE) {
444 $propertyValue = current($result);
445 }
446 }
447 return $propertyValue;
448 }
449
450 /**
451 * Counts the number of related objects assigned to a property of a parent object
452 *
453 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
454 * @param string $propertyName The name of the proxied property in it's parent
455 * @param mixed $fieldValue The raw field value.
456 */
457 public function countRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
458 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
459 return $query->count();
460 }
461
462 /**
463 * Delegates the call to the Data Map.
464 * Returns TRUE if the property is persistable (configured in $TCA)
465 *
466 * @param string $className The property name
467 * @param string $propertyName The property name
468 * @return boolean TRUE if the property is persistable (configured in $TCA)
469 */
470 public function isPersistableProperty($className, $propertyName) {
471 $dataMap = $this->getDataMap($className);
472 return $dataMap->isPersistableProperty($propertyName);
473 }
474
475 /**
476 * Returns a data map for a given class name
477 *
478 * @param string $className The class name you want to fetch the Data Map for
479 * @return Tx_Extbase_Persistence_Mapper_DataMap The data map
480 */
481 public function getDataMap($className) {
482 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);
483 if (!isset($this->dataMaps[$className])) {
484 $this->dataMaps[$className] = $this->dataMapFactory->buildDataMap($className);
485 }
486 return $this->dataMaps[$className];
487 }
488
489 /**
490 * Returns the selector (table) name for a given class name.
491 *
492 * @param string $className
493 * @return string The selector name
494 */
495 public function convertClassNameToTableName($className = NULL) {
496 if ($className !== NULL) {
497 $tableName = $this->getDataMap($className)->getTableName();
498 } else {
499 $tableName = strtolower($className);
500 }
501 return $tableName;
502 }
503
504 /**
505 * Returns the column name for a given property name of the specified class.
506 *
507 * @param string $className
508 * @param string $propertyName
509 * @return string The column name
510 */
511 public function convertPropertyNameToColumnName($propertyName, $className = NULL) {
512 if (!empty($className)) {
513 $dataMap = $this->getDataMap($className);
514 if ($dataMap !== NULL) {
515 $columnMap = $dataMap->getColumnMap($propertyName);
516 if ($columnMap !== NULL) {
517 return $columnMap->getColumnName();
518 }
519 }
520 }
521 return Tx_Extbase_Utility_Extension::convertCamelCaseToLowerCaseUnderscored($propertyName);
522 }
523
524 /**
525 * Returns the type of a child object.
526 *
527 * @param string $parentClassName The class name of the object this proxy is part of
528 * @param string $propertyName The name of the proxied property in it's parent
529 * @return string The class name of the child object
530 */
531 public function getType($parentClassName, $propertyName) {
532 $propertyMetaData = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
533 if (!empty($propertyMetaData['type'])) {
534 $type = $propertyMetaData['type'];
535 } else {
536 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the child object type.', 1251315967);
537 }
538 return $type;
539 }
540
541 /**
542 * Returns the type of the elements inside an ObjectStorage or array.
543 *
544 * @param string $parentClassName The class name of the object this proxy is part of
545 * @param string $propertyName The name of the proxied property in it's parent
546 * @return string The class name of the elements inside an ObjectStorage
547 */
548 public function getElementType($parentClassName, $propertyName) {
549 $propertyMetaData = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
550 if (!empty($propertyMetaData['elementType'])) {
551 $elementType = $propertyMetaData['elementType'];
552 } else {
553 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the type of the contained objects.', 1251315966);
554 }
555 return $elementType;
556 }
557
558 }
559 ?>