Extbase:
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Backend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
6 * All rights reserved
7 *
8 * This class is a backport of the corresponding class of FLOW3.
9 * All credits go to the v5 team.
10 *
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.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 *
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.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27
28 /**
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.
31 *
32 * @package Extbase
33 * @subpackage Persistence
34 * @version $Id: Backend.php 2183 2009-04-24 14:28:37Z k-fish $
35 */
36 class Tx_Extbase_Persistence_Backend implements Tx_Extbase_Persistence_BackendInterface, t3lib_Singleton {
37
38 /**
39 * @var Tx_Extbase_Persistence_Session
40 */
41 protected $session;
42
43 /**
44 * @var Tx_Extbase_Persistence_ObjectStorage
45 */
46 protected $aggregateRootObjects;
47
48 /**
49 * @var Tx_Extbase_Persistence_IdentityMap
50 **/
51 protected $identityMap;
52
53 /**
54 * @var Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface
55 */
56 protected $QOMFactory;
57
58 /**
59 * @var Tx_Extbase_Persistence_ValueFactoryInterface
60 */
61 protected $valueFactory;
62
63 /**
64 * @var Tx_Extbase_Persistence_Storage_BackendInterface
65 */
66 protected $storageBackend;
67
68 /**
69 * @var Tx_Extbase_Persistence_DataMapper
70 */
71 protected $dataMapper;
72
73 /**
74 * The TYPO3 reference index object
75 *
76 * @var t3lib_refindex
77 **/
78 protected $referenceIndex;
79
80 /**
81 * Constructs the backend
82 *
83 * @param Tx_Extbase_Persistence_Session $session The persistence session used to persist data
84 */
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
91 }
92
93 /**
94 * Injects the DataMapper to map nodes to objects
95 *
96 * @param Tx_Extbase_Persistence_Mapper_DataMapper $dataMapper
97 * @return void
98 */
99 public function injectDataMapper(Tx_Extbase_Persistence_Mapper_DataMapper $dataMapper) {
100 $this->dataMapper = $dataMapper;
101 }
102
103 /**
104 * Injects the identity map
105 *
106 * @param Tx_Extbase_Persistence_IdentityMap $identityMap
107 * @return void
108 * @internal
109 */
110 public function injectIdentityMap(Tx_Extbase_Persistence_IdentityMap $identityMap) {
111 $this->identityMap = $identityMap;
112 }
113
114 /**
115 * Injects the QueryObjectModelFactory
116 *
117 * @param Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface $dataMapper
118 * @return void
119 */
120 public function injectQOMFactory(Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface $QOMFactory) {
121 $this->QOMFactory = $QOMFactory;
122 }
123
124 /**
125 * Injects the ValueFactory
126 *
127 * @param Tx_Extbase_Persistence_ValueFactoryInterface $valueFactory
128 * @return void
129 */
130 public function injectValueFactory(Tx_Extbase_Persistence_ValueFactoryInterface $valueFactory) {
131 $this->valueFactory = $valueFactory;
132 }
133
134 /**
135 * Returns the repository session
136 *
137 * @return Tx_Extbase_Persistence_Session
138 */
139 public function getSession() {
140 return $this->session;
141 }
142
143 /**
144 * Returns the current QOM factory
145 *
146 * @return Tx_Extbase_Persistence_QOM_QueryObjectModelFactoryInterface
147 * @internal
148 */
149 public function getQOMFactory() {
150 return $this->QOMFactory;
151 }
152
153 /**
154 * Returns the current value factory
155 *
156 * @return Tx_Extbase_Persistence_ValueFactoryInterface
157 * @internal
158 */
159 public function getValueFactory() {
160 return $this->valueFactory;
161 }
162
163 /**
164 * Returns the current identityMap
165 *
166 * @return Tx_Extbase_Persistence_IdentityMap
167 * @internal
168 */
169 public function getIdentityMap() {
170 return $this->identityMap;
171 }
172
173 /**
174 * Returns the (internal) identifier for the object, if it is known to the
175 * backend. Otherwise NULL is returned.
176 *
177 * @param object $object
178 * @return string The identifier for the object if it is known, or NULL
179 */
180 public function getUidByObject($object) {
181 if ($this->identityMap->hasObject($object)) {
182 return $this->identityMap->getUidByObject($object);
183 } else {
184 return NULL;
185 }
186 }
187
188 /**
189 * Checks if the given object has ever been persisted.
190 *
191 * @param object $object The object to check
192 * @return boolean TRUE if the object is new, FALSE if the object exists in the repository
193 */
194 public function isNewObject($object) {
195 return ($this->getUidByObject($object) === NULL);
196 }
197
198 /**
199 * Replaces the given object by the second object.
200 *
201 * This method will unregister the existing object at the identity map and
202 * register the new object instead. The existing object must therefore
203 * already be registered at the identity map which is the case for all
204 * reconstituted objects.
205 *
206 * The new object will be identified by the uid which formerly belonged
207 * to the existing object. The existing object looses its uid.
208 *
209 * @param object $existingObject The existing object
210 * @param object $newObject The new object
211 * @return void
212 */
213 public function replaceObject($existingObject, $newObject) {
214 $existingUid = $this->getUidByObject($existingObject);
215 if ($existingUid === NULL) throw new Tx_Extbase_Persistence_Exception_UnknownObject('The given object is unknown to this persistence backend.', 1238070163);
216
217 $this->identityMap->unregisterObject($existingObject);
218 $this->identityMap->registerObject($newObject, $existingUid);
219 }
220
221 /**
222 * Sets the aggregate root objects
223 *
224 * @param Tx_Extbase_Persistence_ObjectStorage $objects
225 * @return void
226 */
227 public function setAggregateRootObjects(Tx_Extbase_Persistence_ObjectStorage $objects) {
228 $this->aggregateRootObjects = $objects;
229 }
230
231 /**
232 * Sets the deleted objects
233 *
234 * @param Tx_Extbase_Persistence_ObjectStorage $objects
235 * @return void
236 */
237 public function setDeletedObjects(Tx_Extbase_Persistence_ObjectStorage $objects) {
238 $this->deletedObjects = $objects;
239 }
240
241 /**
242 * Commits the current persistence session.
243 *
244 * @return void
245 */
246 public function commit() {
247 $this->persistObjects();
248 $this->processDeletedObjects();
249 }
250
251 /**
252 * Traverse and persist all aggregate roots and their object graph.
253 *
254 * @return void
255 */
256 protected function persistObjects() {
257 foreach ($this->aggregateRootObjects as $object) {
258 $this->persistObject($object);
259 }
260 }
261
262 /**
263 * Inserts an objects corresponding row into the database. If the object is a value object an
264 * existing instance will be looked up.
265 *
266 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be inserted
267 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The parent object
268 * @param string $parentPropertyName The name of the property the object is stored in
269 * @return void
270 */
271 protected function persistObject($object, $parentObject = NULL, $parentPropertyName = NULL, $processQueue = TRUE) {
272 $row = array();
273 $queue = array();
274 $className = get_class($object);
275 $dataMap = $this->dataMapper->getDataMap($className);
276 $properties = $object->_getProperties();
277
278 if ($object instanceof Tx_Extbase_DomainObject_AbstractValueObject) {
279 $this->checkForAlreadyPersistedValueObject($object);
280 }
281
282 // Fill up $row[$columnName] array with changed values which need to be stored
283 foreach ($properties as $propertyName => $propertyValue) {
284 if ($dataMap->isPersistableProperty($propertyName) && ($propertyValue instanceof Tx_Extbase_Persistence_LazyLoadingProxy)) {
285 continue;
286 }
287
288 $columnMap = $dataMap->getColumnMap($propertyName);
289 $columnName = $columnMap->getColumnName();
290 if ($object->_isNew() || $object->_isDirty($propertyName)) {
291 if ($columnMap->isRelation()) {
292 if (($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) || ($columnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY)) {
293 $row[$columnName] = count($properties[$propertyName]);
294 foreach ($propertyValue as $containedObject) {
295 $queue[] = array($propertyName => $containedObject);
296 }
297 } elseif ($propertyValue instanceof Tx_Extbase_DomainObject_DomainObjectInterface) {
298 // TODO Handle Value Objects different
299 // SK: this is the case RELATION_HAS_ONE, correct?
300 if ($propertyValue->_isNew()) {
301 // SK: What happens if the value is not new, but changed?
302 $this->persistObject($propertyValue);
303 }
304 $row[$columnName] = $propertyValue->getUid();
305 }
306 } else {
307 // Not an relation, this means it is a simple type such as STRING or Integer
308 // SK: I think that the second option $fullQuoteString is NOT needed here, as this should be the job of the persistence backend.
309 $row[$columnName] = $dataMap->convertPropertyValueToFieldValue($properties[$propertyName], FALSE);
310 }
311 }
312 }
313
314 if ($object->_isNew()) {
315 $this->insertObject($object, $parentObject, $parentPropertyName, $row);
316 } elseif ($object->_isDirty()) {
317 $this->updateObject($object, $parentObject, $parentPropertyName, $row);
318 }
319
320 // SK: I need to check the code below more thoroughly
321 if ($parentObject instanceof Tx_Extbase_DomainObject_DomainObjectInterface && !empty($parentPropertyName)) {
322 $parentClassName = get_class($parentObject);
323 $parentDataMap = $this->dataMapper->getDataMap($parentClassName);
324 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
325
326 if (($parentColumnMap->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY)) {
327 $this->insertRelation($object, $parentObject, $parentPropertyName);
328 }
329 }
330
331 if ($object instanceof Tx_Extbase_DomainObject_AbstractEntity) {
332 $object->_memorizeCleanState();
333 }
334
335 // SK: Where does $queue come from? Do we need the code below?
336 if ($processQueue === TRUE) {
337 foreach ($queue as $queuedObjects) {
338 foreach($queuedObjects as $propertyName => $queuedObject) {
339 $this->persistObject($queuedObject, $object, $propertyName);
340 }
341 }
342 }
343
344 }
345
346 /*
347 * Tests, if the given Domain Object already exists in the storage backend
348 *
349 * @param Tx_Extbase_DomainObject_AbstractValueObject $object The object to be tested
350 */
351 protected function checkForAlreadyPersistedValueObject(Tx_Extbase_DomainObject_AbstractValueObject $object) {
352 $dataMap = $this->dataMapper->getDataMap(get_class($object));
353 $properties = $object->_getProperties();
354 $result = $this->storageBackend->hasValueObject($properties, $dataMap);
355 if ($result !== FALSE) {
356 $object->_setProperty('uid', $result);
357 }
358 }
359
360 /**
361 * Inserts an object in the storage
362 *
363 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
364 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
365 * @param string|NULL $parentPropertyName The name of the property
366 * @param array $row The $row
367 */
368 protected function insertObject(Tx_Extbase_DomainObject_DomainObjectInterface $object, Tx_Extbase_DomainObject_AbstractEntity $parentObject = NULL, $parentPropertyName = NULL, array &$row) {
369 $className = get_class($object);
370 $dataMap = $this->dataMapper->getDataMap($className);
371 $tableName = $dataMap->getTableName();
372 $this->addCommonFieldsToRow($object, $parentObject, $parentPropertyName, $row);
373 $uid = $this->storageBackend->addRow(
374 $tableName,
375 $row
376 );
377 $object->_setProperty('uid', $uid);
378 $this->referenceIndex->updateRefIndexTable($tableName, $uid);
379 }
380
381 /**
382 * Inserts mm-relation into a relation table
383 *
384 * @param Tx_Extbase_DomainObject_DomainObjectInterface $relatedObject The related object
385 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The parent object
386 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
387 * @return void
388 */
389 protected function insertRelation(Tx_Extbase_DomainObject_DomainObjectInterface $relatedObject, Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $parentPropertyName) {
390 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
391 $row = array(
392 'uid_local' => (int)$parentObject->getUid(), // TODO Aliases for relation field names
393 'uid_foreign' => (int)$relatedObject->getUid(),
394 'tablenames' => $dataMap->getTableName(),
395 'sorting' => 9999 // TODO sorting of mm table items
396 );
397 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
398 $res = $this->storageBackend->addRow(
399 $tableName,
400 $row
401 );
402 return $res;
403 }
404
405 /**
406 * Updates a given object in the storage
407 *
408 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
409 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
410 * @param string|NULL $parentPropertyName The name of the property
411 * @param array $row The $row
412 */
413 protected function updateObject(Tx_Extbase_DomainObject_DomainObjectInterface $object, $parentObject = NULL, $parentPropertyName = NULL, array &$row) {
414 $className = get_class($object);
415 $dataMap = $this->dataMapper->getDataMap($className);
416 $tableName = $dataMap->getTableName();
417 $this->addCommonFieldsToRow($object, $parentObject, $parentPropertyName, $row);
418 $uid = $object->getUid();
419 $row['uid'] = $uid;
420 $res = $this->storageBackend->updateRow(
421 $tableName,
422 $row
423 );
424 $this->referenceIndex->updateRefIndexTable($tableName, $uid);
425 return $res;
426 }
427
428 /**
429 * Returns a table row to be inserted or updated in the database
430 *
431 * @param Tx_Extbase_Persistence_Mapper_DataMap $dataMap The appropriate data map representing a database table
432 * @param array $properties The properties of the object
433 * @return array A single row to be inserted in the database
434 */
435 protected function addCommonFieldsToRow(Tx_Extbase_DomainObject_DomainObjectInterface $object, $parentObject = NULL, $parentPropertyName = NULL, array &$row) {
436 $className = get_class($object);
437 $dataMap = $this->dataMapper->getDataMap($className);
438 if ($dataMap->hasCreationDateColumn() && $object->_isNew()) {
439 $row[$dataMap->getCreationDateColumnName()] = $GLOBALS['EXEC_TIME'];
440 }
441 if ($dataMap->hasTimestampColumn()) {
442 $row[$dataMap->getTimestampColumnName()] = $GLOBALS['EXEC_TIME'];
443 }
444 if ($dataMap->hasPidColumn()) {
445 // FIXME Make the settings from $this->cObj available
446 $row['pid'] = !empty($this->cObj->data['pages']) ? $this->cObj->data['pages'] : $GLOBALS['TSFE']->id;
447 }
448 if ($parentObject instanceof Tx_Extbase_DomainObject_DomainObjectInterface && !empty($parentPropertyName)) {
449 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
450 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
451 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
452 if ($parentKeyFieldName !== NULL) {
453 $row[$parentKeyFieldName] = $parentObject->getUid();
454 }
455 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
456 if ($parentTableFieldName !== NULL) {
457 $row[$parentTableFieldName] = $parentDataMap->getTableName();
458 }
459 }
460 }
461
462 /**
463 * Inserts and updates all relations of an object. It also inserts and updates data in relation tables.
464 *
465 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object for which the relations should be updated
466 * @param string $propertyName The name of the property holding the related child objects
467 * @param array $relations The queued relations
468 * @return void
469 */
470 protected function persistRelations(Tx_Extbase_DomainObject_DomainObjectInterface $object, $propertyName, array $relations) {
471 $dataMap = $this->dataMapper->getDataMap(get_class($object));
472 foreach ($relations as $propertyName => $relatedObjects) {
473 if (!empty($relatedObjects)) {
474 $typeOfRelation = $dataMap->getColumnMap($propertyName)->getTypeOfRelation();
475 foreach ($relatedObjects as $relatedObject) {
476 if ($relatedObject->_isNew()) {
477 $this->persistObject($relatedObject, $object, $propertyName);
478 if ($typeOfRelation === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
479 $this->insertRelationInRelationTable($relatedObject, $object, $propertyName);
480 }
481 } elseif ($relatedObject->_isDirty()) {
482 $this->persistObject($relatedObject, $object, $propertyName);
483 }
484 }
485 }
486 }
487 }
488
489 /**
490 * Iterate over deleted objects and process them
491 *
492 * @return void
493 */
494 protected function processDeletedObjects() {
495 foreach ($this->deletedObjects as $object) {
496 $this->deleteObject($object);
497 if ($this->identityMap->hasObject($object)) {
498 $this->session->registerRemovedObject($object);
499 $this->identityMap->unregisterObject($object);
500 }
501 }
502 $this->deletedObjects = new Tx_Extbase_Persistence_ObjectStorage();
503 }
504
505 /**
506 * Deletes an object, it's 1:n related objects, and the m:n relations in relation tables (but not the m:n related objects!)
507 *
508 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object to be insterted in the storage
509 * @param Tx_Extbase_DomainObject_AbstractEntity|NULL $parentObject The parent object (if any)
510 * @param string|NULL $parentPropertyName The name of the property
511 * @param bool $markAsDeleted Shold we only mark the row as deleted instead of deleting (TRUE by default)?
512 * @param bool $recurseIntoRelations Shold we delete also dependant aggregates (FALSE by default)?
513 * @return void
514 */
515 protected function deleteObject(Tx_Extbase_DomainObject_DomainObjectInterface $object, $parentObject = NULL, $parentPropertyName = NULL, $markAsDeleted = TRUE, $recurseIntoRelations = FALSE) {
516 // TODO Implement recursive deletions
517 $dataMap = $this->dataMapper->getDataMap(get_class($object));
518 $tableName = $dataMap->getTableName();
519 if ($markAsDeleted === TRUE && $dataMap->hasDeletedColumn()) {
520 $deletedColumnName = $dataMap->getDeletedColumnName();
521 $res = $this->storageBackend->updateRow(
522 $tableName,
523 array(
524 'uid' => $object->getUid(),
525 $deletedColumnName => 1
526 )
527 );
528 } else {
529 $res = $this->storageBackend->removeRow(
530 $tableName,
531 $object->getUid()
532 );
533 }
534 $this->referenceIndex->updateRefIndexTable($tableName, $uid);
535 }
536
537 /**
538 * Deletes all relations of an object.
539 *
540 * @param Tx_Extbase_DomainObject_DomainObjectInterface $object The object for which the relations should be updated
541 * @param string $propertyName The name of the property holding the related child objects
542 * @param array $relations The queued relations
543 * @return void
544 */
545 // SK: The below method is never called.
546 // SK: I think there is still a problem with deleted objects and deleted relations.
547 // SK: I am not yet sure where deleted relations ae handled. Need to check more thoroughly!
548 protected function deleteRelatedObjects(Tx_Extbase_DomainObject_DomainObjectInterface $object, array $relations) {
549 $dataMap = $this->dataMapper->getDataMap(get_class($object));
550 foreach ($relations as $propertyName => $relatedObjects) {
551 if (is_array($relatedObjects)) {
552 foreach ($relatedObjects as $relatedObject) {
553 $this->deleteObject($relatedObject, $object, $propertyName);
554 if ($dataMap->getColumnMap($propertyName)->getTypeOfRelation() === Tx_Extbase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
555 $this->deleteRelationInRelationTable($relatedObject, $object, $propertyName);
556 }
557 }
558 }
559 }
560 }
561
562 /**
563 * Update relations in a relation table
564 *
565 * @param array $relatedObjects An array of related objects
566 * @param Tx_Extbase_DomainObject_DomainObjectInterface $parentObject The parent object
567 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
568 * @return void
569 */
570 protected function deleteRelationInRelationTable($relatedObject, Tx_Extbase_DomainObject_DomainObjectInterface $parentObject, $parentPropertyName) {
571 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
572 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
573 // TODO Remove dependency to the t3lib_db instance
574 $res = $this->persistenceBackend->exec_SELECTquery(
575 'uid_foreign',
576 $tableName,
577 'uid_local=' . $parentObject->getUid()
578 );
579 $existingRelations = array();
580 while($row = $this->persistenceBackend->sql_fetch_assoc($res)) {
581 $existingRelations[current($row)] = current($row);
582 }
583 $relationsToDelete = $existingRelations;
584 if (is_array($relatedObject)) {
585 foreach ($relatedObject as $relatedObject) {
586 $relatedObjectUid = $relatedObject->getUid();
587 if (array_key_exists($relatedObjectUid, $relationsToDelete)) {
588 unset($relationsToDelete[$relatedObjectUid]);
589 }
590 }
591 }
592 if (count($relationsToDelete) > 0) {
593 $relationsToDeleteList = implode(',', $relationsToDelete);
594 $res = $this->persistenceBackend->exec_DELETEquery(
595 $tableName,
596 'uid_local=' . $parentObject->getUid() . ' AND uid_foreign IN (' . $relationsToDeleteList . ')'
597 );
598 }
599 }
600
601 /**
602 * Delegates the call to the Data Map.
603 * Returns TRUE if the property is persistable (configured in $TCA)
604 *
605 * @param string $className The property name
606 * @param string $propertyName The property name
607 * @return boolean TRUE if the property is persistable (configured in $TCA)
608 */
609 public function isPersistableProperty($className, $propertyName) {
610 $dataMap = $this->dataMapper->getDataMap($className);
611 return $dataMap->isPersistableProperty($propertyName);
612 }
613
614 }
615
616 ?>