e49246b407a242b72091c82f6bf27c0df93f8ce2
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Mapper / ObjectRelationalMapper.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 require_once(PATH_t3lib . 'interfaces/interface.t3lib_singleton.php');
26 require_once(PATH_tslib . 'class.tslib_content.php');
27
28 /**
29 * A mapper to map database tables configured in $TCA on domain objects.
30 *
31 * @package TYPO3
32 * @subpackage extbase
33 * @version $ID:$
34 */
35 class Tx_ExtBase_Persistence_Mapper_ObjectRelationalMapper implements t3lib_Singleton {
36
37 /**
38 * The persistence session
39 *
40 * @var Tx_ExtBase_Persistence_Session
41 **/
42 protected $persistenceSession;
43
44 /**
45 * Cached data maps
46 *
47 * @var array
48 **/
49 protected $dataMaps = array();
50
51 /**
52 * Constructs a new mapper
53 *
54 */
55 public function __construct() {
56 $this->persistenceSession = t3lib_div::makeInstance('Tx_ExtBase_Persistence_Session');
57 $GLOBALS['TSFE']->includeTCA();
58 }
59
60 /**
61 * @see Repository#find(...)
62 */
63 public function find($className, $conditions = '', $groupBy = '', $orderBy = '', $limit = '', $useEnableFields = TRUE) {
64 if (is_array($conditions)) {
65 $whereParts = array();
66 foreach ($conditions as $key => $condition) {
67 if (is_array($condition) && isset($condition[0])) {
68 $sql = $condition[0];
69 for ($i = 1; $i < count($condition); $i++) {
70 $markPos = strpos($sql, '?');
71 if ($markPos !== FALSE) {
72 $sql = substr($sql, 0, $markPos) . $this->convertValueToQueryParameter($condition[$i]) . substr($sql, $markPos + 1);
73 }
74 }
75 $whereParts[] = '(' . $sql . ')';
76 } elseif (is_string($key)) {
77 if (!is_array($condition)) {
78 $column = $this->getDataMap($className)->getColumnMap($key)->getColumnName();
79 $sql = $column . ' = ' . $this->convertValueToQueryParameter($condition);
80 }
81 $whereParts[] = '(' . $sql . ')';
82 }
83 }
84 $where = implode(' AND ', $whereParts);
85 } elseif (is_string($conditions)) {
86 $where = $conditions;
87 }
88 return $this->fetch($className, $where, $groupBy, $orderBy, $limit, $useEnableFields);
89 }
90
91 protected function convertValueToQueryParameter($value) {
92 if (is_bool($value)) {
93 $parameter = $value ? 1 : 0;
94 } elseif ($value instanceof Tx_ExtBase_DomainObject_AbstractDomainObject) {
95 $parameter = $value->getUid();
96 } else {
97 $parameter = (string)$value;
98 }
99 return $GLOBALS['TYPO3_DB']->fullQuoteStr($parameter, '');
100 }
101
102
103 /**
104 * Fetches rows from the database by given SQL statement snippets
105 *
106 * @param string $className the className
107 * @param string $where WHERE statement
108 * @param string $groupBy GROUP BY statement
109 * @param string $orderBy ORDER BY statement
110 * @param string $limit LIMIT statement
111 * @return array The matched rows
112 */
113 public function fetch($className, $where = '1=1', $groupBy = '', $orderBy = '', $limit = '', $useEnableFields = TRUE) {
114 $dataMap = $this->getDataMap($className);
115 if ($useEnableFields === TRUE) {
116 $enableFields = $GLOBALS['TSFE']->sys_page->enableFields($dataMap->getTableName());
117 } else {
118 $enableFields = '';
119 }
120 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
121 '*', // TODO limit fetched fields
122 $dataMap->getTableName(),
123 $where . $enableFields,
124 $groupBy,
125 $orderBy,
126 $limit
127 );
128 // SK: Do we want to make it possible to ignore "enableFields"?
129 // TODO language overlay; workspace overlay
130 $objects = array();
131 if (is_array($rows)) {
132 if (count($rows) > 0) {
133 $objects = $this->reconstituteObjects($dataMap, $rows);
134 }
135 }
136 return $objects;
137 }
138
139 /**
140 * Fetches a rows from the database by given SQL statement snippets taking a relation table into account
141 *
142 * @param string Optional WHERE clauses put in the end of the query, defaults to '1=1. NOTICE: You must escape values in this argument with $this->fullQuoteStr() yourself!
143 * @param string Optional GROUP BY field(s), defaults to blank string.
144 * @param string Optional ORDER BY field(s), defaults to blank string.
145 * @param string Optional LIMIT value ([begin,]max), defaults to blank string.
146 */
147 // SK: Are SQL injections possible here? Can we somehow prevent them? I did not check it thoroughly yet, but I think they are possible
148 public function fetchWithRelationTable($parentObject, $columnMap, $where = '1=1', $groupBy = '', $orderBy = '', $limit = '', $useEnableFields = TRUE) {
149 if ($useEnableFields === TRUE) {
150 $enableFields = $GLOBALS['TSFE']->sys_page->enableFields($columnMap->getChildTableName());
151 } else {
152 $enableFields = '';
153 }
154 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
155 $columnMap->getChildTableName() . '.*, ' . $columnMap->getRelationTableName() . '.*',
156 $columnMap->getChildTableName() . ' LEFT JOIN ' . $columnMap->getRelationTableName() . ' ON (' . $columnMap->getChildTableName() . '.uid=' . $columnMap->getRelationTableName() . '.uid_foreign)',
157 $where . ' AND ' . $columnMap->getRelationTableName() . '.uid_local=' . t3lib_div::intval_positive($parentObject->getUid()) . $enableFields,
158 $groupBy,
159 $orderBy,
160 $limit
161 );
162 // TODO language overlay; workspace overlay; sorting
163 return $rows ? $rows : array();
164 }
165
166 /**
167 * reconstitutes domain objects from $rows (array)
168 *
169 * @param Tx_ExtBase_Persistence_Mapper_DataMap $dataMap The data map corresponding to the domain object
170 * @param array $rows The rows array fetched from the database
171 * @return array An array of reconstituted domain objects
172 */
173 // SK: I Need to check this method more thoroughly.
174 // SK: Are loops detected during reconstitution?
175 protected function reconstituteObjects($dataMap, array $rows) {
176 $objects = array();
177 foreach ($rows as $row) {
178 $properties = array();
179 foreach ($dataMap->getColumnMaps() as $columnMap) {
180 $properties[$columnMap->getPropertyName()] = $dataMap->convertFieldValueToPropertyValue($columnMap->getPropertyName(), $row[$columnMap->getColumnName()]);
181 }
182 $object = $this->reconstituteObject($dataMap->getClassName(), $properties);
183 foreach ($dataMap->getColumnMaps() as $columnMap) {
184 if ($columnMap->getTypeOfRelation() === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
185 $where = $columnMap->getParentKeyFieldName() . '=' . intval($object->getUid());
186 $relatedDataMap = $this->getDataMap($columnMap->getChildClassName());
187 $relatedObjects = $this->fetch($columnMap->getChildClassName(), $where);
188 $object->_reconstituteProperty($columnMap->getPropertyName(), $relatedObjects);
189 } elseif ($columnMap->getTypeOfRelation() === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
190 $relatedDataMap = $this->getDataMap($columnMap->getChildClassName());
191 $relatedObjects = $this->fetchWithRelationTable($object, $columnMap);
192 $object->_reconstituteProperty($columnMap->getPropertyName(), $relatedObjects);
193 }
194 }
195 $this->persistenceSession->registerReconstitutedObject($object);
196 $objects[] = $object;
197 }
198 return $objects;
199 }
200
201 /**
202 * Reconstitutes the specified object and fills it with the given properties.
203 *
204 * @param string $objectName Name of the object to reconstitute
205 * @param array $properties The names of properties and their values which should be set during the reconstitution
206 * @return object The reconstituted object
207 */
208 protected function reconstituteObject($className, array $properties = array()) {
209 // those objects will be fetched from within the __wakeup() method of the object...
210 $GLOBALS['ExtBase']['reconstituteObject']['properties'] = $properties;
211 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
212 unset($GLOBALS['ExtBase']['reconstituteObject']);
213 return $object;
214 }
215
216 /**
217 * Persists all objects of a persistence session
218 *
219 * @return void
220 */
221 public function persistAll() {
222 // first, persit all aggregate root objects
223 $aggregateRootClassNames = $this->persistenceSession->getAggregateRootClassNames();
224 foreach ($aggregateRootClassNames as $className) {
225 $this->persistObjects($className);
226 }
227 // persist all remaining objects registered manually
228 // $this->persistObjects();
229 }
230
231 /**
232 * Persists all objects of a persitance persistence session that are of a given class. If there
233 * is no class specified, it persits all objects of a persistence session.
234 *
235 * @param string $className Name of the class of the objects to be persisted
236 */
237 protected function persistObjects($className = NULL) {
238 foreach ($this->persistenceSession->getAddedObjects($className) as $object) {
239 $this->insertObject($object);
240 $this->persistenceSession->unregisterObject($object);
241 $this->persistenceSession->registerReconstitutedObject($object);
242 }
243 foreach ($this->persistenceSession->getDirtyObjects($className) as $object) {
244 $this->updateObject($object);
245 $this->persistenceSession->unregisterObject($object);
246 $this->persistenceSession->registerReconstitutedObject($object);
247 }
248 foreach ($this->persistenceSession->getRemovedObjects($className) as $object) {
249 $this->deleteObject($object);
250 $this->persistenceSession->unregisterObject($object);
251 }
252 }
253
254 /**
255 * Inserts an object to the database.
256 *
257 * @return void
258 */
259 // SK: I need to check this more thorougly
260 protected function insertObject(Tx_ExtBase_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL, $recurseIntoRelations = TRUE) {
261 $properties = $object->_getProperties();
262 $dataMap = $this->getDataMap(get_class($object));
263 $row = $this->getRow($dataMap, $properties);
264
265 if ($parentObject instanceof Tx_ExtBase_DomainObject_AbstractDomainObject && $parentPropertyName !== NULL) {
266 $parentDataMap = $this->getDataMap(get_class($parentObject));
267 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
268 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
269 if ($parentKeyFieldName !== NULL) {
270 $row[$parentKeyFieldName] = $parentObject->getUid();
271 }
272 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
273 if ($parentTableFieldName !== NULL) {
274 $row[$parentTableFieldName] = $parentDataMap->getTableName();
275 }
276 }
277
278 unset($row['uid']);
279
280 $row['pid'] = !empty($this->cObj->data['pages']) ? $this->cObj->data['pages'] : $GLOBALS['TSFE']->id;
281 $row['tstamp'] = time();
282
283 $tableName = $dataMap->getTableName();
284 $res = $GLOBALS['TYPO3_DB']->exec_INSERTquery(
285 $tableName,
286 $row
287 );
288 $object->_reconstituteProperty('uid', $GLOBALS['TYPO3_DB']->sql_insert_id());
289
290 $this->persistRelations($object, $propertyName, $this->getRelations($dataMap, $properties));
291 }
292
293 /**
294 * Updates a modified object in the database
295 *
296 * @return void
297 */
298 // SK: I need to check this more thorougly
299 protected function updateObject(Tx_ExtBase_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL, $recurseIntoRelations = TRUE) {
300 $properties = $object->_getDirtyProperties();
301 $dataMap = $this->getDataMap(get_class($object));
302 $row = $this->getRow($dataMap, $properties);
303 unset($row['uid']);
304 // TODO Check for crdate column
305 $row['crdate'] = time();
306 if (!empty($GLOBALS['TSFE']->fe_user->user['uid'])) {
307 $row['cruser_id'] = $GLOBALS['TSFE']->fe_user->user['uid'];
308 }
309 if ($parentObject instanceof Tx_ExtBase_DomainObject_AbstractDomainObject && $parentPropertyName !== NULL) {
310 $parentDataMap = $this->getDataMap(get_class($parentObject));
311 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
312 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
313 if ($parentKeyFieldName !== NULL) {
314 $row[$parentKeyFieldName] = $parentObject->getUid();
315 }
316 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
317 if ($parentTableFieldName !== NULL) {
318 $row[$parentTableFieldName] = $parentDataMap->getTableName();
319 }
320 }
321
322 $tableName = $dataMap->getTableName();
323 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
324 $tableName,
325 'uid=' . $object->getUid(),
326 $row
327 );
328
329 $this->persistRelations($object, $propertyName, $this->getRelations($dataMap, $properties));
330 }
331
332 /**
333 * Deletes an object, it's 1:n related objects, and the m:n relations in relation tables (but not the m:n related objects!)
334 *
335 * @return void
336 */
337 // SK: I need to check this more thorougly
338 protected function deleteObject(Tx_ExtBase_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL, $recurseIntoRelations = FALSE, $onlySetDeleted = TRUE) {
339 $relations = array();
340 $properties = $object->_getDirtyProperties();
341 $dataMap = $this->getDataMap(get_class($object));
342 $relations = $this->getRelations($dataMap, $properties);
343
344 $tableName = $dataMap->getTableName();
345 if ($onlySetDeleted === TRUE && !empty($deletedColumnName)) {
346 $deletedColumnName = $dataMap->getDeletedColumnName();
347 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
348 $tableName,
349 'uid=' . $object->getUid(),
350 array($deletedColumnName => 1)
351 );
352 } else {
353 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
354 $tableName,
355 'uid=' . $object->getUid()
356 );
357 }
358
359 if ($recurseIntoRelations === TRUE) {
360 $this->processRelations($object, $propertyName, $relations);
361 }
362 }
363
364 /**
365 * Returns a table row to be inserted or updated in the database
366 *
367 * @param Tx_ExtBase_Persistence_Mapper_DataMap $dataMap The appropriate data map representing a database table
368 * @param array $properties The properties of the object
369 * @return array A single row to be inserted in the database
370 */
371 // SK: I need to check this more thorougly
372 protected function getRow(Tx_ExtBase_Persistence_Mapper_DataMap $dataMap, $properties) {
373 $relations = array();
374 foreach ($dataMap->getColumnMaps() as $columnMap) {
375 $propertyName = $columnMap->getPropertyName();
376 $columnName = $columnMap->getColumnName();
377 if ($columnMap->getTypeOfRelation() === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
378 $row[$columnName] = count($properties[$propertyName]);
379 } elseif ($columnMap->getTypeOfRelation() === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
380 // TODO Check if this elseif is needed or could be merged with the lines above
381 $row[$columnName] = count($properties[$propertyName]);
382 } else {
383 if ($properties[$propertyName] !== NULL) {
384 $row[$columnName] = $dataMap->convertPropertyValueToFieldValue($properties[$propertyName]);
385 }
386 }
387 }
388 return $row;
389 }
390
391 /**
392 * Returns all property values holding child objects
393 *
394 * @param Tx_ExtBase_Persistence_Mapper_DataMap $dataMap The data map
395 * @param string $properties The object properties
396 * @return array An array of properties with related child objects
397 */
398 protected function getRelations(Tx_ExtBase_Persistence_Mapper_DataMap $dataMap, $properties) {
399 $relations = array();
400 foreach ($dataMap->getColumnMaps() as $columnMap) {
401 $propertyName = $columnMap->getPropertyName();
402 $columnName = $columnMap->getColumnName();
403 if ($columnMap->isRelation()) {
404 $relations[$propertyName] = $properties[$propertyName];
405 }
406 }
407 return $relations;
408 }
409
410 /**
411 * Inserts and updates all relations of an object. It also inserts and updates data in relation tables.
412 *
413 * @param Tx_ExtBase_DomainObject_AbstractDomainObject $object The object for which the relations should be updated
414 * @param string $propertyName The name of the property holding the related child objects
415 * @param array $relations The queued relations
416 * @return void
417 */
418 protected function persistRelations(Tx_ExtBase_DomainObject_AbstractDomainObject $object, $propertyName, array $relations) {
419 $dataMap = $this->getDataMap(get_class($object));
420 foreach ($relations as $propertyName => $relatedObjects) {
421 if (!empty($relatedObjects)) {
422 $typeOfRelation = $dataMap->getColumnMap($propertyName)->getTypeOfRelation();
423 foreach ($relatedObjects as $relatedObject) {
424 if (!$this->persistenceSession->isReconstitutedObject($relatedObject)) {
425 $this->insertObject($relatedObject, $object, $propertyName);
426 if ($typeOfRelation === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
427 $this->insertRelationInRelationTable($relatedObject, $object, $propertyName);
428 }
429 } elseif ($this->persistenceSession->isReconstitutedObject($relatedObject) && $relatedObject->_isDirty()) {
430 $this->updateObject($relatedObject, $object, $propertyName);
431 }
432 }
433 }
434 }
435 }
436
437 /**
438 * Deletes all relations of an object.
439 *
440 * @param Tx_ExtBase_DomainObject_AbstractDomainObject $object The object for which the relations should be updated
441 * @param string $propertyName The name of the property holding the related child objects
442 * @param array $relations The queued relations
443 * @return void
444 */
445 protected function deleteRelations(Tx_ExtBase_DomainObject_AbstractDomainObject $object, $propertyName, array $relations) {
446 $dataMap = $this->getDataMap(get_class($object));
447 foreach ($relations as $propertyName => $relatedObjects) {
448 if (is_array($relatedObjects)) {
449 foreach ($relatedObjects as $relatedObject) {
450 $this->deleteObject($relatedObject, $object, $propertyName);
451 if ($dataMap->getColumnMap($propertyName)->getTypeOfRelation() === Tx_ExtBase_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
452 $this->deleteRelationInRelationTable($relatedObject, $object, $propertyName);
453 }
454 }
455 }
456 }
457 }
458
459 /**
460 * Inserts relation to a relation table
461 *
462 * @param Tx_ExtBase_DomainObject_AbstractDomainObject $relatedObject The related object
463 * @param Tx_ExtBase_DomainObject_AbstractDomainObject $parentObject The parent object
464 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
465 * @return void
466 */
467 protected function insertRelationInRelationTable(Tx_ExtBase_DomainObject_AbstractDomainObject $relatedObject, Tx_ExtBase_DomainObject_AbstractDomainObject $parentObject, $parentPropertyName) {
468 $dataMap = $this->getDataMap(get_class($parentObject));
469 $rowToInsert = array(
470 'uid_local' => $parentObject->getUid(),
471 'uid_foreign' => $relatedObject->getUid(),
472 'tablenames' => $dataMap->getTableName(),
473 'sorting' => 9999 // TODO sorting of mm table items
474 );
475 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
476 $res = $GLOBALS['TYPO3_DB']->exec_INSERTquery(
477 $tableName,
478 $rowToInsert
479 );
480 }
481
482 /**
483 * Update relations in a relation table
484 *
485 * @param array $relatedObjects An array of related objects
486 * @param Tx_ExtBase_DomainObject_AbstractDomainObject $parentObject The parent object
487 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
488 * @return void
489 */
490 protected function deleteRelationInRelationTable($relatedObject, Tx_ExtBase_DomainObject_AbstractDomainObject $parentObject, $parentPropertyName) {
491 $dataMap = $this->getDataMap(get_class($parentObject));
492 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
493 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
494 'uid_foreign',
495 $tableName,
496 'uid_local=' . $parentObject->getUid()
497 );
498 $existingRelations = array();
499 while($row = mysql_fetch_assoc($res)) {
500 $existingRelations[current($row)] = current($row);
501 }
502 $relationsToDelete = $existingRelations;
503 if (is_array($relatedObject)) {
504 foreach ($relatedObject as $relatedObject) {
505 $relatedObjectUid = $relatedObject->getUid();
506 if (array_key_exists($relatedObjectUid, $relationsToDelete)) {
507 unset($relationsToDelete[$relatedObjectUid]);
508 }
509 }
510 }
511 if (count($relationsToDelete) > 0) {
512 $relationsToDeleteList = implode(',', $relationsToDelete);
513 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
514 $tableName,
515 'uid_local=' . $parentObject->getUid() . ' AND uid_foreign IN (' . $relationsToDeleteList . ')'
516 );
517 }
518 }
519
520 /**
521 * Delegates the call to the Data Map.
522 * Returns TRUE if the property is persistable (configured in $TCA)
523 *
524 * @param string $className The property name
525 * @param string $propertyName The property name
526 * @return boolean TRUE if the property is persistable (configured in $TCA)
527 */
528 public function isPersistableProperty($className, $propertyName) {
529 $dataMap = new Tx_ExtBase_Persistence_Mapper_DataMap($className);
530 $dataMap->initialize();
531 return $dataMap->isPersistableProperty($propertyName);
532 }
533
534 /**
535 * Returns a data map for a given class name
536 *
537 * @return Tx_ExtBase_Persistence_Mapper_DataMap The data map
538 */
539 protected function getDataMap($className) {
540 // TODO Cache data maps
541 if (empty($this->dataMaps[$className])) {
542 $dataMap = new Tx_ExtBase_Persistence_Mapper_DataMap($className);
543 $dataMap->initialize();
544 $this->dataMaps[$className] = $dataMap;
545 }
546 return $this->dataMaps[$className];
547 }
548
549 }
550 ?>