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