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