* some more steps towards an implementation of the TCA mapper
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Mapper / TX_EXTMVC_Persistence_Mapper_TcaMapper.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
27 /**
28 * A mapper to map database tables configured in $TCA onto domain objects.
29 *
30 * @version $Id:$
31 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
32 */
33 class TX_EXTMVC_Persistence_Mapper_TcaMapper implements t3lib_singleton {
34
35 /**
36 * The content object
37 *
38 * @var tslib_cObj
39 **/
40 protected $cObj;
41
42 /**
43 * The persistence session
44 *
45 * @var
46 **/
47 protected $session;
48
49 /**
50 * Constructs a new mapper
51 *
52 * @author Jochen Rau <jochen.rau@typoplanet.de>
53 */
54 public function __construct() {
55 $this->cObj = t3lib_div::makeInstance('tslib_cObj');
56 $this->session = t3lib_div::makeInstance('TX_EXTMVC_Persistence_Session');
57 $GLOBALS['TSFE']->includeTCA();
58 }
59
60 /**
61 * Finds objects matching property="xyz"
62 *
63 * @param string $propertyName The name of the property (will be chekced by a white list)
64 * @param string $arguments The WHERE statement
65 * @return void
66 * @author Jochen Rau <jochen.rau@typoplanet.de>
67 */
68 public function findWhere($className, $where = '1=1') {
69 return $this->reconstituteObjects($className, $this->fetch($className, $where));
70 }
71
72 /**
73 * Fetches a rows from the database by given SQL statement snippets
74 *
75 * @param string $from FROM statement
76 * @param string $where WHERE statement
77 * @param string $groupBy GROUP BY statement
78 * @param string $orderBy ORDER BY statement
79 * @param string $limit LIMIT statement
80 * @return void
81 * @author Jochen Rau <jochen.rau@typoplanet.de>
82 */
83 private function fetch($className, $where = '1=1', $groupBy = NULL, $orderBy = NULL, $limit = NULL) {
84 $tableName = $this->getTableName($className);
85 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
86 '*', // TODO limit fetched fields
87 $tableName,
88 $where . $this->cObj->enableFields($tableName) . $this->cObj->enableFields($tableName),
89 $groupBy,
90 $orderBy,
91 $limit
92 );
93 // TODO language overlay; workspace overlay
94 return $rows ? $rows : array();
95 }
96
97 /**
98 * Fetches a rows from the database by given SQL statement snippets
99 *
100 * @author Jochen Rau <jochen.rau@typoplanet.de>
101 */
102 private function fetchOneToMany($parentObject, $parentField, $tableName, $where = '', $groupBy = NULL, $orderBy = NULL, $limit = NULL) {
103 $where .= ' ' . $parentField . '=' . intval($parentObject->getUid());
104 return $this->fetch($tableName, $where, $groupBy, $orderBy, $limit);
105 }
106
107 /**
108 * Fetches a rows from the database by given SQL statement snippets
109 *
110 * @author Jochen Rau <jochen.rau@typoplanet.de>
111 */
112 private function fetchManyToMany($parentObject, $foreignTableName, $relationTableName, $where = '1=1', $groupBy = NULL, $orderBy = NULL, $limit = NULL) {
113 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
114 $foreignTableName . '.*, ' . $relationTableName . '.*',
115 $foreignTableName . ' LEFT JOIN ' . $relationTableName . ' ON (' . $foreignTableName . '.uid=' . $relationTableName . '.uid_foreign)',
116 $where . ' AND ' . $relationTableName . '.uid_local=' . intval($parentObject->getUid()) . $this->cObj->enableFields($foreignTableName) . $this->cObj->enableFields($foreignTableName),
117 $groupBy,
118 $orderBy,
119 $limit
120 );
121 // TODO language overlay; workspace overlay
122 return $rows ? $rows : array();
123 }
124
125 /**
126 * Dispatches the reconstitution of a domain object to an appropriate method
127 *
128 * @param array $rows The rows array fetched from the database
129 * @throws TX_EXTMVC_Persistence_Exception_RecursionTooDeep
130 * @return array An array of reconstituted domain objects
131 * @author Jochen Rau <jochen.rau@typoplanet.de>
132 */
133 protected function reconstituteObjects($className, array $rows, $depth = 0) {
134 if ($depth > 10) throw new TX_EXTMVC_Persistence_Exception_RecursionTooDeep('The maximum depth of ' . $depth . ' recursions was reached.', 1233352348);
135 foreach ($rows as $row) {
136 $object = $this->reconstituteObject($className, $row);
137 foreach ($this->getOneToManyRelations($className) as $propertyName => $tcaColumnConfiguration) {
138 $relatedRows = $this->fetchOneToMany($object, $tcaColumnConfiguration['foreign_field'], $tcaColumnConfiguration['foreign_table']);
139 $relatedObjects = $this->reconstituteObjects($tcaColumnConfiguration['foreign_class'], $relatedRows, ++$depth);
140 $object->_reconstituteProperty($propertyName, $relatedObjects);
141 }
142 foreach ($this->getManyToManyRelations($className) as $propertyName => $tcaColumnConfiguration) {
143 $relatedRows = $this->fetchManyToMany($object, $tcaColumnConfiguration['foreign_table'], $tcaColumnConfiguration['MM']);
144 $relatedObjects = $this->reconstituteObjects($tcaColumnConfiguration['foreign_class'], $relatedRows, ++$depth);
145 $object->_reconstituteProperty($propertyName, $relatedObjects);
146 }
147 $this->session->registerReconstitutedObject($object);
148 $objects[] = $object;
149 }
150 return $objects;
151 }
152
153 /**
154 * Reconstitutes the specified object and fills it with the given properties.
155 *
156 * @param string $objectName Name of the object to reconstitute
157 * @param array $properties The names of properties and their values which should be set during the reconstitution
158 * @return object The reconstituted object
159 * @author Robert Lemke <robert@typo3.org>
160 */
161 protected function reconstituteObject($className, array $properties = array()) {
162 // those objects will be fetched from within the __wakeup() method of the object...
163 $GLOBALS['EXTMVC']['reconstituteObject']['properties'] = $properties;
164 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
165 unset($GLOBALS['EXTMVC']['reconstituteObject']);
166 return $object;
167 }
168
169 public function persistAll($session) {
170 $this->session = $session;
171 $this->persistAggregateRoots();
172
173 foreach ($this->session->getRemovedObjects() as $object) {
174 $this->delete($object);
175 $this->session->unregisterRemovedObject($object);
176 }
177
178 $this->save();
179 }
180
181 /**
182 * Traverse all aggregate roots breadth first.
183 *
184 * @return void
185 * @author Karsten Dambekalns <karsten@typo3.org>
186 * @author Jochen Rau <jochen.rau@typoplanet.de>
187 */
188 protected function persistAggregateRoots() {
189 $aggregateRootClassNames = $this->session->getAggregateRootClassNames();
190 // make sure we have a corresponding node for all new objects on
191 // first level
192 foreach ($aggregateRootClassNames as $className) {
193 $addedObjects = $this->session->getAddedObjects($className);
194 foreach ($addedObjects as $object) {
195 $this->persistObject($object);
196 $this->session->unregisterAddedObject($object);
197 }
198 }
199
200 // // now traverse into the objects
201 // foreach ($aggregateRootClassNames as $object) {
202 // $this->persistObject($object);
203 // }
204
205 }
206
207 /**
208 * Persists an object to the database.
209 *
210 * @return void
211 * @author Karsten Dambekalns <karsten@typo3.org>
212 * @author Jochen Rau <jochen.rau@typoplanet.de>
213 */
214 public function persistObject(TX_EXTMVC_DomainObject_AbstractDomainObject $object) {
215 $queue = array();
216 $row = array(
217 'pid' => 0, // FIXME
218 'tstamp' => time(),
219 );
220 $properties = $object->_getProperties();
221 foreach ($properties as $propertyName => $propertyValue) {
222 if ($this->isPersistable(get_class($object), $propertyName)) {
223 if ($this->isRelation(get_class($object), $propertyName)) {
224 if (!$this->session->isReconstitutedObject($object) || $this->session->isDirtyObject($object)) {
225 $this->persistArray($object, $propertyName, $propertyValue, $queue);
226 $row[TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName)] = count($properties[$propertyName]);
227 } else {
228 $queue = array_merge($queue, array_values($propertyValue));
229 }
230 } elseif (is_array($propertyValue)) {
231 $this->persistArray($object, $propertyName, $propertyValue, $queue);
232 } elseif ($propertyValue instanceof TX_EXTMVC_DomainObject_AbstractDomainObject) {
233 if (!$this->session->isReconstitutedObject($object)) {
234 $this->persistObject($propertyValue);
235 }
236 $queue[] = $propertyValue;
237 } else {
238 // TODO Property Mapper
239 $row[TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName)] = $propertyValue;
240 }
241 }
242 }
243
244 $tableName = $this->getTableName(get_class($object));
245 $res = $GLOBALS['TYPO3_DB']->exec_INSERTquery(
246 $tableName,
247 $row
248 );
249
250 $object->_reconstituteProperty('uid', $GLOBALS['TYPO3_DB']->sql_insert_id());
251 $this->session->unregisterObject($object);
252 $this->session->registerReconstitutedObject($object);
253 // var_dump($object);
254
255 // here we loop over the objects. their nodes are already at the
256 // right place and have the right name. fancy, eh?
257 foreach ($queue as $object) {
258 $this->persistObject($object);
259 }
260 }
261
262 /**
263 * Store an array as a node of type flow3:arrayPropertyProxy, with each
264 * array element becoming a property named like the key and the value.
265 *
266 * Every element not being an object or array will become a property on the
267 * node, arrays will be handled recursively.
268 *
269 * Note: Objects contained in the array will have a node created, properties
270 * On those nodes must be set elsewhere!
271 *
272 * @param array $array The array for which to create a node
273 * @param \F3\PHPCR\NodeInterface $parentNode The node to add the property proxy to
274 * @param string $nodeName The name to use for the object, must be a legal name as per JSR-283
275 * @param array &$queue Found entities are accumulated here.
276 * @author Karsten Dambekalns <karsten@typo3.org>
277 */
278 protected function persistArray(TX_EXTMVC_DomainObject_AbstractDomainObject $parentObject, $propertyName, array $array, array &$queue) {
279 foreach ($array as $key => $element) {
280 if ($element instanceof TX_EXTMVC_DomainObject_AbstractDomainObject) {
281 if (!$this->session->isReconstitutedObject($element) || $this->session->isDirtyObject($element)) {
282 $this->persistObject($element);
283 }
284 } elseif (is_array($element)) {
285 $this->persistArray($parentObject, $propertyName, $element, $queue);
286 } else {
287 $queue[] = $element;
288 }
289 // TODO persist arrays with plain values
290
291 }
292 }
293
294 /**
295 * Deletes all removed objects from the database.
296 *
297 * @return void
298 * @author Jochen Rau <jochen.rau@typoplanet.de>
299 */
300 protected function processRemovedObject($object) {
301 }
302
303 /**
304 * Updates an object
305 *
306 * @return void
307 * @author Jochen Rau <jochen.rau@typoplanet.de>
308 */
309 public function update(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $depth = 0) {
310 if ($depth > 10) throw new TX_EXTMVC_Persistence_Exception_RecursionTooDeep('The maximum depth of ' . $depth . ' recursions was reached.', 1233352348);
311 $row = array(
312 'tstamp' => time(),
313 );
314 $properties = $object->_getProperties();
315 $columns = $this->getColumns($this->getClassName($object));
316 $relations = $this->getRelations($this->getClassName($object));
317 foreach ($relations as $propertyName => $tcaColumnConfiguration) {
318 foreach ($properties[$propertyName] as $object) {
319 // TODO implement reverse update chain
320 if (TRUE || $object->_isDirty()) {
321 $this->update($object, ++$depth);
322 }
323 }
324 $row[TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName)] = count($properties[$propertyName]);
325 unset($properties[$propertyName]);
326 }
327 foreach ($properties as $propertyName => $propertyValue) {
328 $row[TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName)] = $propertyValue;
329 }
330 $uid = $object->getUid();
331 // debug($uid);
332 // debug($row);
333 // $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
334 // $this->getTableName($this->getClassName($object)),
335 // 'uid=' . $object->getUid(),
336 // $row
337 // );
338 }
339
340 /**
341 * Deletes an object
342 *
343 * @return void
344 * @author Jochen Rau <jochen.rau@typoplanet.de>
345 */
346 public function delete(TX_EXTMVC_DomainObject_AbstractDomainObject $object, $onlyMarkAsDeleted = TRUE) {
347 $tableName = $this->getTableName($this->getClassName($object));
348 if ($onlyMarkAsDeleted) {
349 $deletedColumnName = $this->getDeletedColumnName($tableName);
350 if (empty($deletedColumnName)) throw new Exception('Could not mark object as deleted in table "' . $tableName . '"');
351 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
352 $this->getTableName($object),
353 'uid = ' . intval($object->getUid()),
354 array($deletedColumnName => 1)
355 );
356 } else {
357 // TODO remove associated objects
358
359 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
360 $this->getTableName($object),
361 'uid=' . intval($object->getUid())
362 );
363 }
364 }
365
366 protected function getColumns($className) {
367 $tableName = $this->getTableName($className);
368 t3lib_div::loadTCA($tableName);
369 return $GLOBALS['TCA'][$tableName]['columns'];
370 }
371
372 protected function getClassName(TX_EXTMVC_DomainObject_AbstractDomainObject $object) {
373 return get_class($object);
374 }
375
376 protected function getTableName($className) {
377 // TODO implement table name aliases
378 return strtolower($className);
379 }
380
381 protected function getDeletedColumnName($className) {
382 $this->getTableName($className);
383 return $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
384 }
385
386 protected function getHiddenColumnName($className) {;
387 $this->getTableName($className);
388 return $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns']['disabled'];
389 }
390
391 protected function getRelations($className) {
392 return t3lib_div::array_merge_recursive_overrule($this->getOneToManyRelations($className), $this->getManyToManyRelations($className));
393 }
394
395 protected function isRelation($className, $propertyName) {
396 $columns = $this->getColumns($className);
397 if (array_key_exists('foreign_table', $columns[TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName)]['config'])) return TRUE;
398 return FALSE;
399 }
400
401 protected function getOneToManyRelations($className) {
402 $columns = $this->getColumns($className);
403 $oneToManyRelations = array();
404 foreach ($columns as $columnName => $columnConfiguration) {
405 $propertyName = TX_EXTMVC_Utility_Strings::underscoredToLowerCamelCase($columnName);
406 if (array_key_exists('foreign_table', $columnConfiguration['config'])) {
407 // TODO take IRRE into account
408 if (!array_key_exists('MM', $columnConfiguration['config'])) {
409 // TODO implement a $TCA object
410 $oneToManyRelations[$propertyName] = array(
411 'foreign_class' => $columnConfiguration['config']['foreign_class'],
412 'foreign_table' => $columnConfiguration['config']['foreign_table'],
413 'foreign_field' => $columnConfiguration['config']['foreign_field'],
414 'foreign_table_field' => $columnConfiguration['config']['foreign_table_field']
415 );
416 }
417 }
418 }
419 return $oneToManyRelations;
420 }
421
422 protected function getManyToManyRelations($className) {
423 $columns = $this->getColumns($className);
424 $relations = array();
425 foreach ($columns as $columnName => $columnConfiguration) {
426 $propertyName = TX_EXTMVC_Utility_Strings::underscoredToLowerCamelCase($columnName);
427 if (array_key_exists('foreign_table', $columnConfiguration['config'])) {
428 // TODO take IRRE into account
429 if (array_key_exists('MM', $columnConfiguration['config'])) {
430 // TODO implement a $TCA object
431 $relations[$propertyName] = array(
432 'foreign_class' => $columnConfiguration['config']['foreign_class'],
433 'foreign_table' => $columnConfiguration['config']['foreign_table'],
434 'MM' => $columnConfiguration['config']['MM']
435 );
436 }
437 }
438 }
439 return $relations;
440 }
441
442 public function isPersistable($className, $propertyName) {
443 $columns = $this->getColumns($className);
444 if (array_key_exists(TX_EXTMVC_Utility_Strings::camelCaseToLowerCaseUnderscored($propertyName), $columns)) return TRUE;
445 return FALSE;
446 }
447
448 }
449 ?>