2 /***************************************************************
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
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.
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
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.
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
26 * A mapper to map database tables configured in $TCA on domain objects.
29 * @subpackage Persistence\Mapper
32 class Tx_Extbase_Persistence_Mapper_DataMapper
implements t3lib_Singleton
{
35 * @var Tx_Extbase_Persistence_IdentityMap
37 protected $identityMap;
40 * @var Tx_Extbase_Reflection_Service
42 protected $reflectionService;
45 * @var Tx_Extbase_Persistence_QOM_QueryObjectModelFactory
47 protected $QomFactory;
50 * @var Tx_Extbase_Persistence_Session
52 protected $persistenceSession;
55 * A reference to the page select object providing methods to perform language and work space overlays
57 * @var t3lib_pageSelect
59 protected $pageSelectObject;
66 protected $dataMaps = array();
69 * @var Tx_Extbase_Persistence_QueryFactoryInterface
71 protected $queryFactory;
74 * The TYPO3 reference index object
78 protected $referenceIndex;
81 * Constructs a new mapper
84 public function __construct() {
85 $this->queryFactory
= t3lib_div
::makeInstance('Tx_Extbase_Persistence_QueryFactory');
89 * Injects the identity map
91 * @param Tx_Extbase_Persistence_IdentityMap $identityMap
94 public function injectIdentityMap(Tx_Extbase_Persistence_IdentityMap
$identityMap) {
95 $this->identityMap
= $identityMap;
99 * Injects the persistence session
101 * @param Tx_Extbase_Persistence_Session $persistenceSession
104 public function injectSession(Tx_Extbase_Persistence_Session
$persistenceSession) {
105 $this->persistenceSession
= $persistenceSession;
109 * Injects the Reflection Service
111 * @param Tx_Extbase_Reflection_Service
114 public function injectReflectionService(Tx_Extbase_Reflection_Service
$reflectionService) {
115 $this->reflectionService
= $reflectionService;
119 * Sets the query object model factory
121 * @param Tx_Extbase_Persistence_QOM_QueryObjectModelFactory $qomFactory
124 public function setQomFactory(Tx_Extbase_Persistence_QOM_QueryObjectModelFactory
$qomFactory) {
125 $this->qomFactory
= $qomFactory;
129 * Maps the (aggregate root) rows and registers them as reconstituted
132 * @param Tx_Extbase_Persistence_RowIteratorInterface $rows
135 public function map($className, Tx_Extbase_Persistence_RowIteratorInterface
$rows) {
137 foreach ($rows as $row) {
138 $objects[] = $this->mapSingleRow($className, $row);
144 * Maps a single node into the object it represents
146 * @param Tx_Extbase_Persistence_RowInterface $node
149 protected function mapSingleRow($className, Tx_Extbase_Persistence_RowInterface
$row) {
150 if ($this->identityMap
->hasIdentifier($row->getValue('uid'), $className)) {
151 $object = $this->identityMap
->getObjectByIdentifier($row->getValue('uid'), $className);
153 $object = $this->createEmptyObject($className);
154 $this->thawProperties($object, $row);
155 $this->identityMap
->registerObject($object, $object->getUid());
156 $object->_memorizeCleanState();
157 $this->persistenceSession
->registerReconstitutedObject($object);
163 * Creates a skeleton of the specified object
165 * @param string $className Name of the class to create a skeleton for
166 * @return object The object skeleton
168 protected function createEmptyObject($className) {
169 // Note: The class_implements() function also invokes autoload to assure that the interfaces
170 // and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
171 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);
172 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
177 * Sets the given properties on the object.
179 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to set properties on
180 * @param Tx_Extbase_Persistence_RowInterface $row
183 protected function thawProperties(Tx_Extbase_DomainObject_DomainObjectInterface
$object, Tx_Extbase_Persistence_RowInterface
$row) {
184 $className = get_class($object);
185 $dataMap = $this->getDataMap($className);
186 $properties = $object->_getProperties();
187 $object->_setProperty('uid', $row->getValue('uid'));
188 foreach ($properties as $propertyName => $propertyValue) {
189 if (!$dataMap->isPersistableProperty($propertyName)) continue;
190 $columnMap = $dataMap->getColumnMap($propertyName);
191 $columnName = $columnMap->getColumnName();
192 $propertyValue = NULL;
194 $propertyMetaData = $this->reflectionService
->getClassSchema($className)->getProperty($propertyName);
195 $propertyType = Tx_Extbase_Persistence_PropertyType
::valueFromType($propertyMetaData['type']);
197 if ($propertyType == Tx_Extbase_Persistence_PropertyType
::UNDEFINED
) {
198 $propertyType = $columnMap->getPropertyType();
201 switch ($propertyType) {
202 case Tx_Extbase_Persistence_PropertyType
::STRING;
203 case Tx_Extbase_Persistence_PropertyType
::DATE
;
204 case Tx_Extbase_Persistence_PropertyType
::LONG
;
205 case Tx_Extbase_Persistence_PropertyType
::DOUBLE;
206 case Tx_Extbase_Persistence_PropertyType
::BOOLEAN
;
207 if ($row->hasValue($columnName)) {
208 $rawPropertyValue = $row->getValue($columnName);
209 $propertyValue = $dataMap->convertFieldValueToPropertyValue($propertyType, $rawPropertyValue);
212 case (Tx_Extbase_Persistence_PropertyType
::REFERENCE
):
213 $propertyValue = $row->getValue($columnName);
214 if (!is_null($propertyValue)) {
215 $fieldValue = $row->getValue($columnName);
216 $result = $this->fetchRelated($object, $propertyName, $fieldValue);
217 $propertyValue = $this->mapResultToPropertyValue($object, $propertyName, $result);
221 // FIXME throw exception
224 $object->_setProperty($propertyName, $propertyValue);
229 * Fetches a collection of objects related to a property of a parent object
231 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject The object instance this proxy is part of
232 * @param string $propertyName The name of the proxied property in it's parent
233 * @param mixed $fieldValue The raw field value.
234 * @param Tx_Extbase_Persistence_Mapper_DataMap $dataMap The corresponding Data Map of the property
235 * @return mixed The result
237 public function fetchRelated(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = TRUE) {
238 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
239 $propertyMetaData = $this->reflectionService
->getClassSchema(get_class($parentObject))->getProperty($propertyName);
240 if ($enableLazyLoading === TRUE && ($propertyMetaData['lazy'] ||
($columnMap->getLoadingStrategy() !== Tx_Extbase_Persistence_Mapper_ColumnMap
::STRATEGY_EAGER
))) {
241 if (($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') ||
($columnMap->getLoadingStrategy() === Tx_Extbase_Persistence_Mapper_ColumnMap
::STRATEGY_LAZY_STORAGE
)) {
242 $result = t3lib_div
::makeInstance('Tx_Extbase_Persistence_LazyObjectStorage', $parentObject, $propertyName, $fieldValue);
244 $result = t3lib_div
::makeInstance('Tx_Extbase_Persistence_LazyLoadingProxy', $parentObject, $propertyName, $fieldValue);
247 $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
253 * Fetches the related objects from the storage backend.
255 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject The object instance this proxy is part of
256 * @param string $propertyName The name of the proxied property in it's parent
257 * @param mixed $fieldValue The raw field value.
260 protected function fetchRelatedEager(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName, $fieldValue = '') {
261 if ($fieldValue === '') return array();
262 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
263 return $query->execute();
267 * Builds and returns the prepared query, ready to be executed.
269 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject
270 * @param string $propertyName
271 * @param string $fieldValue
274 protected function getPreparedQuery(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName, $fieldValue = '') {
275 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
276 $queryFactory = t3lib_div
::makeInstance('Tx_Extbase_Persistence_QueryFactory');
277 $parentKeyFieldName = $columnMap->getParentKeyFieldName();
278 $childSortByFieldName = $columnMap->getChildSortByFieldName();
279 if ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_ONE
) {
280 $query = $queryFactory->create($this->getType($parentObject, $propertyName));
281 if (isset($parentKeyFieldName)) {
282 $query->matching($query->equals($parentKeyFieldName, $parentObject->getUid()));
284 $query->matching($query->withUid(intval($fieldValue)));
286 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_MANY
) {
287 $query = $queryFactory->create($this->getElementType($parentObject, $propertyName));
288 // TODO: This is an ugly hack, just ignoring the storage page state from here. Actually, the query settings would have to be passed into the DataMapper, so we can respect
289 // enableFields and storage page settings.
290 $query->getQuerySettings()->setRespectStoragePage(FALSE);
291 if (!empty($childSortByFieldName)) {
292 $query->setOrderings(array($childSortByFieldName => Tx_Extbase_Persistence_QueryInterface
::ORDER_ASCENDING
));
294 if (isset($parentKeyFieldName)) {
295 $query->matching($query->equals($parentKeyFieldName, $parentObject->getUid()));
297 $query->matching($query->equals('uid', t3lib_div
::intExplode(',', $fieldValue)));
299 } elseif ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_AND_BELONGS_TO_MANY
) {
300 $query = $queryFactory->create($this->getElementType($parentObject, $propertyName));
301 // TODO: This is an ugly hack, just ignoring the storage page state from here. Actually, the query settings would have to be passed into the DataMapper, so we can respect
302 // enableFields and storage page settings.
303 $query->getQuerySettings()->setRespectStoragePage(FALSE);
304 $relationTableName = $columnMap->getRelationTableName();
305 $left = $this->qomFactory
->selector(NULL, $relationTableName);
306 $childClassName = $this->getElementType($parentObject, $propertyName);
307 $childTableName = $columnMap->getChildTableName();
308 $right = $this->qomFactory
->selector($childClassName, $childTableName);
309 $joinCondition = $this->qomFactory
->equiJoinCondition($relationTableName, $columnMap->getChildKeyFieldName(), $childTableName, 'uid');
310 $source = $this->qomFactory
->join(
313 Tx_Extbase_Persistence_QOM_QueryObjectModelConstantsInterface
::JCR_JOIN_TYPE_INNER
,
317 $query->setSource($source);
318 if (!empty($childSortByFieldName)) {
319 $query->setOrderings(array($relationTableName . '.' . $childSortByFieldName => Tx_Extbase_Persistence_QueryInterface
::ORDER_ASCENDING
));
322 // attempt to support MM_match_fields
323 $conditions = $query->equals($parentKeyFieldName, $parentObject->getUid());
325 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
326 if (count($relationTableMatchFields)) {
327 foreach($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
328 $relationMatchCondition = $query->equals($relationTableName . '.' . $relationTableMatchFieldName, $relationTableMatchFieldValue);
329 $conditions = $query->logicalAnd($conditions, $relationMatchCondition);
332 $query->matching($conditions);
335 throw new Tx_Extbase_Persistence_Exception('Could not determine type of relation.', 1252502725);
341 * Returns the given result as property value of the specified property type.
343 * @param mixed $result The result could be an object or an ObjectStorage
344 * @param array $propertyMetaData The property meta data
347 public function mapResultToPropertyValue(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName, $result) {
348 if ($result instanceof Tx_Extbase_Persistence_LoadingStrategyInterface
) {
349 $propertyValue = $result;
351 $propertyMetaData = $this->reflectionService
->getClassSchema(get_class($parentObject))->getProperty($propertyName);
352 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
353 if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'Tx_Extbase_Persistence_ObjectStorage'))) {
354 $elementType = $this->getElementType($parentObject, $propertyName);
356 foreach ($result as $value) {
360 if ($propertyMetaData['type'] === 'ArrayObject') {
361 $propertyValue = new ArrayObject($objects);
362 } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
363 $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
364 foreach ($objects as $object) {
365 $propertyValue->attach($object);
368 $propertyValue = $objects;
370 } elseif (strpos($propertyMetaData['type'], '_') !== FALSE) {
371 $propertyValue = current($result);
374 return $propertyValue;
378 * Counts the number of related objects assigned to a property of a parent object
380 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject The object instance this proxy is part of
381 * @param string $propertyName The name of the proxied property in it's parent
382 * @param mixed $fieldValue The raw field value.
384 public function countRelated(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName, $fieldValue = '') {
385 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
386 return $query->count();
390 * Delegates the call to the Data Map.
391 * Returns TRUE if the property is persistable (configured in $TCA)
393 * @param string $className The property name
394 * @param string $propertyName The property name
395 * @return boolean TRUE if the property is persistable (configured in $TCA)
397 public function isPersistableProperty($className, $propertyName) {
398 $dataMap = $this->getDataMap($className);
399 return $dataMap->isPersistableProperty($propertyName);
403 * Returns a data map for a given class name
405 * @param string $className The class name you want to fetch the Data Map for
406 * @return Tx_Extbase_Persistence_Mapper_DataMap The data map
408 public function getDataMap($className) {
409 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);
410 if (!isset($this->dataMaps
[$className])) {
411 // FIXME This is too expensive for table name aliases -> implement a DataMapBuilder (knowing the aliases defined in $TCA)
412 $columnMapping = array();
414 $extbaseSettings = Tx_Extbase_Dispatcher
::getExtbaseFrameworkConfiguration();
415 if (is_array($extbaseSettings['persistence']['classes'][$className])) {
416 $persistenceSettings = $extbaseSettings['persistence']['classes'][$className];
417 if (is_string($persistenceSettings['mapping']['tableName']) && strlen($persistenceSettings['mapping']['tableName']) > 0) {
418 $tableName = $persistenceSettings['mapping']['tableName'];
420 if (is_array($persistenceSettings['mapping']['columns'])) {
421 $columnMapping = $persistenceSettings['mapping']['columns'];
423 } elseif (class_exists($className)) {
424 foreach (class_parents($className) as $parentClassName) {
425 $persistenceSettings = $extbaseSettings['persistence']['classes'][$parentClassName];
426 if (is_array($persistenceSettings)) {
427 if (is_string($persistenceSettings['mapping']['tableName']) && strlen($persistenceSettings['mapping']['tableName']) > 0) {
428 $tableName = $persistenceSettings['mapping']['tableName'];
430 if (is_array($persistenceSettings['mapping']['columns'])) {
431 $columnMapping = $persistenceSettings['mapping']['columns'];
437 throw new Tx_Extbase_Persistence_Exception('Could not determine a Data Map for given class name.', 1256067130);
440 $dataMap = new Tx_Extbase_Persistence_Mapper_DataMap($className, $tableName, $columnMapping);
441 $this->dataMaps
[$className] = $dataMap;
443 return $this->dataMaps
[$className];
447 * Returns the selector (table) name for a given class name.
449 * @param string $className
450 * @return string The selector name
452 public function convertClassNameToTableName($className) {
453 return $this->getDataMap($className)->getTableName();
457 * Returns the column name for a given property name of the specified class.
459 * @param string $className
460 * @param string $propertyName
461 * @return string The column name
463 public function convertPropertyNameToColumnName($propertyName, $className = '') {
464 if (!empty($className)) {
465 $dataMap = $this->getDataMap($className);
466 if ($dataMap !== NULL) {
467 $columnMap = $dataMap->getColumnMap($propertyName);
468 if ($columnMap !== NULL) {
469 return $columnMap->getColumnName();
473 return Tx_Extbase_Utility_Extension
::convertCamelCaseToLowerCaseUnderscored($propertyName);
477 * Returns the type of a child object.
479 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject The object instance this proxy is part of
480 * @param string $propertyName The name of the proxied property in it's parent
481 * @return string The class name of the child object
483 protected function getType(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName) {
484 $propertyMetaData = $this->reflectionService
->getClassSchema(get_class($parentObject))->getProperty($propertyName);
485 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
486 $childClassName = $columnMap->getChildClassName();
487 if (!empty($childClassName)) {
488 $elementType = $childClassName;
489 } elseif (!empty($propertyMetaData['type'])) {
490 $elementType = $propertyMetaData['type'];
492 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the child object object type.', 1251315967);
498 * Returns the type of the elements inside an ObjectStorage or array.
500 * @param Tx_Extbase_DomainObject_AbstractEntity $parentObject The object instance this proxy is part of
501 * @param string $propertyName The name of the proxied property in it's parent
502 * @return string The class name of the elements inside an ObjectStorage
504 protected function getElementType(Tx_Extbase_DomainObject_AbstractEntity
$parentObject, $propertyName) {
505 $propertyMetaData = $this->reflectionService
->getClassSchema(get_class($parentObject))->getProperty($propertyName);
506 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
507 $childClassName = $columnMap->getChildClassName();
508 if (!empty($childClassName)) {
509 $elementType = $childClassName;
510 } elseif (!empty($propertyMetaData['elementType'])) {
511 $elementType = $propertyMetaData['elementType'];
513 throw new Tx_Extbase_Persistence_Exception_UnexpectedTypeException('Could not determine the type of the contained objects.', 1251315966);