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