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