* added isRelation() method to Column Map
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Mapper / TX_EXTMVC_Persistence_Mapper_ObjectRelationalMapper.php
1 <?php
2 declare(ENCODING = 'utf-8');
3
4 /* *
5 * This script belongs to the FLOW3 framework. *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License as published by the *
9 * Free Software Foundation, either version 3 of the License, or (at your *
10 * option) any later version. *
11 * *
12 * This script is distributed in the hope that it will be useful, but *
13 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN- *
14 * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser *
15 * General Public License for more details. *
16 * *
17 * You should have received a copy of the GNU Lesser General Public *
18 * License along with the script. *
19 * If not, see http://www.gnu.org/licenses/lgpl.html *
20 * *
21 * The TYPO3 project - inspiring people to share! *
22 * */
23
24 require_once(PATH_t3lib . 'interfaces/interface.t3lib_singleton.php');
25 require_once(t3lib_extMgm::extPath('extmvc') . 'Classes/Utility/TX_EXTMVC_Utility_Strings.php');
26 require_once(t3lib_extMgm::extPath('extmvc') . 'Classes/Persistence/TX_EXTMVC_Persistence_ObjectStorage.php');
27 require_once(t3lib_extMgm::extPath('extmvc') . 'Classes/Persistence/Mapper/TX_EXTMVC_Persistence_Mapper_DataMap.php');
28
29 /**
30 * A mapper to map database tables configured in $TCA on domain objects.
31 *
32 * @version $Id:$
33 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
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 * Constructs a new mapper
53 *
54 * @author Jochen Rau <jochen.rau@typoplanet.de>
55 */
56 public function __construct() {
57 $this->cObj = t3lib_div::makeInstance('tslib_cObj');
58 $this->session = t3lib_div::makeInstance('TX_EXTMVC_Persistence_Session');
59 $GLOBALS['TSFE']->includeTCA();
60 }
61
62 /**
63 * Finds objects matching a given WHERE Clause
64 *
65 * @param string $className The class name
66 * @param string $arguments The WHERE statement
67 * @return void
68 * @author Jochen Rau <jochen.rau@typoplanet.de>
69 */
70 public function findWhere($className, $where = '1=1') {
71 $dataMap = $this->getDataMap($className);
72 $rows = $this->fetch($dataMap, $where);
73 $objects = $this->reconstituteObjects($dataMap, $rows);
74 return $objects;
75 }
76
77 /**
78 * Fetches rows from the database by given SQL statement snippets
79 *
80 * @param string $from FROM statement
81 * @param string $where WHERE statement
82 * @param string $groupBy GROUP BY statement
83 * @param string $orderBy ORDER BY statement
84 * @param string $limit LIMIT statement
85 * @return void
86 * @author Jochen Rau <jochen.rau@typoplanet.de>
87 */
88 public function fetch($dataMap, $where = '1=1', $groupBy = '', $orderBy = '', $limit = '') {
89 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
90 '*', // TODO limit fetched fields
91 $dataMap->getTableName(),
92 $where . $this->cObj->enableFields($dataMap->getTableName()) . $this->cObj->enableFields($dataMap->getTableName()),
93 $groupBy,
94 $orderBy,
95 $limit
96 );
97 // TODO language overlay; workspace overlay
98 return $rows ? $rows : array();
99 }
100
101 /**
102 * Fetches a rows from the database by given SQL statement snippets taking a relation table into account
103 *
104 * @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!
105 * @param string Optional GROUP BY field(s), if none, supply blank string.
106 * @param string Optional ORDER BY field(s), if none, supply blank string.
107 * @param string Optional LIMIT value ([begin,]max), if none, supply blank string.
108 * @author Jochen Rau <jochen.rau@typoplanet.de>
109 */
110 public function fetchWithRelationTable($parentObject, $columnMap, $where = '1=1', $groupBy = '', $orderBy = '', $limit = '') {
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=' . intval($parentObject->getUid()) . $this->cObj->enableFields($columnMap->getChildTableName()) . $this->cObj->enableFields($columnMap->getChildTableName()),
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 * @author Jochen Rau <jochen.rau@typoplanet.de>
130 */
131 protected function reconstituteObjects($dataMap, array $rows) {
132 $objects = array();
133 foreach ($rows as $row) {
134 $properties = array();
135 foreach ($dataMap->getColumnMaps() as $columnMap) {
136 $properties[$columnMap->getPropertyName()] = $dataMap->convertFieldValueToPropertyValue($columnMap->getPropertyName(), $row[$columnMap->getColumnName()]);
137 }
138 $object = $this->reconstituteObject($dataMap->getClassName(), $properties);
139 foreach ($dataMap->getColumnMaps() as $columnMap) {
140 if ($columnMap->getTypeOfRelation() === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
141 $where .= $columnMap->getParentKeyFieldName() . '=' . intval($object->getUid());
142 $relatedDataMap = $this->getDataMap($columnMap->getChildClassName());
143 $relatedRows = $this->fetch($relatedDataMap, $where);
144 $relatedObjects = $this->reconstituteObjects($relatedDataMap, $relatedRows, $depth);
145 $object->_reconstituteProperty($columnMap->getPropertyName(), $relatedObjects);
146 } elseif ($columnMap->getTypeOfRelation() === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
147 $relatedDataMap = $this->getDataMap($columnMap->getChildClassName());
148 $relatedRows = $this->fetchWithRelationTable($object, $columnMap);
149 $relatedObjects = $this->reconstituteObjects($relatedDataMap, $relatedRows, $depth);
150 $object->_reconstituteProperty($columnMap->getPropertyName(), $relatedObjects);
151 }
152 }
153 $this->session->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 * @author Robert Lemke <robert@typo3.org>
166 */
167 protected function reconstituteObject($className, array $properties = array()) {
168 // those objects will be fetched from within the __wakeup() method of the object...
169 $GLOBALS['EXTMVC']['reconstituteObject']['properties'] = $properties;
170 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
171 unset($GLOBALS['EXTMVC']['reconstituteObject']);
172 return $object;
173 }
174
175 /**
176 * Persists all objects of a persistence session
177 *
178 * @return void
179 * @author Jochen Rau <jochen.rau@typoplanet.de>
180 */
181 public function persistAll() {
182 // first, persit all aggregate root objects
183 $aggregateRootClassNames = $this->session->getAggregateRootClassNames();
184 foreach ($aggregateRootClassNames as $className) {
185 $this->persistObjects($className);
186 }
187 // persist all remaining objects registered manually
188 // $this->persistObjects();
189 }
190
191 /**
192 * Persists all objects of a persitance session that are of a given class. If there
193 * is no class specified, it persits all objects of a session.
194 *
195 * @param string $className Name of the class of the objects to be persisted
196 * @author Jochen Rau <jochen.rau@typoplanet.de>
197 */
198 protected function persistObjects($className = NULL) {
199 foreach ($this->session->getAddedObjects($className) as $object) {
200 $this->insertObject($object);
201 $this->session->unregisterObject($object);
202 $this->session->registerReconstitutedObject($object);
203 }
204 foreach ($this->session->getDirtyObjects($className) as $object) {
205 $this->updateObject($object);
206 $this->session->unregisterObject($object);
207 $this->session->registerReconstitutedObject($object);
208 }
209 foreach ($this->session->getRemovedObjects($className) as $object) {
210 $this->deleteObject($object);
211 $this->session->unregisterObject($object);
212 }
213 }
214
215 /**
216 * Inserts an object to the database.
217 *
218 * @return void
219 * @author Jochen Rau <jochen.rau@typoplanet.de>
220 */
221 protected function insertObject(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL) {
222 $properties = $object->_getProperties();
223 $dataMap = $this->getDataMap(get_class($object));
224 $relations = $this->getRelations($dataMap, $properties);
225 $row = $this->getRow($dataMap, $properties);
226
227 if ($parentObject instanceof TX_EXTMVC_DomainObject_AbstractDomainObject && $parentPropertyName !== NULL) {
228 $parentDataMap = $this->getDataMap(get_class($parentObject));
229 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
230 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
231 if ($parentKeyFieldName !== NULL) {
232 $row[$parentKeyFieldName] = $parentObject->getUid();
233 }
234 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
235 if ($parentTableFieldName !== NULL) {
236 $row[$parentTableFieldName] = $parentDataMap->getTableName();
237 }
238 }
239
240 unset($row['uid']);
241 $row['pid'] = !empty($this->cObj->data['pages']) ? $this->cObj->data['pages'] : $GLOBALS['TSFE']->id;
242 $row['tstamp'] = time();
243
244 $tableName = $dataMap->getTableName();
245 $res = $GLOBALS['TYPO3_DB']->exec_INSERTquery(
246 $tableName,
247 $row
248 );
249 $object->_reconstituteProperty('uid', $GLOBALS['TYPO3_DB']->sql_insert_id());
250
251 $recursionMode = TRUE; // TODO make parametric
252 if ($recursionMode === TRUE) {
253 $this->processRelations($object, $propertyName, 'persist', $relations);
254 }
255 }
256
257 /**
258 * Updates a modified object in the database
259 *
260 * @return void
261 * @author Jochen Rau <jochen.rau@typoplanet.de>
262 */
263 protected function updateObject(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL) {
264 $properties = $object->_getDirtyProperties();
265 $dataMap = $this->getDataMap(get_class($object));
266 $relations = $this->getRelations($dataMap, $properties);
267 $row = $this->getRow($dataMap, $properties);
268
269 unset($row['uid']);
270 $row['crdate'] = time();
271 if (!empty($GLOBALS['TSFE']->fe_user->user['uid'])) {
272 $row['cruser_id'] = $GLOBALS['TSFE']->fe_user->user['uid'];
273 }
274 if ($parentObject instanceof TX_EXTMVC_DomainObject_AbstractDomainObject && $parentPropertyName !== NULL) {
275 $parentDataMap = $this->getDataMap(get_class($parentObject));
276 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
277 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
278 if ($parentKeyFieldName !== NULL) {
279 $row[$parentKeyFieldName] = $parentObject->getUid();
280 }
281 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
282 if ($parentTableFieldName !== NULL) {
283 $row[$parentTableFieldName] = $parentDataMap->getTableName();
284 }
285 }
286
287 $tableName = $dataMap->getTableName();
288 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
289 $tableName,
290 'uid=' . $object->getUid(),
291 $row
292 );
293
294 $recursionMode = TRUE; // TODO make parametric
295 if ($recursionMode === TRUE) {
296 $this->processRelations($object, $propertyName, 'persist', $relations);
297 }
298 }
299
300 /**
301 * Deletes an object, it's 1:n related objects, and the m:n relations in relation tables (but not the m:n related objects!)
302 *
303 * @return void
304 * @author Jochen Rau <jochen.rau@typoplanet.de>
305 */
306 protected function deleteObject(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $parentObject = NULL, $parentPropertyName = NULL, $recursionMode = FALSE, $onlySetDeleted = TRUE) {
307 $relations = array();
308 $properties = $object->_getDirtyProperties();
309 $dataMap = $this->getDataMap(get_class($object));
310 $relations = $this->getRelations($dataMap, $properties);
311
312 $tableName = $dataMap->getTableName();
313 if ($onlySetDeleted === TRUE && !empty($deletedColumnName)) {
314 $deletedColumnName = $dataMap->getDeletedColumnName();
315 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
316 $tableName,
317 'uid=' . $object->getUid(),
318 array($deletedColumnName => 1)
319 );
320 } else {
321 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
322 $tableName,
323 'uid=' . $object->getUid()
324 );
325 }
326
327 if ($recursionMode === TRUE) {
328 $this->processRelations($object, $propertyName, 'delete', $relations);
329 }
330 }
331
332 /**
333 * Returns a table row to be inserted or updated in the database
334 *
335 * @param TX_EXTMVC_Persistence_Mapper_DataMap $dataMap The appropriate data map representing a database table
336 * @param string $properties The properties of the object
337 * @return array A single row to be inserted in the database
338 * @author Jochen Rau <jochen.rau@typoplanet.de>
339 */
340 protected function getRow(TX_EXTMVC_Persistence_Mapper_DataMap $dataMap, $properties) {
341 $relations = array();
342 foreach ($dataMap->getColumnMaps() as $columnMap) {
343 $propertyName = $columnMap->getPropertyName();
344 $columnName = $columnMap->getColumnName();
345 if ($columnMap->getTypeOfRelation() === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_MANY) {
346 $row[$columnName] = count($properties[$propertyName]);
347 } elseif ($columnMap->getTypeOfRelation() === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
348 $row[$columnName] = count($properties[$propertyName]);
349 } else {
350 if ($properties[$propertyName] !== NULL) {
351 $row[$columnName] = $dataMap->convertPropertyValueToFieldValue($properties[$propertyName]);
352 }
353 }
354 }
355 return $row;
356 }
357
358 /**
359 * Returns all property values holding child objects
360 *
361 * @param TX_EXTMVC_Persistence_Mapper_DataMap $dataMap The data map
362 * @param string $properties The object properties
363 * @return array An array of properties with related child objects
364 * @author Jochen Rau <jochen.rau@typoplanet.de>
365 */
366 protected function getRelations(TX_EXTMVC_Persistence_Mapper_DataMap $dataMap, $properties) {
367 $relations = array();
368 foreach ($dataMap->getColumnMaps() as $columnMap) {
369 $propertyName = $columnMap->getPropertyName();
370 $columnName = $columnMap->getColumnName();
371 if ($columnMap->isRelation()) {
372 $relations[$propertyName] = $properties[$propertyName];
373 }
374 }
375 return $relations;
376 }
377
378 /**
379 * Processes all relations of an object. It also updates relation tables.
380 *
381 * @param TX_EXTMVC_DomainObject_AbstractDomainObject $object The object for which the relations should be updated
382 * @param string $propertyName The name of the property holding the related child objects
383 * @param string $command The command (one of "persist", "delete"). Persist updates and inserts records as needed
384 * @param array $relations The queued relations
385 * @return void
386 * @author Jochen Rau <jochen.rau@typoplanet.de>
387 */
388 protected function processRelations(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $propertyName, $command, array $relations) {
389 $dataMap = $this->getDataMap(get_class($object));
390 if ($command === 'persist') {
391 foreach ($relations as $propertyName => $relatedObjects) {
392 if (!empty($relatedObjects)) {
393 $typeOfRelation = $dataMap->getColumnMap($propertyName)->getTypeOfRelation();
394 foreach ($relatedObjects as $relatedObject) {
395 if (!$this->session->isReconstitutedObject($relatedObject)) {
396 $this->insertObject($relatedObject, $object, $propertyName);
397 if ($typeOfRelation === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
398 $this->insertRelationInRelationTable($relatedObject, $object, $propertyName);
399 }
400 } elseif ($this->session->isReconstitutedObject($relatedObject) && $relatedObject->_isDirty()) {
401 $this->updateObject($relatedObject, $object, $propertyName);
402 }
403 }
404 }
405 if ($typeOfRelation === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
406 $this->updateRelationsInRelationTable($relatedObjects, $object, $propertyName);
407 }
408 }
409 } elseif ($command === 'delete') {
410 foreach ($relations as $propertyName => $relatedObjects) {
411 foreach ($relatedObjects as $relatedObject) {
412 $this->deleteObject($relatedObject, $object, $propertyName);
413 if ($dataMap->getColumnMap($propertyName)->getTypeOfRelation() === TX_EXTMVC_Persistence_Mapper_ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
414 $this->deleteRelation($relatedObject, $object, $propertyName);
415 }
416 }
417 }
418 }
419 }
420
421 /**
422 * Inserts relation to a relation table
423 *
424 * @param TX_EXTMVC_DomainObject_AbstractDomainObject $relatedObject The related object
425 * @param TX_EXTMVC_DomainObject_AbstractDomainObject $parentObject The parent object
426 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
427 * @return void
428 * @author Jochen Rau <jochen.rau@typoplanet.de>
429 */
430 protected function insertRelationInRelationTable(TX_EXTMVC_DomainObject_AbstractDomainObject $relatedObject, TX_EXTMVC_DomainObject_AbstractDomainObject $parentObject, $parentPropertyName) {
431 $dataMap = $this->getDataMap(get_class($parentObject));
432 $rowToInsert = array(
433 'uid_local' => $parentObject->getUid(),
434 'uid_foreign' => $relatedObject->getUid(),
435 'tablenames' => $dataMap->getTableName(),
436 'sorting' => 9999 // TODO sorting of mm table items
437 );
438 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
439 $res = $GLOBALS['TYPO3_DB']->exec_INSERTquery(
440 $tableName,
441 $rowToInsert
442 );
443 }
444
445 /**
446 * Update relations in a relation table
447 *
448 * @param array $relatedObjects An array of related objects
449 * @param TX_EXTMVC_DomainObject_AbstractDomainObject $parentObject The parent object
450 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
451 * @return void
452 * @author Jochen Rau <jochen.rau@typoplanet.de>
453 */
454 protected function updateRelationsInRelationTable($relatedObjects, TX_EXTMVC_DomainObject_AbstractDomainObject $parentObject, $parentPropertyName) {
455 $dataMap = $this->getDataMap(get_class($parentObject));
456 $tableName = $dataMap->getColumnMap($parentPropertyName)->getRelationTableName();
457 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
458 'uid_foreign',
459 $tableName,
460 'uid_local=' . $parentObject->getUid()
461 );
462 $existingRelations = array();
463 while($row = mysql_fetch_assoc($res)) {
464 $existingRelations[current($row)] = current($row);
465 }
466 $relationsToDelete = $existingRelations;
467 if (is_array($relatedObjects)) {
468 foreach ($relatedObjects as $relatedObject) {
469 $relatedObjectUid = $relatedObject->getUid();
470 if (array_key_exists($relatedObjectUid, $relationsToDelete)) {
471 unset($relationsToDelete[$relatedObjectUid]);
472 }
473 }
474 }
475 if (count($relationsToDelete) > 0) {
476 $relationsToDeleteList = implode(',', $relationsToDelete);
477 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
478 $tableName,
479 'uid_local=' . $parentObject->getUid() . ' AND uid_foreign IN (' . $relationsToDeleteList . ')'
480 );
481 }
482 }
483
484 /**
485 * Delegates the call to the Data Map.
486 * Returns TRUE if the property is persistable (configured in $TCA)
487 *
488 * @param string $className The property name
489 * @param string $propertyName The property name
490 * @return boolean TRUE if the property is persistable (configured in $TCA)
491 * @author Jochen Rau <jochen.rau@typoplanet.de>
492 */
493 public function isPersistableProperty($className, $propertyName) {
494 $dataMapClassName = t3lib_div::makeInstanceClassName('TX_EXTMVC_Persistence_Mapper_DataMap');
495 $dataMap = new $dataMapClassName($className);
496 $dataMap->initialize();
497 return $dataMap->isPersistableProperty($propertyName);
498 }
499
500 /**
501 * Returns a data map for a given class name
502 *
503 * @return TX_EXTMVC_Persistence_Mapper_DataMap The data map
504 * @author Jochen Rau <jochen.rau@typoplanet.de>
505 */
506 protected function getDataMap($className) {
507 $dataMapClassName = t3lib_div::makeInstanceClassName('TX_EXTMVC_Persistence_Mapper_DataMap');
508 // TODO Cache data maps
509 $dataMap = new $dataMapClassName($className);
510 $dataMap->initialize();
511 return $dataMap;
512 }
513
514 }
515 ?>