public function deleteObjectTree(Schema $schema, ObjectEntity $entity, $id, Audit $audit) { $numTerminated = 0; // Get a set of ObjectRefs to objects that are 'owned' by the given entity and id. // Also determine the set of all related link entities. $ownedEntitiesWithFk = array(); $ownedEntitiesWithoutFk = array(); $ownerEntitiesWithState = array(); $uncheckedEntities = array(); foreach ($entity->getRelationships() as $relationship) { $otherEntity = $relationship->getOppositeEntity($entity); $otherEntityName = $otherEntity->getName(); // If this is a link relationship... if (!$relationship->getFkEntity()->isObjectEntity()) { // ...then delete any link object referring to the current (to be deleted) object. $propertyValues = array(); $propertyValues[$relationship->getFkColumnName($entity)] = $id; $this->terminateLinks($schema, $relationship->getFkEntity(), $propertyValues, $audit); } else { if ($relationship->getOwnerEntity() == $entity) { if ($relationship->getFkEntity() == $otherEntity) { $ownedEntitiesWithFk[$otherEntityName] = $otherEntity; } else { $ownedEntitiesWithoutFk[$otherEntityName] = $otherEntity; } } else { if ($otherEntity->getStateIdColumnName() != NULL) { $ownerEntitiesWithState[$otherEntityName] = $otherEntity; } else { $uncheckedEntities[$otherEntityName] = $otherEntity; } } } } // Now fetch the ObjectRefs. $objectRef = new ObjectRef($entity, $id); $scope = Scope::parseValue(Scope::VALUE_C_REF . Scope::VALUE_A_REF); $ownedObjectRefs = $objectRef->fetchAllRelatedObjectRefs($schema->getMySQLi(), array_merge($ownedEntitiesWithFk, $ownedEntitiesWithoutFk), $this->defaultQueryContext, $scope); // If the object supports the 'terminated' state... if ($entity->getStateIdColumnName() != NULL) { // ...then terminate it. $numTerminated += $this->terminateObject($schema, $entity, $id, $audit); if ($numTerminated > 0) { // Traverse the object tree and recursively delete children. foreach ($ownedObjectRefs as $ownedObjectRef) { $numTerminated += $this->deleteObjectTree($schema, $ownedObjectRef->getEntity(), $ownedObjectRef->getId(), $audit); } } } else { // The object does not support the 'terminated' state, so it must be deleted permanently. // Check if any 'owner-objects-with-state' refer to the given object, either now or in the past. if (count($ownerEntitiesWithState) > 0) { $queryContext = new QueryContext(array(RestUrlParams::AT => RestUrlParams::ALL_TIMES), NULL); $scope = Scope::parseValue(Scope::VALUE_O_REF); $ownerObjectRefs = $objectRef->fetchAllRelatedObjectRefs($schema->getMySQLi(), $ownerEntitiesWithState, $queryContext, $scope); if (count($ownerObjectRefs) > 0) { // Some terminated owner objects are still referring to this object, so we cannot delete it. return $numTerminated; } } // Split the set of $ownedObjectRefs in two: one set that keeps a foreign key and another that doesn't. $ownedObjectRefsWithFk = array(); $ownedObjectRefsWithoutFk = array(); foreach ($ownedObjectRefs as $ownedObjectRef) { if (array_search($ownedObjectRef->getEntity(), $ownedEntitiesWithFk) !== FALSE) { $ownedObjectRefsWithFk[] = $ownedObjectRef; } else { $ownedObjectRefsWithoutFk[] = $ownedObjectRef; } } // First get rid of all owned objects that keep a foreign key to self... $numTerminated += $this->deleteAndPurgeObjectTrees($schema, $entity, $id, $ownedObjectRefsWithFk, $audit); // ...then delete the object itself. // If deleting an _account, then make sure that any reference to it is 'patched'... $this->patchAccountRefsIfNecessary($schema, $entity, $id, $audit); // ...and then delete the object itself. $queryString = "DELETE d FROM " . $entity->getName() . " d" . " WHERE d." . $entity->getObjectIdColumnName() . " = {$id}"; $mySQLi = $schema->getMySQLi(); $queryResult = $mySQLi->query($queryString); if (!$queryResult) { throw new Exception("Error deleting objects of entity '" . $entity->getName() . "' - {$mySQLi->error}\n<!--\n{$queryString}\n-->"); } $numTerminated += $mySQLi->affected_rows; // Finally, after all foreign keys referring to them have been deleted, delete the remaining owned objects. $numTerminated += $this->deleteAndPurgeObjectTrees($schema, $entity, $id, $ownedObjectRefsWithoutFk, $audit); } return $numTerminated; }
public function detectObsoleteConnections(Schema $schema, ObjectFetcher $objectFetcher) { // CHANGED objects can have persisted relationships that don't exist in the parsed dataset. // Such persisted relationships must be removed. This implies that either // a. nothing special needs to be done, or // b. persisted connected objects must be 'patched', i.e. their foreign key must be set to NULL, or // c. persisted connected objects must be deleted, or // d. a persisted link (mant-to-many relationship) must be deleted. // Option a. is the case when the CHANGED object holds the foreign key and if is NOT an owner of the // connected object, or when the connected object is present in the set of ParsedObjects. // Option b. applies to connected objects that are not in the set of ParsedObjects. Such connected objects // must be added as CHANGED ParsedObjects and filled with all existing property values, except for the // foreign key. // Option c. applies to situations where the CHANGED object is the owning entity. In this case the existing // connected object must be added to the set of ParsedObjects and marked as DELETED. // Option d. is similar to option c, but the connected object is a LinkEntity. LinkEntities cannot be referred // to by a single id and therefore cannot be dealt with via ParsedObjects. Creating or deleting LinkEntities is // done in ParsedObject::establishLinks(). // // Note that ParsedObjects marked as 'touched' are not modified themselves, but they may have modified // relationships. These ParsedObjects must be treadet the same as CHANGED objects here. // // So these are the steps to be taken: // 1. loop over the CHANGED and 'touched' ParsedObjects // 2. per object, loop over the relationships, ignoring many-to-many (link) relationships // 3. check if the entity of the object is NOT the foreign key entity in the relationship // and check if the object is owner of the connected object // 4. if either is the case, then see if the parsed data specifies a connection for this relationship // 5. if NOT, then add the connected object to the set of ParsedObjects // 6. if the connected object is NOT an owner, then fetch its id and mark the newly created ParsedObject // as DELETED // 7. if the connected object is the foreign key entity, then fetch and set its id and all its properties // (except for the foreign key) and mark the newly created ParsedObject as CHANGED foreach ($this->getChangedAndTouchedObjects() as $changedObject) { if ($changedObject->getScope()->includes(Scope::TAG_COMPONENTS) == Scope::INCLUDES_NONE) { continue; } $changedEntity = $changedObject->getEntity(); foreach ($changedEntity->getRelationships() as $relationship) { // Ignore LinkEntities, see ParsedObject::establishLinks(). if ($relationship->getFkEntity()->isObjectEntity()) { $connectedEntity = $relationship->getOppositeEntity($changedEntity); $isFkEntity = $changedEntity == $relationship->getFkEntity(); $isOwnerEntity = $changedEntity == $relationship->getOwnerEntity(); if ($isOwnerEntity or !$isFkEntity) { // Check if the parsed data specifies a (new) connection for this relationship. $parsedConnectedObject = NULL; foreach ($changedObject->getRelatedObjects() as $relatedObject) { if ($relatedObject->getEntity() == $connectedEntity) { $parsedConnectedObject = $relatedObject; break; } } // Also check the other direction. foreach ($this->parsedObjects as $parsedObject) { if ($parsedConnectedObject != NULL) { break; } if ($parsedObject->getEntity() == $connectedEntity) { foreach ($parsedObject->getRelatedObjects() as $relatedObject) { if ($relatedObject->getEntity() == $changedEntity) { $parsedConnectedObject = $parsedObject; break; } } } } // Only do something if there is no (new) connection for this relationship, so any existing // connection must be deleted. if ($parsedConnectedObject == NULL) { $persistedConnectionIds = NULL; if ($isFkEntity) { $persistedConnectionIds = array(); $fkColumnName = $relationship->getFkColumnName($connectedEntity); // Fetch the foreign key. $persistedConnectionId = $objectFetcher->getObjectProperty($changedEntity, $changedObject->getId(), $fkColumnName, array()); if ($persistedConnectionId != NULL) { $persistedConnectionIds[] = $persistedConnectionId; } } else { // The existing connected object must be patched or deleted. // Fetch the ids of the connected objects. $changedObjectRef = new ObjectRef($changedEntity, $changedObject->getId()); $persistedConnectionIds = $changedObjectRef->fetchRelatedObjectIdsOfEntity($schema->getMySQLi(), $connectedEntity); } foreach ($persistedConnectionIds as $persistedConnectionId) { // Check if the existing connected object is already specified in the current // transaction. $alreadyParsedObject = NULL; foreach ($this->parsedObjects as $parsedObject) { if ($parsedObject->getEntity() == $connectedEntity and $parsedObject->getId() == $persistedConnectionId) { $alreadyParsedObject = $parsedObject; break; } } // Do nothing if the existing connected object is already present in the set of // ParsedObjects. if ($alreadyParsedObject != NULL) { continue; } if ($isOwnerEntity) { $deletedObject = new ParsedObject($connectedEntity, $persistedConnectionId, array(), ParsedObject::DELETED); $this->deletedObjects[] = $deletedObject; } else { $propertyValues = $objectFetcher->getPropertyValues($connectedEntity, $persistedConnectionId); $propertyValues[$relationship->getFkColumnName($changedEntity)] = NULL; $additionalChangedObject = new ParsedObject($connectedEntity, $persistedConnectionId, $propertyValues, ParsedObject::CHANGED); $additionalChangedObject->adjustForeignIdProperties(); $this->changedObjects[] = $additionalChangedObject; } } } } } } } }
private function getOwnedObjectRefs(Schema $schema, ObjectEntity $entity, $id, $isPublished) { // Find any related objects that are (still) published. // First create a set of entities that are 'owned' by the given one... $ownedEntities = array(); foreach ($entity->getRelationships() as $relationship) { if ($relationship->getOwnerEntity() == $entity) { $oppositeEntity = $relationship->getOppositeEntity($entity); if ($oppositeEntity->isObjectEntity()) { $ownedEntities[] = $oppositeEntity; } } } // ...and then fetch the published ObjectRefs. if (count($ownedEntities) == 0) { return NULL; } $objectRef = new ObjectRef($entity, $id); $params = array(RestUrlParams::PUBLISHED => $isPublished ? 'true' : 'false'); $queryContext = new QueryContext($params, NULL); $scope = Scope::parseValue(Scope::VALUE_C_REF . Scope::VALUE_A_REF . Scope::VALUE_O_REF); return $objectRef->fetchAllRelatedObjectRefs($schema->getMySQLi(), $ownedEntities, $queryContext, $scope); }
private function fetchRelatedObjects(ObjectRef $givenObjectRef, QueryContext $queryContext, Scope $scope, $skipBinaries, DOMDocument $domDoc, DOMNode $xmlElement, array &$xmlElementsWithState, array &$allFetchedObjects) { $hasTerminatedObjects = FALSE; $mySQLi = $this->schema->getMySQLi(); $givenEntity = $givenObjectRef->getEntity(); foreach ($givenEntity->getRelationships() as $relationship) { $otherEntity = $relationship->getOppositeEntity($givenEntity); // Ignore relationships with link-entities. if ($otherEntity->isObjectEntity()) { // Determine the scope of the related object to be fetched. $scopeUnitTag = NULL; if ($relationship->getOwnerEntity() == $givenEntity) { $scopeUnitTag = Scope::TAG_COMPONENTS; } else { if ($relationship->getOwnerEntity() == $otherEntity) { $scopeUnitTag = Scope::TAG_OWNERS; } else { $scopeUnitTag = Scope::TAG_ASSOCIATES; } } if ($scope->includes($scopeUnitTag) != Scope::INCLUDES_NONE) { $subScope = $scope->getSubScope($scopeUnitTag); // Fetch ObjectRefs to each related entity... $fetchedObjectRefs = array(); $hasTerminatedObjects |= $givenObjectRef->fetchRelatedObjectRefsOfEntity($mySQLi, $otherEntity, $queryContext, $subScope, $fetchedObjectRefs); // ...and then fetch each non-terminated entity. foreach ($fetchedObjectRefs as $fetchedObjectRef) { $this->fetchObjects($fetchedObjectRef->getEntity(), $fetchedObjectRef->getId(), $queryContext, $subScope, $skipBinaries, $domDoc, $xmlElement, $xmlElementsWithState, $allFetchedObjects); } } } } return $hasTerminatedObjects; }