2 /***************************************************************
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
8 * This class is a backport of the corresponding class of FLOW3.
9 * All credits go to the v5 team.
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
29 * A persistence backend. This backend maps objects to the relational model of the storage backend.
30 * It persists all added, removed and changed objects.
33 * @subpackage Persistence
34 * @version $Id: Backend.php 2183 2009-04-24 14:28:37Z k-fish $
36 class Tx_Extbase_Persistence_Backend
implements Tx_Extbase_Persistence_BackendInterface
, t3lib_Singleton
{
39 * @var Tx_Extbase_Persistence_Session
44 * @var Tx_Extbase_Persistence_ObjectStorage
46 protected $aggregateRootObjects;
49 * @var Tx_Extbase_Persistence_IdentityMap
51 protected $identityMap;
54 * @var Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface
56 protected $QOMFactory;
59 * @var Tx_Extbase_Persistence_ValueFactoryInterface
61 protected $valueFactory;
64 * @var Tx_Extbase_Persistence_Storage_BackendInterface
66 protected $storageBackend;
69 * @var Tx_Extbase_Persistence_DataMapper
71 protected $dataMapper;
74 * The TYPO3 reference index object
78 protected $referenceIndex;
81 * Constructs the backend
83 * @param Tx_Extbase_Persistence_Session $session The persistence session used to persist data
85 public function __construct(Tx_Extbase_Persistence_Session
$session, Tx_Extbase_Persistence_Storage_BackendInterface
$storageBackend) {
86 $this->session
= $session;
87 $this->storageBackend
= $storageBackend;
88 $this->referenceIndex
= t3lib_div
::makeInstance('t3lib_refindex');
89 $this->aggregateRootObjects
= new Tx_Extbase_Persistence_ObjectStorage();
90 $this->persistenceBackend
= $GLOBALS['TYPO3_DB']; // FIXME This is just an intermediate solution
94 * Injects the DataMapper to map nodes to objects
96 * @param Tx_Extbase_Persistence_Mapper_DataMapper $dataMapper
99 public function injectDataMapper(Tx_Extbase_Persistence_Mapper_DataMapper
$dataMapper) {
100 $this->dataMapper
= $dataMapper;
104 * Injects the identity map
106 * @param Tx_Extbase_Persistence_IdentityMap $identityMap
110 public function injectIdentityMap(Tx_Extbase_Persistence_IdentityMap
$identityMap) {
111 $this->identityMap
= $identityMap;
115 * Injects the QueryObjectModelFactory
117 * @param Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface $dataMapper
120 public function injectQOMFactory(Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface
$QOMFactory) {
121 $this->QOMFactory
= $QOMFactory;
125 * Injects the ValueFactory
127 * @param Tx_Extbase_Persistence_ValueFactoryInterface $valueFactory
130 public function injectValueFactory(Tx_Extbase_Persistence_ValueFactoryInterface
$valueFactory) {
131 $this->valueFactory
= $valueFactory;
135 * Returns the repository session
137 * @return Tx_Extbase_Persistence_Session
139 public function getSession() {
140 return $this->session
;
144 * Returns the Data Mapper
146 * @return Tx_Extbase_Persistence_Mapper_DataMapper
148 public function getDataMapper() {
149 return $this->dataMapper
;
153 * Returns the current QOM factory
155 * @return Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface
158 public function getQOMFactory() {
159 return $this->QOMFactory
;
163 * Returns the current value factory
165 * @return Tx_Extbase_Persistence_ValueFactoryInterface
168 public function getValueFactory() {
169 return $this->valueFactory
;
173 * Returns the current identityMap
175 * @return Tx_Extbase_Persistence_IdentityMap
178 public function getIdentityMap() {
179 return $this->identityMap
;
183 * Returns the (internal) identifier for the object, if it is known to the
184 * backend. Otherwise NULL is returned.
186 * @param object $object
187 * @return string The identifier for the object if it is known, or NULL
189 public function getIdentifierByObject($object) {
190 if ($this->identityMap
->hasObject($object)) {
191 return $this->identityMap
->getIdentifierByObject($object);
198 * Checks if the given object has ever been persisted.
200 * @param object $object The object to check
201 * @return boolean TRUE if the object is new, FALSE if the object exists in the repository
203 public function isNewObject($object) {
204 return ($this->getIdentifierByObject($object) === NULL);
208 * Replaces the given object by the second object.
210 * This method will unregister the existing object at the identity map and
211 * register the new object instead. The existing object must therefore
212 * already be registered at the identity map which is the case for all
213 * reconstituted objects.
215 * The new object will be identified by the uid which formerly belonged
216 * to the existing object. The existing object looses its uid.
218 * @param object $existingObject The existing object
219 * @param object $newObject The new object
222 public function replaceObject($existingObject, $newObject) {
223 $existingUid = $this->getIdentifierByObject($existingObject);
224 if ($existingUid === NULL) throw new Tx_Extbase_Persistence_Exception_UnknownObject('The given object is unknown to this persistence backend.', 1238070163);
226 $this->identityMap
->unregisterObject($existingObject);
227 $this->identityMap
->registerObject($newObject, $existingUid);
231 * Sets the aggregate root objects
233 * @param Tx_Extbase_Persistence_ObjectStorage $objects
236 public function setAggregateRootObjects(Tx_Extbase_Persistence_ObjectStorage
$objects) {
237 $this->aggregateRootObjects
= $objects;
241 * Sets the deleted objects
243 * @param Tx_Extbase_Persistence_ObjectStorage $objects
246 public function setDeletedObjects(Tx_Extbase_Persistence_ObjectStorage
$objects) {
247 $this->deletedObjects
= $objects;
251 * Commits the current persistence session.
255 public function commit() {
256 $this->persistObjects();
257 $this->processDeletedObjects();
261 * Traverse and persist all aggregate roots and their object graph.
265 protected function persistObjects() {
266 foreach ($this->aggregateRootObjects
as $object) {
267 $this->persistObject($object);
272 * Persists an object (instert, update) and its related objects (instert, update, delete).
274 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be inserted
275 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The parent object
276 * @param string $parentPropertyName The name of the property the object is stored in
279 protected function persistObject(Tx_Extbase_DomainObject_DomainObjectInterface
$object, $parentObject = NULL, $parentPropertyName = NULL) {
281 $queuedObjects = array();
282 $className = get_class($object);
283 $dataMap = $this->dataMapper
->getDataMap($className);
285 if ($object instanceof Tx_Extbase_DomainObject_AbstractValueObject
) {
286 $this->mapAlreadyPersistedValueObject($object);
289 $properties = $object->_getProperties();
290 // Fill up $row[$columnName] array with changed values which need to be stored
291 foreach ($properties as $propertyName => $propertyValue) {
292 if (!$dataMap->isPersistableProperty($propertyName) ||
($propertyValue instanceof Tx_Extbase_Persistence_LazyLoadingProxy
)) {
296 $columnMap = $dataMap->getColumnMap($propertyName);
297 if ($object->_isNew() ||
$object->_isDirty($propertyName)) {
298 if ($columnMap->isRelation()) {
299 $this->persistRelations($object, $propertyName, $propertyValue, $columnMap, $queuedObjects, $row);
301 // We have not a relation, this means it is a scalar (like a string or interger value) or an object
302 $row[$columnMap->getColumnName()] = $dataMap->convertPropertyValueToFieldValue($propertyValue);
305 } // end property iteration for loop
307 // The state of the Object has to be stored in a local variable because $object->_isNew() will return FALSE after
308 // the object was inserted. We need the initial state here.
309 $objectIsNew = $object->_isNew();
310 if ($objectIsNew === TRUE) {
311 $this->insertObject($object, $parentObject, $parentPropertyName, $row);
312 } elseif ($object->_isDirty()) {
313 $this->updateObject($object, $parentObject, $parentPropertyName, $row);
316 // SK: Where does $queueChildObjects come from? Do we need the code below?
317 $objectHasToBeUpdated = $this->processQueuedChildObjects($object, $queuedObjects, $row);
318 if ($objectHasToBeUpdated === TRUE) {
319 // TODO Check if this can be merged with the first update
320 $this->updateObject($object, $parentObject, $parentPropertyName, $row);
323 // SK: I need to check the code below more thoroughly
324 if ($parentObject instanceof Tx_Extbase_DomainObject_DomainObjectInterface
&& !empty($parentPropertyName)) {
325 $parentDataMap = $this->dataMapper
->getDataMap(get_class($parentObject));
326 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
327 if (($parentColumnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_AND_BELONGS_TO_MANY
)) {
328 $this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName);
332 $this->identityMap
->registerObject($object, $object->getUid());
333 $object->_memorizeCleanState();
337 * Persists a relation. Objects of a 1:n or m:n relation are queued and processed with the parent object. A 1:1 relation
338 * gets persisted immediately. Objects which were removed from the property were deleted immediately, too.
340 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be inserted
341 * @param string $propertyName The name of the property the related objects are stored in
342 * @param mixed $propertyValue The property value (an array of Domain Objects, ObjectStorage holding Domain Objects or a Domain Object itself)
345 protected function persistRelations(Tx_Extbase_DomainObject_DomainObjectInterface
$object, $propertyName, $propertyValue, Tx_Extbase_Persistence_Mapper_ColumnMap
$columnMap, &$queuedObjects, &$row) {
346 $columnName = $columnMap->getColumnName();
347 if (($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_MANY
) ||
($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_AND_BELONGS_TO_MANY
)) {
348 if (is_array($propertyValue) ||
$propertyValue instanceof ArrayAccess
) {
349 foreach ($propertyValue as $relatedObject) {
350 $queuedObjects[$propertyName][] = $relatedObject;
352 $row[$columnName] = count($propertyValue); // Will be overwritten if the related objects are referenced by a comma separated list
353 foreach ($this->getDeletedChildObjects($object, $propertyName) as $deletedObject) {
354 $this->deleteObject($deletedObject, $object, $propertyName, TRUE, FALSE);
357 } elseif ($propertyValue instanceof Tx_Extbase_DomainObject_DomainObjectInterface
) {
358 // TODO Handle Value Objects different
359 if ($propertyValue->_isNew() ||
$propertyValue->_isDirty()) {
360 $this->persistObject($propertyValue);
362 $row[$columnName] = $propertyValue->getUid();
367 * Returns the deleted objects determined by a comparison of the clean property value
368 * with the actual property value.
370 * @param Tx_Extbase_DomainObject_AbstractEntity $object The object to be insterted in the storage
371 * @param string $parentPropertyName The name of the property
372 * @return array An array of deleted objects
374 protected function getDeletedChildObjects(Tx_Extbase_DomainObject_AbstractEntity
$object, $propertyName) {
375 $deletedObjects = array();
376 if (!$object->_isNew()) {
377 $cleanProperties = $object->_getCleanProperties();
378 $cleanPropertyValue = $cleanProperties[$propertyName];
379 $propertyValue = $object->_getProperty($propertyName);
380 if ($cleanPropertyValue instanceof Tx_Extbase_Persistence_ObjectStorage
) {
381 $cleanPropertyValue = $cleanPropertyValue->toArray();
383 if ($propertyValue instanceof Tx_Extbase_Persistence_ObjectStorage
) {
384 $propertyValue = $propertyValue->toArray();
386 $deletedObjects = array_diff($cleanPropertyValue, $propertyValue);
389 return $deletedObjects;
393 * This function processes the queued child objects to be persisted. The queue is build while looping over the
394 * collection of Domain Objects stored in a object property.
396 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object holding the collection
397 * @param array $queuedObjects The queued child objects
398 * @param array $row The row to be inseted or updated in the database. Passed as reference.
399 * @return boolean TRUE if the object holding the collection has to be updated; otherwise FALSE
401 protected function processQueuedChildObjects(Tx_Extbase_DomainObject_DomainObjectInterface
$object, array $queuedChildObjects, array &$row) {
402 $objectHasToBeUpdated = FALSE;
403 $className = get_class($object);
404 $dataMap = $this->dataMapper
->getDataMap($className);
405 foreach ($queuedChildObjects as $propertyName => $childObjects) {
406 $childPidArray = array();
407 $columnMap = $dataMap->getColumnMap($propertyName);
408 foreach($childObjects as $childObject) {
409 $this->persistObject($childObject, $object, $propertyName);
410 if ($childObject instanceof Tx_Extbase_DomainObject_DomainObjectInterface
) {
411 $childPidArray[] = (int)$childObject->getUid();
414 if ($columnMap->getParentKeyFieldName() === NULL) { // TRUE: We have to generate a comma separated list stored in the field
415 $row[$propertyName] = implode(',', $childPidArray);
416 $objectHasToBeUpdated = TRUE;
419 return $objectHasToBeUpdated;
423 * Tests, if the given Value Object already exists in the storage backend. If so, it maps the uid
424 * to the given object.
426 * @param Tx_Extbase_DomainObject_AbstractValueObject $object The object to be tested
428 protected function mapAlreadyPersistedValueObject(Tx_Extbase_DomainObject_AbstractValueObject
$object) {
429 $dataMap = $this->dataMapper
->getDataMap(get_class($object));
430 $properties = $object->_getProperties();
431 $result = $this->storageBackend
->hasValueObject($properties, $dataMap);
432 if ($result !== FALSE) {
433 $object->_setProperty('uid', $result);
438 * Inserts an object in the storage
440 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
441 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
442 * @param string|NULL $parentPropertyName The name of the property
443 * @param array $row The $row
445 protected function insertObject(Tx_Extbase_DomainObject_DomainObjectInterface
$object, Tx_Extbase_DomainObject_AbstractEntity
$parentObject = NULL, $parentPropertyName = NULL, array &$row) {
446 $className = get_class($object);
447 $dataMap = $this->dataMapper
->getDataMap($className);
448 $tableName = $dataMap->getTableName();
449 $this->addCommonFieldsToRow($object, $parentObject, $parentPropertyName, $row);
450 $uid = $this->storageBackend
->addRow(
454 $object->_setProperty('uid', $uid);
455 $this->referenceIndex
->updateRefIndexTable($tableName, $uid);
459 * Inserts mm-relation into a relation table
461 * @param Tx_Extbase_DomainObject_DomainObjectInterface $relatedObject The related object
462 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The parent object
463 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
466 protected function insertRelationInRelationtable(Tx_Extbase_DomainObject_DomainObjectInterface
$relatedObject, Tx_Extbase_DomainObject_DomainObjectInterface
$parentObject, $parentPropertyName) {
467 $dataMap = $this->dataMapper
->getDataMap(get_class($parentObject));
468 $columnMap = $dataMap->getColumnMap($parentPropertyName);
470 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
471 $columnMap->getChildKeyFieldName() => (int)$relatedObject->getUid(),
472 'tablenames' => $columnMap->getChildTableName(),
473 'sorting' => 9999 // TODO sorting of mm table items
475 $res = $this->storageBackend
->addRow(
476 $columnMap->getRelationTableName(),
483 * Updates a given object in the storage
485 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
486 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
487 * @param string|NULL $parentPropertyName The name of the property
488 * @param array $row The $row
490 protected function updateObject(Tx_Extbase_DomainObject_DomainObjectInterface
$object, $parentObject = NULL, $parentPropertyName = NULL, array &$row) {
491 $className = get_class($object);
492 $dataMap = $this->dataMapper
->getDataMap($className);
493 $tableName = $dataMap->getTableName();
494 $this->addCommonFieldsToRow($object, $parentObject, $parentPropertyName, $row);
495 $uid = $object->getUid();
497 $res = $this->storageBackend
->updateRow(
501 $this->referenceIndex
->updateRefIndexTable($tableName, $uid);
506 * Returns a table row to be inserted or updated in the database
508 * @param Tx_Extbase_Persistence_Mapper_DataMap $dataMap The appropriate data map representing a database table
509 * @param array $properties The properties of the object
510 * @return array A single row to be inserted in the database
512 protected function addCommonFieldsToRow(Tx_Extbase_DomainObject_DomainObjectInterface
$object, $parentObject = NULL, $parentPropertyName = NULL, array &$row) {
513 $className = get_class($object);
514 $dataMap = $this->dataMapper
->getDataMap($className);
515 if ($dataMap->hasCreationDateColumn() && $object->_isNew()) {
516 $row[$dataMap->getCreationDateColumnName()] = $GLOBALS['EXEC_TIME'];
518 if ($dataMap->hasTimestampColumn()) {
519 $row[$dataMap->getTimestampColumnName()] = $GLOBALS['EXEC_TIME'];
522 if ($object->_isNew() && $dataMap->hasPidColumn() && !isset($row['pid'])) {
523 $row['pid'] = $this->determineStoragePageIdForNewRecord($object);
526 if ($parentObject instanceof Tx_Extbase_DomainObject_DomainObjectInterface
&& !empty($parentPropertyName)) {
527 $parentDataMap = $this->dataMapper
->getDataMap(get_class($parentObject));
528 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
529 // FIXME This is a hacky solution
530 if ($parentColumnMap->getTypeOfRelation() !== Tx_Extbase_Persistence_Mapper_ColumnMap
::RELATION_HAS_AND_BELONGS_TO_MANY
) {
531 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
532 if ($parentKeyFieldName !== NULL) {
533 $row[$parentKeyFieldName] = $parentObject->getUid();
535 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
536 if ($parentTableFieldName !== NULL) {
537 $row[$parentTableFieldName] = $parentDataMap->getTableName();
544 * Determine the storage page ID for a given NEW record
546 * This does the following:
547 * - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
548 * - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
550 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object
551 * @return int the storage Page ID where the object should be stored
553 protected function determineStoragePageIdForNewRecord(Tx_Extbase_DomainObject_DomainObjectInterface
$object) {
554 $className = get_class($object);
555 $extbaseSettings = Tx_Extbase_Dispatcher
::getSettings();
557 if (isset($extbaseSettings['classes'][$className]) && !empty($extbaseSettings['classes'][$className]['newRecordStoragePid'])) {
558 return (int)$extbaseSettings['classes'][$className]['newRecordStoragePid'];
560 $storagePidList = t3lib_div
::intExplode(',', $extbaseSettings['storagePid']);
561 return (int) $storagePidList[0];
566 * Iterate over deleted aggregate root objects and process them
570 protected function processDeletedObjects() {
571 foreach ($this->deletedObjects
as $object) {
572 $this->deleteObject($object);
573 $this->identityMap
->unregisterObject($object);
575 $this->deletedObjects
= new Tx_Extbase_Persistence_ObjectStorage();
579 * Deletes an object, it's 1:n related objects, and the m:n relations in relation tables (but not the m:n related objects!)
581 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
582 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
583 * @param string|NULL $parentPropertyName The name of the property
584 * @param bool $markAsDeleted Shold we only mark the row as deleted instead of deleting (TRUE by default)?
587 protected function deleteObject(Tx_Extbase_DomainObject_DomainObjectInterface
$object, $parentObject = NULL, $parentPropertyName = NULL, $markAsDeleted = TRUE) {
588 // TODO Implement recursive deletions
589 $dataMap = $this->dataMapper
->getDataMap(get_class($object));
590 $tableName = $dataMap->getTableName();
591 if (($markAsDeleted === TRUE) && $dataMap->hasDeletedColumn()) {
592 $deletedColumnName = $dataMap->getDeletedColumnName();
593 $res = $this->storageBackend
->updateRow(
596 'uid' => $object->getUid(),
597 $deletedColumnName => 1
601 $res = $this->storageBackend
->removeRow(
606 $this->referenceIndex
->updateRefIndexTable($tableName, $uid);
610 * Delegates the call to the Data Map.
611 * Returns TRUE if the property is persistable (configured in $TCA)
613 * @param string $className The property name
614 * @param string $propertyName The property name
615 * @return boolean TRUE if the property is persistable (configured in $TCA)
617 public function isPersistableProperty($className, $propertyName) {
618 $dataMap = $this->dataMapper
->getDataMap($className);
619 return $dataMap->isPersistableProperty($propertyName);