[BUGFIX] Follow up foreign_match_fields not fully supported
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Backend.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
8 * Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
9 * All rights reserved
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 * A copy is found in the text file GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30
31 use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
32 use TYPO3\CMS\Extbase\Persistence\ObjectMonitoringInterface;
33
34 /**
35 * A persistence backend. This backend maps objects to the relational model of the storage backend.
36 * It persists all added, removed and changed objects.
37 */
38 class Backend implements \TYPO3\CMS\Extbase\Persistence\Generic\BackendInterface, \TYPO3\CMS\Core\SingletonInterface {
39
40 /**
41 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session
42 * @inject
43 */
44 protected $session;
45
46 /**
47 * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
48 */
49 protected $persistenceManager;
50
51 /**
52 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
53 */
54 protected $aggregateRootObjects;
55
56 /**
57 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
58 */
59 protected $deletedEntities;
60
61 /**
62 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
63 */
64 protected $changedEntities;
65
66 /**
67 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
68 */
69 protected $visitedDuringPersistence;
70
71 /**
72 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
73 * @inject
74 */
75 protected $reflectionService;
76
77 /**
78 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
79 * @inject
80 */
81 protected $qomFactory;
82
83 /**
84 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface
85 * @inject
86 */
87 protected $storageBackend;
88
89 /**
90 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
91 * @inject
92 */
93 protected $dataMapper;
94
95 /**
96 * The TYPO3 reference index object
97 *
98 * @var \TYPO3\CMS\Core\Database\ReferenceIndex
99 */
100 protected $referenceIndex;
101
102 /**
103 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
104 */
105 protected $configurationManager;
106
107 /**
108 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
109 * @inject
110 */
111 protected $signalSlotDispatcher;
112
113 /**
114 * Constructs the backend
115 *
116 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
117 */
118 public function __construct(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager) {
119 $this->configurationManager = $configurationManager;
120 $this->referenceIndex = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
121 $this->aggregateRootObjects = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
122 $this->deletedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
123 $this->changedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
124 }
125
126 /**
127 * @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
128 */
129 public function setPersistenceManager(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager) {
130 $this->persistenceManager = $persistenceManager;
131 }
132
133 /**
134 * Returns the repository session
135 *
136 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Session
137 */
138 public function getSession() {
139 return $this->session;
140 }
141
142 /**
143 * Returns the Data Mapper
144 *
145 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
146 */
147 public function getDataMapper() {
148 return $this->dataMapper;
149 }
150
151 /**
152 * Returns the current QOM factory
153 *
154 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
155 */
156 public function getQomFactory() {
157 return $this->qomFactory;
158 }
159
160 /**
161 * Returns the reflection service
162 *
163 * @return \TYPO3\CMS\Extbase\Reflection\ReflectionService
164 */
165 public function getReflectionService() {
166 return $this->reflectionService;
167 }
168
169 /**
170 * Returns the number of records matching the query.
171 *
172 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
173 * @return integer
174 * @api
175 */
176 public function getObjectCountByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
177 return $this->storageBackend->getObjectCountByQuery($query);
178 }
179
180 /**
181 * Returns the object data matching the $query.
182 *
183 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
184 * @return array
185 * @api
186 */
187 public function getObjectDataByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
188 $signalResult = $this->signalSlotDispatcher->dispatch(__CLASS__, 'beforeGettingObjectData', array($query));
189 list($query) = $signalResult;
190 $result = $this->storageBackend->getObjectDataByQuery($query);
191 list($query, $result) = $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterGettingObjectData', array($query, $result));
192 return $result;
193 }
194
195 /**
196 * Returns the (internal) identifier for the object, if it is known to the
197 * backend. Otherwise NULL is returned.
198 *
199 * @param object $object
200 * @return string The identifier for the object if it is known, or NULL
201 */
202 public function getIdentifierByObject($object) {
203 if ($object instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
204 $object = $object->_loadRealInstance();
205 if (!is_object($object)) {
206 return NULL;
207 }
208 }
209 return $this->session->getIdentifierByObject($object);
210 }
211
212 /**
213 * Returns the object with the (internal) identifier, if it is known to the
214 * backend. Otherwise NULL is returned.
215 *
216 * @param string $identifier
217 * @param string $className
218 * @return object The object for the identifier if it is known, or NULL
219 */
220 public function getObjectByIdentifier($identifier, $className) {
221 if ($this->session->hasIdentifier($identifier, $className)) {
222 return $this->session->getObjectByIdentifier($identifier, $className);
223 } else {
224 $query = $this->persistenceManager->createQueryForType($className);
225 $query->getQuerySettings()->setRespectStoragePage(FALSE);
226 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
227 return $query->matching($query->equals('uid', $identifier))->execute()->getFirst();
228 }
229 }
230
231 /**
232 * Checks if the given object has ever been persisted.
233 *
234 * @param object $object The object to check
235 * @return boolean TRUE if the object is new, FALSE if the object exists in the repository
236 */
237 public function isNewObject($object) {
238 return $this->getIdentifierByObject($object) === NULL;
239 }
240
241 /**
242 * Replaces the given object by the second object.
243 *
244 * This method will unregister the existing object at the identity map and
245 * register the new object instead. The existing object must therefore
246 * already be registered at the identity map which is the case for all
247 * reconstituted objects.
248 *
249 * The new object will be identified by the uid which formerly belonged
250 * to the existing object. The existing object looses its uid.
251 *
252 * @param object $existingObject The existing object
253 * @param object $newObject The new object
254 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException
255 * @return void
256 * @deprecated since 6.1, will be removed two versions later
257 */
258 public function replaceObject($existingObject, $newObject) {
259 $this->session->replaceReconstitutedEntity($existingObject, $newObject);
260 }
261
262 /**
263 * Sets the aggregate root objects
264 *
265 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects
266 * @return void
267 */
268 public function setAggregateRootObjects(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects) {
269 $this->aggregateRootObjects = $objects;
270 }
271
272 /**
273 * Sets the changed objects
274 *
275 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
276 * @return void
277 */
278 public function setChangedEntities(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities) {
279 $this->changedEntities = $entities;
280 }
281
282 /**
283 * Sets the deleted objects
284 *
285 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
286 * @return void
287 */
288 public function setDeletedEntities(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities) {
289 $this->deletedEntities = $entities;
290 }
291
292 /**
293 * Commits the current persistence session.
294 *
295 * @return void
296 */
297 public function commit() {
298 $this->persistObjects();
299 $this->processDeletedObjects();
300 }
301
302 /**
303 * Sets the deleted objects
304 *
305 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects
306 * @return void
307 * @deprecated since 6.1, will be removed two versions later
308 */
309 public function setDeletedObjects(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects) {
310 $this->setDeletedEntities($objects);
311 }
312
313 /**
314 * Traverse and persist all aggregate roots and their object graph.
315 *
316 * @return void
317 */
318 protected function persistObjects() {
319 $this->visitedDuringPersistence = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
320 foreach ($this->aggregateRootObjects as $object) {
321 /** @var DomainObjectInterface $object */
322 if ($object->_isNew()) {
323 $this->insertObject($object);
324 }
325 $this->persistObject($object, NULL);
326 }
327 foreach ($this->changedEntities as $object) {
328 $this->persistObject($object, NULL);
329 }
330 }
331
332 /**
333 * Persists the given object.
334 *
335 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be inserted
336 * @return void
337 */
338 protected function persistObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object) {
339 if (isset($this->visitedDuringPersistence[$object])) {
340 return;
341 }
342 $row = array();
343 $queue = array();
344 $dataMap = $this->dataMapper->getDataMap(get_class($object));
345 $properties = $object->_getProperties();
346 foreach ($properties as $propertyName => $propertyValue) {
347 if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
348 continue;
349 }
350 $columnMap = $dataMap->getColumnMap($propertyName);
351 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\ObjectStorage) {
352 $cleanProperty = $object->_getCleanProperty($propertyName);
353 // objectstorage needs to be persisted if the object is new, the objectstorge is dirty, meaning it has
354 // been changed after initial build, or a empty objectstorge is present and the cleanstate objectstorage
355 // has childelements, meaning all elements should been removed from the objectstorage
356 if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() == 0 && $cleanProperty && $cleanProperty->count() > 0)) {
357 $this->persistObjectStorage($propertyValue, $object, $propertyName, $row);
358 $propertyValue->_memorizeCleanState();
359 }
360 foreach ($propertyValue as $containedObject) {
361 if ($containedObject instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
362 $queue[] = $containedObject;
363 }
364 }
365 } elseif ($propertyValue instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface
366 && $object instanceof ObjectMonitoringInterface) {
367 if ($object->_isDirty($propertyName)) {
368 if ($propertyValue->_isNew()) {
369 $this->insertObject($propertyValue, $object, $propertyName);
370 }
371 $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue);
372 }
373 $queue[] = $propertyValue;
374 } elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
375 $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
376 }
377 }
378 if (count($row) > 0) {
379 $this->updateObject($object, $row);
380 $object->_memorizeCleanState();
381 }
382 $this->visitedDuringPersistence[$object] = $object->getUid();
383 foreach ($queue as $queuedObject) {
384 $this->persistObject($queuedObject);
385 }
386 }
387
388 /**
389 * Checks, if the property value is lazy loaded and was not initialized
390 *
391 * @param mixed $propertyValue The property value
392 * @return boolean
393 */
394 protected function propertyValueIsLazyLoaded($propertyValue) {
395 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
396 return TRUE;
397 }
398 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage) {
399 if ($propertyValue->isInitialized() === FALSE) {
400 return TRUE;
401 }
402 }
403 return FALSE;
404 }
405
406 /**
407 * Persists a an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object. A 1:1 relation
408 * gets persisted immediately. Objects which were removed from the property were detached from the parent object. They will not be
409 * deleted by default. You have to annotate the property with "@cascade remove" if you want them to be deleted as well.
410 *
411 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted.
412 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage.
413 * @param string $propertyName The name of the property holding the object storage.
414 * @param array &$row The row array of the parent object to be persisted. It's passed by reference and gets filled with either a comma separated list of uids (csv) or the number of contained objects.
415 * @return void
416 */
417 protected function persistObjectStorage(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, array &$row) {
418 $className = get_class($parentObject);
419 $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
420 $propertyMetaData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
421 foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) {
422 $this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName);
423 if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY && $propertyMetaData['cascade'] === 'remove') {
424 $this->removeEntity($removedObject);
425 }
426 }
427
428 $currentUids = array();
429 $sortingPosition = 1;
430 $updateSortingOfFollowing = FALSE;
431
432 foreach ($objectStorage as $object) {
433 /** @var DomainObjectInterface $object */
434 if (empty($currentUids)) {
435 $sortingPosition = 1;
436 } else {
437 $sortingPosition++;
438 }
439 $cleanProperty = $parentObject->_getCleanProperty($propertyName);
440 if ($object->_isNew()) {
441 $this->insertObject($object);
442 $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
443 // if a new object is inserted, all objects after this need to have their sorting updated
444 $updateSortingOfFollowing = TRUE;
445 } elseif ($cleanProperty === NULL || $cleanProperty->getPosition($object) === NULL) {
446 // if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null
447 $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
448 // if a relation is dirty (speaking the same object is removed and added again at a different position), all objects after this needs to be updated the sorting
449 $updateSortingOfFollowing = TRUE;
450 } elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) {
451 $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
452 $updateSortingOfFollowing = TRUE;
453 } elseif ($updateSortingOfFollowing) {
454 if ($sortingPosition > $objectStorage->getPosition($object)) {
455 $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
456 } else {
457 $sortingPosition = $objectStorage->getPosition($object);
458 }
459 }
460 $currentUids[] = $object->getUid();
461 }
462
463 if ($columnMap->getParentKeyFieldName() === NULL) {
464 $row[$columnMap->getColumnName()] = implode(',', $currentUids);
465 } else {
466 $row[$columnMap->getColumnName()] = $this->dataMapper->countRelated($parentObject, $propertyName);
467 }
468 }
469
470 /**
471 * Returns the removed objects determined by a comparison of the clean property value
472 * with the actual property value.
473 *
474 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object
475 * @param string $propertyName
476 * @return array An array of removed objects
477 */
478 protected function getRemovedChildObjects(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, $propertyName) {
479 $removedObjects = array();
480 $cleanPropertyValue = $object->_getCleanProperty($propertyName);
481 if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) {
482 $propertyValue = $object->_getProperty($propertyName);
483 foreach ($cleanPropertyValue as $containedObject) {
484 if (!$propertyValue->contains($containedObject)) {
485 $removedObjects[] = $containedObject;
486 }
487 }
488 }
489 return $removedObjects;
490 }
491
492 /**
493 * Updates the fields defining the relation between the object and the parent object.
494 *
495 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
496 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject
497 * @param string $parentPropertyName
498 * @param integer $sortingPosition
499 * @return void
500 */
501 protected function attachObjectToParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName, $sortingPosition = 0) {
502 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
503 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
504 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
505 $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
506 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
507 $this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition);
508 }
509 }
510
511 /**
512 * Updates the fields defining the relation between the object and the parent object.
513 *
514 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
515 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject
516 * @param string $parentPropertyName
517 * @param integer $sortingPosition
518 * @return void
519 */
520 protected function updateRelationOfObjectToParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject, $parentPropertyName, $sortingPosition = 0) {
521 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
522 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
523 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
524 $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
525 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
526 $this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition);
527 }
528 }
529
530 /**
531 * Updates fields defining the relation between the object and the parent object in relation has-many.
532 *
533 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
534 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject
535 * @param string $parentPropertyName
536 * @param integer $sortingPosition
537 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException
538 * @return void
539 */
540 protected function attachObjectToParentObjectRelationHasMany(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject, $parentPropertyName, $sortingPosition = 0) {
541 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
542 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
543 if ($parentColumnMap->getTypeOfRelation() !== \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
544 throw new \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException(
545 'Parent column relation type is ' . $parentColumnMap->getTypeOfRelation() .
546 ' but should be ' . \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY,
547 1345368105
548 );
549 }
550 $row = array();
551 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
552 if ($parentKeyFieldName !== NULL) {
553 $row[$parentKeyFieldName] = $parentObject->getUid();
554 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
555 if ($parentTableFieldName !== NULL) {
556 $row[$parentTableFieldName] = $parentDataMap->getTableName();
557 }
558 $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
559 if (is_array($relationTableMatchFields) && count($relationTableMatchFields)) {
560 $row = array_merge($relationTableMatchFields, $row);
561 }
562 }
563 $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
564 if (!empty($childSortByFieldName)) {
565 $row[$childSortByFieldName] = $sortingPosition;
566 }
567 if (!empty($row)) {
568 $this->updateObject($object, $row);
569 }
570 }
571
572 /**
573 * Updates the fields defining the relation between the object and the parent object.
574 *
575 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
576 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject
577 * @param string $parentPropertyName
578 * @return void
579 */
580 protected function detachObjectFromParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
581 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
582 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
583 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
584 $row = array();
585 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
586 if ($parentKeyFieldName !== NULL) {
587 $row[$parentKeyFieldName] = '';
588 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
589 if ($parentTableFieldName !== NULL) {
590 $row[$parentTableFieldName] = '';
591 }
592 $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
593 if (is_array($relationTableMatchFields) && count($relationTableMatchFields)) {
594 $row = array_merge(array_fill_keys(array_keys($relationTableMatchFields), ''), $row);
595 }
596 }
597 $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
598 if (!empty($childSortByFieldName)) {
599 $row[$childSortByFieldName] = 0;
600 }
601 if (count($row) > 0) {
602 $this->updateObject($object, $row);
603 }
604 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
605 $this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName);
606 }
607 }
608
609 /**
610 * Inserts an object in the storage backend
611 *
612 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be insterted in the storage
613 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parentobject.
614 * @param string $parentPropertyName
615 * @return void
616 */
617 protected function insertObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject = NULL, $parentPropertyName = '') {
618 if ($object instanceof \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject) {
619 $result = $this->getUidOfAlreadyPersistedValueObject($object);
620 if ($result !== FALSE) {
621 $object->_setProperty('uid', (int)$result);
622 return;
623 }
624 }
625 $dataMap = $this->dataMapper->getDataMap(get_class($object));
626 $row = array();
627 $this->addCommonFieldsToRow($object, $row);
628 if ($dataMap->getLanguageIdColumnName() !== NULL) {
629 $row[$dataMap->getLanguageIdColumnName()] = -1;
630 }
631 if ($parentObject !== NULL && $parentPropertyName) {
632 $parentColumnDataMap = $this->dataMapper->getDataMap(get_class($parentObject))->getColumnMap($parentPropertyName);
633 $relationTableMatchFields = $parentColumnDataMap->getRelationTableMatchFields();
634 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
635 $row = array_merge($relationTableMatchFields, $row);
636 }
637 if ($parentColumnDataMap->getParentKeyFieldName() !== NULL) {
638 $row[$parentColumnDataMap->getParentKeyFieldName()] = (int)$parentObject->getUid();
639 }
640 }
641 $uid = $this->storageBackend->addRow($dataMap->getTableName(), $row);
642 $object->_setProperty('uid', (int)$uid);
643 if ((int)$uid >= 1) {
644 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterInsertObject', array('object' => $object));
645 }
646 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
647 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
648 $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $uid);
649 }
650 $this->session->registerObject($object, $uid);
651 }
652
653 /**
654 * Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid.
655 *
656 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The object to be tested
657 * @return mixed The matching uid if an object was found, else FALSE
658 */
659 protected function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object) {
660 return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object);
661 }
662
663 /**
664 * Inserts mm-relation into a relation table
665 *
666 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The related object
667 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
668 * @param string $propertyName The name of the parent object's property where the related objects are stored in
669 * @param integer $sortingPosition Defaults to NULL
670 * @return integer The uid of the inserted row
671 */
672 protected function insertRelationInRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, $sortingPosition = NULL) {
673 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
674 $columnMap = $dataMap->getColumnMap($propertyName);
675 $row = array(
676 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
677 $columnMap->getChildKeyFieldName() => (int)$object->getUid(),
678 $columnMap->getChildSortByFieldName() => !is_null($sortingPosition) ? (int)$sortingPosition : 0
679 );
680 $relationTableName = $columnMap->getRelationTableName();
681 // FIXME Reenable support for tablenames
682 // $childTableName = $columnMap->getChildTableName();
683 // if (isset($childTableName)) {
684 // $row['tablenames'] = $childTableName;
685 // }
686 if ($columnMap->getRelationTablePageIdColumnName() !== NULL) {
687 $row[$columnMap->getRelationTablePageIdColumnName()] = $this->determineStoragePageIdForNewRecord();
688 }
689 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
690 if (count($relationTableMatchFields)) {
691 foreach ($relationTableMatchFields as $matchField => $matchValue) {
692 $row[$matchField] = $matchValue;
693 }
694 }
695 $relationTableInsertFields = $columnMap->getRelationTableInsertFields();
696 if (count($relationTableInsertFields)) {
697 foreach ($relationTableInsertFields as $insertField => $insertValue) {
698 $row[$insertField] = $insertValue;
699 }
700 }
701 $res = $this->storageBackend->addRow($relationTableName, $row, TRUE);
702 return $res;
703 }
704
705 /**
706 * Inserts mm-relation into a relation table
707 *
708 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The related object
709 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
710 * @param string $propertyName The name of the parent object's property where the related objects are stored in
711 * @param integer $sortingPosition Defaults to NULL
712 * @return integer The uid of the inserted row
713 */
714 protected function updateRelationInRelationTable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, $sortingPosition = 0) {
715 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
716 $columnMap = $dataMap->getColumnMap($propertyName);
717 $row = array(
718 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
719 $columnMap->getChildKeyFieldName() => (int)$object->getUid(),
720 $columnMap->getChildSortByFieldName() => (int)$sortingPosition
721 );
722 $relationTableName = $columnMap->getRelationTableName();
723 // FIXME Reenable support for tablenames
724 // $childTableName = $columnMap->getChildTableName();
725 // if (isset($childTableName)) {
726 // $row['tablenames'] = $childTableName;
727 // }
728
729 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
730 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
731 $row = array_merge($relationTableMatchFields, $row);
732 }
733 $res = $this->storageBackend->updateRelationTableRow(
734 $relationTableName,
735 $row);
736 return $res;
737 }
738
739 /**
740 * Delete all mm-relations of a parent from a relation table
741 *
742 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
743 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
744 * @return boolean
745 */
746 protected function deleteAllRelationsFromRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
747 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
748 $columnMap = $dataMap->getColumnMap($parentPropertyName);
749 $relationTableName = $columnMap->getRelationTableName();
750 $relationMatchFields = array(
751 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid()
752 );
753 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
754 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
755 $relationMatchFields = array_merge($relationTableMatchFields, $relationMatchFields);
756 }
757 $res = $this->storageBackend->removeRow($relationTableName, $relationMatchFields, FALSE);
758 return $res;
759 }
760
761 /**
762 * Delete an mm-relation from a relation table
763 *
764 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $relatedObject The related object
765 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
766 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
767 * @return boolean
768 */
769 protected function deleteRelationFromRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $relatedObject, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
770 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
771 $columnMap = $dataMap->getColumnMap($parentPropertyName);
772 $relationTableName = $columnMap->getRelationTableName();
773 $res = $this->storageBackend->removeRow($relationTableName, array(
774 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
775 $columnMap->getChildKeyFieldName() => (int)$relatedObject->getUid()
776 ), FALSE);
777 return $res;
778 }
779
780 /**
781 * Fetches maximal value currently used for sorting field in parent table
782 *
783 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
784 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
785 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException
786 * @return mixed the max value
787 */
788 protected function fetchMaxSortingFromParentTable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
789 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
790 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
791 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
792 $tableName = $parentColumnMap->getChildTableName();
793 $sortByFieldName = $parentColumnMap->getChildSortByFieldName();
794
795 if (empty($sortByFieldName)) {
796 return FALSE;
797 }
798 $matchFields = array();
799 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
800 if ($parentKeyFieldName !== NULL) {
801 $matchFields[$parentKeyFieldName] = $parentObject->getUid();
802 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
803 if ($parentTableFieldName !== NULL) {
804 $matchFields[$parentTableFieldName] = $parentDataMap->getTableName();
805 }
806 }
807
808 if (empty($matchFields)) {
809 return FALSE;
810 }
811 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
812 $tableName = $parentColumnMap->getRelationTableName();
813 $sortByFieldName = $parentColumnMap->getChildSortByFieldName();
814
815 $matchFields = array(
816 $parentColumnMap->getParentKeyFieldName() => (int)$parentObject->getUid()
817 );
818
819 $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
820 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
821 $matchFields = array_merge($relationTableMatchFields, $matchFields);
822 }
823 } else {
824 throw new \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException('Unexpected parent column relation type:' . $parentColumnMap->getTypeOfRelation(), 1345368106);
825 }
826
827 $result = $this->storageBackend->getMaxValueFromTable(
828 $tableName,
829 $matchFields,
830 $sortByFieldName);
831 return $result;
832 }
833
834 /**
835 * Updates a given object in the storage
836 *
837 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be updated
838 * @param array $row Row to be stored
839 * @return boolean
840 */
841 protected function updateObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array $row) {
842 $dataMap = $this->dataMapper->getDataMap(get_class($object));
843 $this->addCommonFieldsToRow($object, $row);
844 $row['uid'] = $object->getUid();
845 if ($dataMap->getLanguageIdColumnName() !== NULL) {
846 $row[$dataMap->getLanguageIdColumnName()] = $object->_getProperty('_languageUid');
847 if ($object->_getProperty('_localizedUid') !== NULL) {
848 $row['uid'] = $object->_getProperty('_localizedUid');
849 }
850 }
851 $res = $this->storageBackend->updateRow($dataMap->getTableName(), $row);
852 if ($res === TRUE) {
853 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterUpdateObject', array('object' => $object));
854 }
855 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
856 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
857 $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $row['uid']);
858 }
859 return $res;
860 }
861
862 /**
863 * Adds common databse fields to a row
864 *
865 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
866 * @param array &$row
867 * @return void
868 */
869 protected function addCommonFieldsToRow(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array &$row) {
870 $dataMap = $this->dataMapper->getDataMap(get_class($object));
871 $this->addCommonDateFieldsToRow($object, $row);
872 if ($dataMap->getRecordTypeColumnName() !== NULL && $dataMap->getRecordType() !== NULL) {
873 $row[$dataMap->getRecordTypeColumnName()] = $dataMap->getRecordType();
874 }
875 if ($object->_isNew() && !isset($row['pid'])) {
876 $row['pid'] = $this->determineStoragePageIdForNewRecord($object);
877 }
878 }
879
880 /**
881 * Adjustes the common date fields of the given row to the current time
882 *
883 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
884 * @param array &$row The row to be updated
885 * @return void
886 */
887 protected function addCommonDateFieldsToRow(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array &$row) {
888 $dataMap = $this->dataMapper->getDataMap(get_class($object));
889 if ($object->_isNew() && $dataMap->getCreationDateColumnName() !== NULL) {
890 $row[$dataMap->getCreationDateColumnName()] = $GLOBALS['EXEC_TIME'];
891 }
892 if ($dataMap->getModificationDateColumnName() !== NULL) {
893 $row[$dataMap->getModificationDateColumnName()] = $GLOBALS['EXEC_TIME'];
894 }
895 }
896
897 /**
898 * Iterate over deleted aggregate root objects and process them
899 *
900 * @return void
901 */
902 protected function processDeletedObjects() {
903 foreach ($this->deletedEntities as $entity) {
904 if ($this->session->hasObject($entity)) {
905 $this->removeEntity($entity);
906 $this->session->unregisterReconstitutedEntity($entity);
907 $this->session->unregisterObject($entity);
908 }
909 }
910 $this->deletedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
911 }
912
913 /**
914 * Deletes an object
915 *
916 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be removed from the storage
917 * @param boolean $markAsDeleted Whether to just flag the row deleted (default) or really delete it
918 * @return void
919 */
920 protected function removeEntity(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, $markAsDeleted = TRUE) {
921 $dataMap = $this->dataMapper->getDataMap(get_class($object));
922 $tableName = $dataMap->getTableName();
923 if ($markAsDeleted === TRUE && $dataMap->getDeletedFlagColumnName() !== NULL) {
924 $deletedColumnName = $dataMap->getDeletedFlagColumnName();
925 $row = array(
926 'uid' => $object->getUid(),
927 $deletedColumnName => 1
928 );
929 $this->addCommonDateFieldsToRow($object, $row);
930 $res = $this->storageBackend->updateRow($tableName, $row);
931 } else {
932 $res = $this->storageBackend->removeRow($tableName, array('uid' => $object->getUid()));
933 }
934 if ($res === TRUE) {
935 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterRemoveObject', array('object' => $object));
936 }
937 $this->removeRelatedObjects($object);
938 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
939 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
940 $this->referenceIndex->updateRefIndexTable($tableName, $object->getUid());
941 }
942 }
943
944 /**
945 * Remove related objects
946 *
947 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to scanned for related objects
948 * @return void
949 */
950 protected function removeRelatedObjects(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object) {
951 $className = get_class($object);
952 $dataMap = $this->dataMapper->getDataMap($className);
953 $classSchema = $this->reflectionService->getClassSchema($className);
954 $properties = $object->_getProperties();
955 foreach ($properties as $propertyName => $propertyValue) {
956 $columnMap = $dataMap->getColumnMap($propertyName);
957 $propertyMetaData = $classSchema->getProperty($propertyName);
958 if ($propertyMetaData['cascade'] === 'remove') {
959 if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
960 foreach ($propertyValue as $containedObject) {
961 $this->removeEntity($containedObject);
962 }
963 } elseif ($propertyValue instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
964 $this->removeEntity($propertyValue);
965 }
966 }
967 }
968 }
969
970 /**
971 * Determine the storage page ID for a given NEW record
972 *
973 * This does the following:
974 * - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record.
975 * - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
976 * - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
977 *
978 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
979 * @return integer the storage Page ID where the object should be stored
980 */
981 protected function determineStoragePageIdForNewRecord(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object = NULL) {
982 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
983 if ($object !== NULL) {
984 if (\TYPO3\CMS\Extbase\Reflection\ObjectAccess::isPropertyGettable($object, 'pid')) {
985 $pid = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getProperty($object, 'pid');
986 if (isset($pid)) {
987 return (int)$pid;
988 }
989 }
990 $className = get_class($object);
991 if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) {
992 return (int)$frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'];
993 }
994 }
995 $storagePidList = \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode(',', $frameworkConfiguration['persistence']['storagePid']);
996 return (int)$storagePidList[0];
997 }
998
999 /**
1000 * Returns a plain value, i.e. objects are flattened out if possible.
1001 *
1002 * @param mixed $input
1003 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap $columnMap
1004 * @return mixed
1005 */
1006 protected function getPlainValue($input, $columnMap = NULL) {
1007 if ($input instanceof \DateTime) {
1008 if (!is_null($columnMap) && !is_null($columnMap->getDateTimeStorageFormat())) {
1009 if ($columnMap->getDateTimeStorageFormat() == 'datetime') {
1010 return $input->format('Y-m-d H:i:s');
1011 }
1012 if ($columnMap->getDateTimeStorageFormat() == 'date') {
1013 return $input->format('Y-m-d');
1014 }
1015 } else {
1016 return $input->format('U');
1017 }
1018 } elseif ($input instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
1019 return $input->getUid();
1020 } elseif ($input instanceof \TYPO3\CMS\Core\Type\TypeInterface) {
1021 return (string) $input;
1022 } elseif (is_bool($input)) {
1023 return $input === TRUE ? 1 : 0;
1024 } else {
1025 return $input;
1026 }
1027 return NULL;
1028 }
1029 }