[BUGFIX] DataMapper returns array on RELATION_HAS_ONE
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / 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 = $this->objectManager->getEmptyObject($className);
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('pid', intval($row['pid']));
240 $object->_setProperty('_localizedUid', intval($row['uid']));
241 if ($dataMap->getLanguageIdColumnName() !== NULL) {
242 $object->_setProperty('_languageUid', intval($row[$dataMap->getLanguageIdColumnName()]));
243 if (isset($row['_LOCALIZED_UID'])) {
244 $object->_setProperty('_localizedUid', intval($row['_LOCALIZED_UID']));
245 }
246 }
247 $properties = $object->_getProperties();
248 foreach ($properties as $propertyName => $propertyValue) {
249 if (!$dataMap->isPersistableProperty($propertyName)) continue;
250 $columnMap = $dataMap->getColumnMap($propertyName);
251 $columnName = $columnMap->getColumnName();
252 $propertyData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
253 $propertyValue = NULL;
254 if ($row[$columnName] !== NULL) {
255 switch ($propertyData['type']) {
256 case 'integer':
257 $propertyValue = (int) $row[$columnName];
258 break;
259 case 'float':
260 $propertyValue = (float) $row[$columnName];
261 break;
262 case 'boolean':
263 $propertyValue = (boolean) $row[$columnName];
264 break;
265 case 'string':
266 $propertyValue = (string) $row[$columnName];
267 break;
268 case 'array':
269 // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet!
270 break;
271 case 'SplObjectStorage':
272 case 'Tx_Extbase_Persistence_ObjectStorage':
273 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
274 break;
275 default:
276 if (($propertyData['type'] === 'DateTime') || in_array('DateTime', class_parents($propertyData['type']))) {
277 $propertyValue = $this->mapDateTime($row[$columnName]);
278 } else {
279 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $this->fetchRelated($object, $propertyName, $row[$columnName]));
280 // $propertyValue = $this->mapToObject($row[$columnName]); // Not supported, yet!
281 }
282 break;
283 }
284 }
285
286 if ($propertyValue !== NULL) {
287 $object->_setProperty($propertyName, $propertyValue);
288 }
289 }
290 }
291
292 /**
293 * Creates a DateTime from an unix timestamp. If the input is empty
294 * NULL is returned.
295 *
296 * @param integer $timestamp
297 * @return DateTime
298 */
299 protected function mapDateTime($timestamp) {
300 if (empty($timestamp)) { // 0 -> NULL !!!
301 return NULL;
302 } else {
303 return new DateTime(date('c', $timestamp));
304 }
305 }
306
307 /**
308 * Fetches a collection of objects related to a property of a parent object
309 *
310 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
311 * @param string $propertyName The name of the proxied property in it's parent
312 * @param mixed $fieldValue The raw field value.
313 * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
314 * @param bool $performLanguageOverlay A flag indication if the related objects should be localized
315 * @return Tx_Extbase_Persistence_LazyObjectStorage|Tx_Extbase_Persistence_QueryResultInterface The result
316 */
317 public function fetchRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = TRUE) {
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 mixed
343 */
344 protected function fetchRelatedEager(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
345 return ($fieldValue === ''
346 ? $this->getEmptyRelationValue($parentObject, $propertyName)
347 : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue)
348 );
349 }
350
351 /**
352 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
353 * @param $propertyName
354 * @return array|NULL
355 */
356 protected function getEmptyRelationValue(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName) {
357 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
358 $relatesToOne = ($columnMap->getTypeOfRelation() == Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_ONE);
359 return $relatesToOne ? NULL : array();
360 }
361
362 /**
363 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
364 * @param string $propertyName
365 * @param string $fieldValue
366 * @return Tx_Extbase_Persistence_QueryResultInterfaces
367 */
368 protected function getNonEmptyRelationValue(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue) {
369 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
370 return $query->execute();
371 }
372
373 /**
374 * Builds and returns the prepared query, ready to be executed.
375 *
376 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
377 * @param string $propertyName
378 * @param string $fieldValue
379 * @return void
380 */
381 protected function getPreparedQuery(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
382 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
383 $type = $this->getType(get_class($parentObject), $propertyName);
384
385 $query = $this->queryFactory->create($type);
386 $query->getQuerySettings()->setRespectStoragePage(FALSE);
387 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
388 if ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
389 if ($columnMap->getChildSortByFieldName() !== NULL) {
390 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
391 }
392 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
393 $query->setSource($this->getSource($parentObject, $propertyName));
394 if ($columnMap->getChildSortByFieldName() !== NULL) {
395 $query->setOrderings(array($columnMap->getChildSortByFieldName() => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
396 }
397 }
398 $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->getRelationTableMatchFields()));
399 return $query;
400 }
401
402 /**
403 * Builds and returns the constraint for multi value properties.
404 *
405 * @param Tx_Extbase_Persistence_QueryInterface $query
406 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
407 * @param string $propertyName
408 * @param string $fieldValue
409 * @param array $relationTableMatchFields
410 * @return Tx_Extbase_Persistence_QOM_ConstraintInterface $constraint
411 */
412 protected function getConstraint(Tx_Extbase_Persistence_QueryInterface $query, Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = array()) {
413 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
414 if ($columnMap->getParentKeyFieldName() !== NULL) {
415 $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
416 if ($columnMap->getParentTableFieldName() !== NULL) {
417 $constraint = $query->logicalAnd(
418 $constraint,
419 $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
420 );
421 }
422 } else {
423 $constraint = $query->in('uid', t3lib_div::intExplode(',', $fieldValue));
424 }
425 if (count($relationTableMatchFields) > 0) {
426 foreach($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
427 $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
428 }
429 }
430 return $constraint;
431 }
432
433 /**
434 * Builds and returns the source to build a join for a m:n relation.
435 *
436 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject
437 * @param string $propertyName
438 * @return Tx_Extbase_Persistence_QOM_SourceInterface $source
439 */
440 protected function getSource(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName) {
441 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
442 $left = $this->qomFactory->selector(NULL, $columnMap->getRelationTableName());
443 $childClassName = $this->getType(get_class($parentObject), $propertyName);
444 $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName());
445 $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid');
446 $source = $this->qomFactory->join(
447 $left,
448 $right,
449 Tx_Extbase_Persistence_QueryInterface::JCR_JOIN_TYPE_INNER,
450 $joinCondition
451 );
452 return $source;
453 }
454
455 /**
456 * Returns the given result as property value of the specified property type.
457 *
458 * @param mixed $result The result could be an object or an ObjectStorage
459 * @param array $propertyMetaData The property meta data
460 * @param Tx_Extbase_Persistence_QueryResultInterface|Tx_Extbase_Persistence_LoadingStrategyInterface $result The result
461 * @return void
462 */
463 public function mapResultToPropertyValue(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $result) {
464 if ($result instanceof Tx_Extbase_Persistence_LoadingStrategyInterface) {
465 $propertyValue = $result;
466 } else {
467 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
468 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
469 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'SplObjectStorage', 'Tx_Extbase_Persistence_ObjectStorage'))) {
470 $elementType = $this->getType(get_class($parentObject), $propertyName);
471 $objects = array();
472 foreach ($result as $value) {
473 $objects[] = $value;
474 }
475
476 if ($propertyMetaData['type'] === 'ArrayObject') {
477 $propertyValue = new ArrayObject($objects);
478 } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
479 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
480 foreach ($objects as $object) {
481 $propertyValue->attach($object);
482 }
483 $propertyValue->_memorizeCleanState();
484 } else {
485 $propertyValue = $objects;
486 }
487 } elseif (strpos($propertyMetaData['type'], '_') !== FALSE) {
488 if (is_object($result) && $result instanceof Tx_Extbase_Persistence_QueryResultInterface) {
489 $propertyValue = $result->getFirst();
490 } else {
491 $propertyValue = $result;
492 }
493 }
494 }
495 return $propertyValue;
496 }
497
498 /**
499 * Counts the number of related objects assigned to a property of a parent object
500 *
501 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The object instance this proxy is part of
502 * @param string $propertyName The name of the proxied property in it's parent
503 * @param mixed $fieldValue The raw field value.
504 */
505 public function countRelated(Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') {
506 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
507 return $query->execute()->count();
508 }
509
510 /**
511 * Delegates the call to the Data Map.
512 * Returns TRUE if the property is persistable (configured in $TCA)
513 *
514 * @param string $className The property name
515 * @param string $propertyName The property name
516 * @return boolean TRUE if the property is persistable (configured in $TCA)
517 */
518 public function isPersistableProperty($className, $propertyName) {
519 $dataMap = $this->getDataMap($className);
520 return $dataMap->isPersistableProperty($propertyName);
521 }
522
523 /**
524 * Returns a data map for a given class name
525 *
526 * @param string $className The class name you want to fetch the Data Map for
527 * @return Tx_Extbase_Persistence_Mapper_DataMap The data map
528 */
529 public function getDataMap($className) {
530 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);
531 if (!isset($this->dataMaps[$className])) {
532 $this->dataMaps[$className] = $this->dataMapFactory->buildDataMap($className);
533 }
534 return $this->dataMaps[$className];
535 }
536
537 /**
538 * Returns the selector (table) name for a given class name.
539 *
540 * @param string $className
541 * @return string The selector name
542 */
543 public function convertClassNameToTableName($className = NULL) {
544 if ($className !== NULL) {
545 $tableName = $this->getDataMap($className)->getTableName();
546 } else {
547 $tableName = strtolower($className);
548 }
549 return $tableName;
550 }
551
552 /**
553 * Returns the column name for a given property name of the specified class.
554 *
555 * @param string $className
556 * @param string $propertyName
557 * @return string The column name
558 */
559 public function convertPropertyNameToColumnName($propertyName, $className = NULL) {
560 if (!empty($className)) {
561 $dataMap = $this->getDataMap($className);
562 if ($dataMap !== NULL) {
563 $columnMap = $dataMap->getColumnMap($propertyName);
564 if ($columnMap !== NULL) {
565 return $columnMap->getColumnName();
566 }
567 }
568 }
569 return t3lib_div::camelCaseToLowerCaseUnderscored($propertyName);
570 }
571
572 /**
573 * Returns the type of a child object.
574 *
575 * @param string $parentClassName The class name of the object this proxy is part of
576 * @param string $propertyName The name of the proxied property in it's parent
577 * @return string The class name of the child object
578 */
579 public function getType($parentClassName, $propertyName) {
580 $propertyMetaData = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
581 if (!empty($propertyMetaData['elementType'])) {
582 $type = $propertyMetaData['elementType'];
583 } elseif (!empty($propertyMetaData['type'])) {
584 $type = $propertyMetaData['type'];
585 } else {
586 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the child object type.', 1251315967);
587 }
588 return $type;
589 }
590
591 }
592 ?>