aggregateRootObjects = new ObjectStorage(); $this->deletedEntities = new ObjectStorage(); $this->changedEntities = new ObjectStorage(); } public function setPersistenceManager(PersistenceManagerInterface $persistenceManager): void { $this->persistenceManager = $persistenceManager; } /** * Returns the number of records matching the query. * * @return int */ public function getObjectCountByQuery(QueryInterface $query) { $event = new ModifyQueryBeforeFetchingObjectCountEvent($query); $this->eventDispatcher->dispatch($event); $query = $event->getQuery(); $result = $this->storageBackend->getObjectCountByQuery($query); $event = new ModifyResultAfterFetchingObjectCountEvent($query, $result); $this->eventDispatcher->dispatch($event); return $event->getResult(); } /** * Returns the object data matching the $query. * * @return list> */ public function getObjectDataByQuery(QueryInterface $query) { $event = new ModifyQueryBeforeFetchingObjectDataEvent($query); $this->eventDispatcher->dispatch($event); $query = $event->getQuery(); $result = $this->storageBackend->getObjectDataByQuery($query); $event = new ModifyResultAfterFetchingObjectDataEvent($query, $result); $this->eventDispatcher->dispatch($event); return $event->getResult(); } /** * Returns the (internal) identifier for the object, if it is known to the * backend. Otherwise NULL is returned. * * The returned identifier is the base identifier (UID or UID_localizedUID) * without the language content identifier suffix, suitable for use as an external identifier. * * @param object $object * @return string|null The identifier for the object if it is known, or NULL */ public function getIdentifierByObject($object) { if ($object instanceof LazyLoadingProxy) { $object = $object->_loadRealInstance(); } if (!is_object($object)) { return null; } $identifier = $this->session->getIdentifierByObject($object); if ($identifier === null) { return null; } return $this->session->getBaseIdentifier($identifier); } /** * Returns the object with the (internal) identifier, if it is known to the * backend. Otherwise NULL is returned. * * @param string $identifier * @param string $className * @return object|null The object for the identifier if it is known, or NULL */ public function getObjectByIdentifier($identifier, $className) { $query = $this->persistenceManager->createQueryForType($className); // This allows to fetch IDs for languages for default language AND language IDs // This is especially important when using the PropertyMapper of the Extbase MVC part to get // an object of the translated version of the incoming ID of a record. // "Free" mode (OVERLAYS_OFF) is mapped to OVERLAYS_MIXED - overlays need to be enabled for the // identity lookup, but hiding untranslated records is not a configured intent in free mode. // This is consistent with the same handling for related objects in DataMapper->getPreparedQuery(). $languageAspect = $query->getQuerySettings()->getLanguageAspect(); $languageAspect = new LanguageAspect( $languageAspect->getId(), $languageAspect->getContentId(), $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(), $languageAspect->getFallbackChain() ); // Build language-aware session identifier $sessionIdentifier = $this->session->buildIdentifier($identifier, $languageAspect); if ($this->session->hasIdentifier($sessionIdentifier, $className)) { return $this->session->getObjectByIdentifier($sessionIdentifier, $className); } $query->getQuerySettings()->setLanguageAspect($languageAspect); $query->getQuerySettings()->setRespectStoragePage(false); $query->getQuerySettings()->setRespectSysLanguage(false); return $query->matching($query->equals('uid', $identifier))->execute()->getFirst(); } /** * Checks if the given object has ever been persisted. * * @param object $object The object to check * @return bool TRUE if the object is new, FALSE if the object exists in the repository */ public function isNewObject($object) { return $this->getIdentifierByObject($object) === null; } /** * Sets the aggregate root objects */ public function setAggregateRootObjects(ObjectStorage $objects) { $this->aggregateRootObjects = $objects; } /** * Sets the changed objects */ public function setChangedEntities(ObjectStorage $entities) { $this->changedEntities = $entities; } /** * Sets the deleted objects */ public function setDeletedEntities(ObjectStorage $entities) { $this->deletedEntities = $entities; } /** * Commits the current persistence session. */ public function commit() { $this->persistObjects(); $this->processDeletedObjects(); } /** * Traverse and persist all aggregate roots and their object graph. */ protected function persistObjects(): void { $this->visitedDuringPersistence = new ObjectStorage(); foreach ($this->aggregateRootObjects as $object) { /** @var DomainObjectInterface $object */ if ($object->_isNew()) { $this->insertObject($object); } $this->persistObject($object); } foreach ($this->changedEntities as $object) { $this->persistObject($object); } } /** * Persists the given object. */ protected function persistObject(DomainObjectInterface $object): void { if (isset($this->visitedDuringPersistence[$object])) { return; } $row = []; $queue = []; $className = get_class($object); $dataMap = $this->dataMapFactory->buildDataMap($className); $classSchema = $this->reflectionService->getClassSchema($className); foreach ($classSchema->getDomainObjectProperties() as $property) { $propertyName = $property->getName(); if (!$dataMap->isPersistableProperty($propertyName)) { continue; } $propertyValue = $object->_getProperty($propertyName); if ($this->propertyValueIsLazyLoaded($propertyValue)) { continue; } $columnMap = $dataMap->getColumnMap($propertyName); if ($propertyValue instanceof ObjectStorage) { $cleanProperty = $object->_getCleanProperty($propertyName); // objectstorage needs to be persisted if the object is new, the objectstorage is dirty, meaning it has // been changed after initial build, or an empty objectstorage is present and the cleanstate objectstorage // has childelements, meaning all elements should been removed from the objectstorage if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() === 0 && $cleanProperty && $cleanProperty->count() > 0)) { $this->persistObjectStorage($propertyValue, $object, $propertyName, $row); $propertyValue->_memorizeCleanState(); } foreach ($propertyValue as $containedObject) { if ($containedObject instanceof DomainObjectInterface) { $queue[] = $containedObject; } } } elseif ($propertyValue instanceof DomainObjectInterface) { if ($object->_isDirty($propertyName)) { if ($propertyValue->_isNew()) { $this->insertObject($propertyValue, $object, $propertyName); } $row[$columnMap->columnName] = $this->getPlainValue($propertyValue, null, $property); } $queue[] = $propertyValue; } elseif ($object->_isNew() || $object->_isDirty($propertyName)) { $row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property); } } if (!empty($row)) { $this->updateObject($object, $row); $object->_memorizeCleanState(); } $this->visitedDuringPersistence[$object] = $object->getUid(); foreach ($queue as $queuedObject) { $this->persistObject($queuedObject); } $this->eventDispatcher->dispatch(new EntityPersistedEvent($object)); } /** * Checks, if the property value is lazy loaded and was not initialized */ protected function propertyValueIsLazyLoaded(mixed $propertyValue): bool { if ($propertyValue instanceof LazyLoadingProxy) { return true; } if (($propertyValue instanceof LazyObjectStorage) && $propertyValue->isInitialized() === false) { return true; } return false; } /** * Persists an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object. * A 1:1 relation gets persisted immediately. Objects which were removed from the property were detached from * the parent object. They will not be deleted by default. You have to add the attribute * #[\TYPO3\CMS\Extbase\Attribute\ORM\Cascade(['value' => 'remove'])] to the property if you want them to * be deleted as well. * * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted. * @param DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage. * @param string $propertyName The name of the property holding the object storage. * @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. */ protected function persistObjectStorage( ObjectStorage $objectStorage, DomainObjectInterface $parentObject, string $propertyName, array &$row ): void { $className = get_class($parentObject); $dataMapper = GeneralUtility::makeInstance(DataMapper::class); $columnMap = $this->dataMapFactory->buildDataMap($className)->getColumnMap($propertyName); $property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName); foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) { $this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName); if ($columnMap->typeOfRelation === Relation::HAS_MANY && $property->getCascadeValue() === 'remove') { $this->removeEntity($removedObject); } } $currentUids = []; $sortingPosition = 1; $updateSortingOfFollowing = false; foreach ($objectStorage as $object) { /** @var DomainObjectInterface $object */ if (empty($currentUids)) { $sortingPosition = 1; } else { $sortingPosition++; } $cleanProperty = $parentObject->_getCleanProperty($propertyName); if ($object->_isNew()) { $this->insertObject($object, $parentObject); $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition); // if a new object is inserted, all objects after this need to have their sorting updated $updateSortingOfFollowing = true; } elseif ($cleanProperty === null || $cleanProperty->getPosition($object) === null) { // if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition); // 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 $updateSortingOfFollowing = true; } elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) { $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition); $updateSortingOfFollowing = true; } elseif ($updateSortingOfFollowing) { if ($sortingPosition > $objectStorage->getPosition($object)) { $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition); } else { $sortingPosition = $objectStorage->getPosition($object); } } $currentUids[] = $object->getUid(); } if ($columnMap->parentKeyFieldName === null) { $row[$columnMap->columnName] = implode(',', $currentUids); } else { $row[$columnMap->columnName] = $dataMapper->countRelated($parentObject, $propertyName); } } /** * Returns the removed objects determined by a comparison of the clean property value * with the actual property value. */ protected function getRemovedChildObjects(DomainObjectInterface $object, string $propertyName): array { $removedObjects = []; $cleanPropertyValue = $object->_getCleanProperty($propertyName); if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) { $propertyValue = $object->_getProperty($propertyName); foreach ($cleanPropertyValue as $containedObject) { if (!$propertyValue->contains($containedObject)) { $removedObjects[] = $containedObject; } } } return $removedObjects; } /** * Updates the fields defining the relation between the object and the parent object. */ protected function attachObjectToParentObject( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $parentPropertyName, int $sortingPosition = 0 ): void { $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName); if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) { $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition); } elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { $this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition); } } /** * Updates the fields defining the relation between the object and the parent object. */ protected function updateRelationOfObjectToParentObject( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $parentPropertyName, int $sortingPosition = 0 ): void { $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName); if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) { $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition); } elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { $this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition); } } /** * Updates fields defining the relation between the object and the parent object in relation has-many. * * @throws IllegalRelationTypeException */ protected function attachObjectToParentObjectRelationHasMany( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $parentPropertyName, int $sortingPosition = 0 ): void { $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName); if ($parentColumnMap->typeOfRelation !== Relation::HAS_MANY) { throw new IllegalRelationTypeException( 'Parent column relation type is ' . Relation::class . '::' . $parentColumnMap->typeOfRelation->name . ' but should be ' . Relation::class . '::' . Relation::HAS_MANY->name, 1345368105 ); } $row = []; if ($parentColumnMap->parentKeyFieldName !== null) { $row[$parentColumnMap->parentKeyFieldName] = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) ?: $parentObject->getUid(); if ($parentColumnMap->parentTableFieldName !== null) { $row[$parentColumnMap->parentTableFieldName] = $parentDataMap->tableName; } $row = array_merge($parentColumnMap->relationTableMatchFields, $row); } $childSortByFieldName = $parentColumnMap->childSortByFieldName; if (!empty($childSortByFieldName)) { $row[$childSortByFieldName] = $sortingPosition; } if (!empty($row)) { $this->updateObject($object, $row); } } /** * Updates the fields defining the relation between the object and the parent object. */ protected function detachObjectFromParentObject( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $parentPropertyName ): void { $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName); if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) { $row = []; if ($parentColumnMap->parentKeyFieldName !== null) { $row[$parentColumnMap->parentKeyFieldName] = 0; if ($parentColumnMap->parentTableFieldName !== null) { $row[$parentColumnMap->parentTableFieldName] = ''; } if (!empty($parentColumnMap->relationTableMatchFields)) { $row = array_merge(array_fill_keys(array_keys($parentColumnMap->relationTableMatchFields), ''), $row); } } if (!empty($parentColumnMap->childSortByFieldName)) { $row[$parentColumnMap->childSortByFieldName] = 0; } if (!empty($row)) { $this->updateObject($object, $row); } } elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { $this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName); } } /** * Inserts an object in the storage backend */ protected function insertObject( DomainObjectInterface $object, ?DomainObjectInterface $parentObject = null, string $parentPropertyName = '' ): void { if ($object instanceof AbstractValueObject) { $result = $this->getUidOfAlreadyPersistedValueObject($object); if ($result !== null) { $object->_setProperty(AbstractDomainObject::PROPERTY_UID, $result); return; } } $className = get_class($object); $dataMap = $this->dataMapFactory->buildDataMap($className); $row = []; $classSchema = $this->reflectionService->getClassSchema($className); foreach ($classSchema->getDomainObjectProperties() as $property) { $propertyName = $property->getName(); if (!$dataMap->isPersistableProperty($propertyName)) { continue; } $propertyValue = $object->_getProperty($propertyName); if ($this->propertyValueIsLazyLoaded($propertyValue)) { continue; } $columnMap = $dataMap->getColumnMap($propertyName); if ($columnMap->typeOfRelation === Relation::HAS_ONE) { $row[$columnMap->columnName] = 0; } elseif ($columnMap->typeOfRelation !== Relation::NONE) { if ($columnMap->parentKeyFieldName === null) { // CSV type relation $row[$columnMap->columnName] = ''; } else { // MM type relation $row[$columnMap->columnName] = 0; } } elseif ($propertyValue !== null) { $row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property); } } $this->addCommonFieldsToRow($object, $row); if ($dataMap->languageIdColumnName !== null && $object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) === null) { $row[$dataMap->languageIdColumnName] = 0; $object->_setProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID, 0); } if ($dataMap->translationOriginColumnName !== null) { $row[$dataMap->translationOriginColumnName] = 0; } if ($dataMap->translationOriginDiffSourceName !== null) { $row[$dataMap->translationOriginDiffSourceName] = ''; } if ($parentObject !== null && $parentPropertyName) { $parentColumnDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject))->getColumnMap($parentPropertyName); $row = array_merge($parentColumnDataMap->relationTableMatchFields, $row); if ($parentColumnDataMap->parentKeyFieldName !== null) { $row[$parentColumnDataMap->parentKeyFieldName] = (int)$parentObject->getUid(); } } if ($parentObject) { // Ensure a nested object respects the storage PID for new records or inherits the storage PID from // the parent object. $storagePidForObject = $this->determineStoragePageIdForNewRecord($object); if ($storagePidForObject === 0) { $storagePidForObject = $parentObject->getPid() ?? 0; } $row['pid'] = $storagePidForObject; } $uid = $this->storageBackend->addRow($dataMap->tableName, $row); $localizedUid = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID); $identifier = $this->session->buildIdentifier(['uid' => $uid, '_LOCALIZED_UID' => $localizedUid]); $object->_setProperty(AbstractDomainObject::PROPERTY_UID, $uid); $object->setPid((int)$row['pid']); if ($uid >= 1) { $this->eventDispatcher->dispatch(new EntityAddedToPersistenceEvent($object)); } $this->referenceIndex->updateRefIndexTable($dataMap->tableName, $uid); $this->session->registerObject($object, $identifier); if ($uid >= 1) { $this->eventDispatcher->dispatch(new EntityFinalizedAfterPersistenceEvent($object)); } } /** * Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid. * * @return int|null The matching uid if an object was found, else null */ protected function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int { return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object); } /** * Inserts mm-relation into a relation table * * @return int The uid of the inserted row */ protected function insertRelationInRelationtable( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $propertyName, ?int $sortingPosition = null ): int { $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($propertyName); $parentUid = $parentObject->getUid(); if ($parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) { $parentUid = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID); } $row = []; if ($columnMap->parentKeyFieldName !== null) { $row[$columnMap->parentKeyFieldName] = (int)$parentUid; } if ($columnMap->childKeyFieldName !== null) { $row[$columnMap->childKeyFieldName] = (int)$object->getUid(); } if ($columnMap->childSortByFieldName !== null) { $row[$columnMap->childSortByFieldName] = $sortingPosition ?? 0; } $relationTableName = $columnMap->relationTableName; if ($this->tcaSchemaFactory->has($relationTableName)) { $row[AbstractDomainObject::PROPERTY_PID] = $this->determineStoragePageIdForNewRecord(); } $row = array_merge($columnMap->relationTableMatchFields, $row); return $this->storageBackend->addRow($relationTableName, $row, true); } /** * Updates mm-relation in a relation table * * @return bool TRUE if update was successfully */ protected function updateRelationInRelationTable( DomainObjectInterface $object, DomainObjectInterface $parentObject, string $propertyName, int $sortingPosition = 0 ): bool { $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($propertyName); $row = []; if ($columnMap->parentKeyFieldName !== null) { $row[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid(); } if ($columnMap->childKeyFieldName !== null) { $row[$columnMap->childKeyFieldName] = (int)$object->getUid(); } if ($columnMap->childSortByFieldName !== null) { $row[$columnMap->childSortByFieldName] = $sortingPosition; } $relationTableName = $columnMap->relationTableName; $row = array_merge($columnMap->relationTableMatchFields, $row); $this->storageBackend->updateRelationTableRow($relationTableName, $row); return true; } /** * Delete all mm-relations of a parent from a relation table * * @return bool TRUE if delete was successfully */ protected function deleteAllRelationsFromRelationtable( DomainObjectInterface $parentObject, string $parentPropertyName ): bool { $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($parentPropertyName); $relationTableName = $columnMap->relationTableName; $relationMatchFields = []; if ($columnMap->parentKeyFieldName !== null) { $relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid(); } $relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields); $this->storageBackend->removeRow($relationTableName, $relationMatchFields); return true; } /** * Delete an mm-relation from a relation table */ protected function deleteRelationFromRelationtable( DomainObjectInterface $relatedObject, DomainObjectInterface $parentObject, string $parentPropertyName ): bool { $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($parentPropertyName); $relationTableName = $columnMap->relationTableName; $relationMatchFields = []; if ($columnMap->parentKeyFieldName !== null) { $relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid(); } if ($columnMap->childKeyFieldName !== null) { $relationMatchFields[$columnMap->childKeyFieldName] = (int)$relatedObject->getUid(); } $relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields); $this->storageBackend->removeRow($relationTableName, $relationMatchFields); return true; } /** * Updates a given object in the storage */ protected function updateObject(DomainObjectInterface $object, array $row): void { $dataMap = $this->dataMapFactory->buildDataMap(get_class($object)); $this->addCommonFieldsToRow($object, $row); $row['uid'] = $object->getUid(); if ($dataMap->languageIdColumnName !== null) { $row[$dataMap->languageIdColumnName] = (int)$object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID); if ($object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) { $row['uid'] = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID); } } $this->storageBackend->updateRow($dataMap->tableName, $row); $this->eventDispatcher->dispatch(new EntityUpdatedInPersistenceEvent($object)); $this->referenceIndex->updateRefIndexTable($dataMap->tableName, (int)$row['uid']); } /** * Adds common database fields to a row */ protected function addCommonFieldsToRow(DomainObjectInterface $object, array &$row): void { $dataMap = $this->dataMapFactory->buildDataMap(get_class($object)); $this->addCommonDateFieldsToRow($object, $row); if ($dataMap->recordTypeColumnName !== null && $dataMap->recordType !== null) { $row[$dataMap->recordTypeColumnName] = $dataMap->recordType; } if ($object->_isNew() && !isset($row['pid'])) { $row['pid'] = $this->determineStoragePageIdForNewRecord($object); } } /** * Adjusts the common date fields of the given row to the current time */ protected function addCommonDateFieldsToRow(DomainObjectInterface $object, array &$row): void { $dataMap = $this->dataMapFactory->buildDataMap(get_class($object)); if ($object->_isNew() && $dataMap->creationDateColumnName !== null) { $row[$dataMap->creationDateColumnName] = $GLOBALS['EXEC_TIME']; } if ($dataMap->modificationDateColumnName !== null) { $row[$dataMap->modificationDateColumnName] = $GLOBALS['EXEC_TIME']; } } /** * Iterate over deleted aggregate root objects and process them */ protected function processDeletedObjects(): void { foreach ($this->deletedEntities as $entity) { if ($this->session->hasObject($entity)) { $this->removeEntity($entity); $this->session->unregisterReconstitutedEntity($entity); $this->session->unregisterObject($entity); } } $this->deletedEntities = new ObjectStorage(); } /** * Deletes an object */ protected function removeEntity(DomainObjectInterface $object, bool $markAsDeleted = true): void { $dataMap = $this->dataMapFactory->buildDataMap(get_class($object)); if ($markAsDeleted === true && $dataMap->deletedFlagColumnName !== null) { $deletedColumnName = $dataMap->deletedFlagColumnName; $row = [ 'uid' => $object->getUid(), $deletedColumnName => 1, ]; $this->addCommonDateFieldsToRow($object, $row); $this->storageBackend->updateRow($dataMap->tableName, $row); } else { $this->storageBackend->removeRow($dataMap->tableName, ['uid' => $object->getUid()]); } $this->eventDispatcher->dispatch(new EntityRemovedFromPersistenceEvent($object)); $this->removeRelatedObjects($object); $this->referenceIndex->updateRefIndexTable($dataMap->tableName, $object->getUid()); } /** * Remove related objects */ protected function removeRelatedObjects(DomainObjectInterface $object): void { $className = get_class($object); $dataMap = $this->dataMapFactory->buildDataMap($className); $classSchema = $this->reflectionService->getClassSchema($className); foreach ($classSchema->getDomainObjectProperties() as $property) { $propertyName = $property->getName(); $columnMap = $dataMap->getColumnMap($propertyName); if ($columnMap === null) { continue; } $propertyValue = $object->_getProperty($propertyName); if ($property->getCascadeValue() === 'remove') { if ($columnMap->typeOfRelation === Relation::HAS_MANY) { foreach ($propertyValue as $containedObject) { $this->removeEntity($containedObject); } } elseif ($propertyValue instanceof DomainObjectInterface) { $this->removeEntity($propertyValue); } } elseif ($dataMap->deletedFlagColumnName === null && $columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY ) { $this->deleteAllRelationsFromRelationtable($object, $propertyName); } } } /** * Determine the storage page ID for a given NEW record * * This does the following: * - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record. * - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records. * - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records. * * @return int the storage Page ID where the object should be stored */ protected function determineStoragePageIdForNewRecord(?DomainObjectInterface $object = null): int { $frameworkConfiguration = []; try { $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK); } catch (NoServerRequestGivenException) { // Fallback to empty array if ConfigurationManager has not been initialized with a Request. // This implies storagePid 0. This is a measure to specifically allow running the extbase // persistence layer without a Request, which may be useful in some CLI scenarios (and can // be convenient in tests) when no other code branches of extbase that have a hard dependency // to the Request (e.g. controllers / view) are used. } if ($object !== null) { if (ObjectAccess::isPropertyGettable($object, AbstractDomainObject::PROPERTY_PID)) { $pid = ObjectAccess::getProperty($object, AbstractDomainObject::PROPERTY_PID); if (isset($pid)) { return (int)$pid; } } $className = get_class($object); if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) { return (int)$frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid']; } } $storagePidList = GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '0')); return $storagePidList[0]; } /** * Returns a plain value, i.e. objects are flattened out if possible. * Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'. * For null values, the expected DB null value will be considered. * * @param mixed $input The value that will be converted * @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format * @param Property|null $property The current property * @return int|string|null */ protected function getPlainValue(mixed $input, ?ColumnMap $columnMap = null, ?Property $property = null) { if ($input !== null) { return GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap); } if ($columnMap?->type === TableColumnType::DATETIME) { return QueryHelper::transformDateTimeToDatabaseValue( null, $columnMap->isNullable, $columnMap->dateTimeFormat ?? 'datetime', $columnMap->dateTimeStorageFormat ); } if ($property === null) { return null; } $className = $property->getPrimaryType()->getClassName() ?? null; if ($className === null) { return null; } // Nullable domain model property if (is_subclass_of($className, DomainObjectInterface::class)) { return 0; } return null; } }