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;
 }