Cascades a detach operation to associated documents.

@param object $document
@param array  $visited
769074
private function cascadeDetach(ClassMetadata $class, $document, array &$visited)
    {
        foreach ($class->childrenMappings as $fieldName) {
            $mapping = $class->mappings[$fieldName];
            if (!($mapping['cascade'] & ClassMetadata::CASCADE_DETACH)) {
                continue;
            }
            $related = $class->reflFields[$fieldName]->getValue($document);
            if ($related instanceof Collection || is_array($related)) {
                foreach ($related as $relatedDocument) {
                    $this->doDetach($relatedDocument, $visited);
                }
            } elseif ($related !== null) {
                $this->doDetach($related, $visited);
            }
        }

        foreach (array_merge($class->referenceMappings, $class->referrersMappings) as $fieldName) {
            $mapping = $class->mappings[$fieldName];
            if (!($mapping['cascade'] & ClassMetadata::CASCADE_DETACH)) {
                continue;
            }
            $related = $class->reflFields[$fieldName]->getValue($document);
            if ($related instanceof Collection || is_array($related)) {
                foreach ($related as $relatedDocument) {
                    $this->doDetach($relatedDocument, $visited);
                }
            } elseif ($related !== null) {
                $this->doDetach($related, $visited);
            }
        }

        // no cascade for mixed referrers
    }
25782
public function unlock($document)
    {
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
            throw new \InvalidArgumentException("Document is not MANAGED.");
        }
        $documentName = get_class($document);
        $this->getDocumentPersister($documentName)->unlock($document);
    }
322124
private function cascadeRefresh(object $document, array &$visited) : void
    {
        $class = $this->dm->getClassMetadata(get_class($document));

        $associationMappings = array_filter(
            $class->associationMappings,
            static function ($assoc) {
                return $assoc['isCascadeRefresh'];
            }
        );

        foreach ($associationMappings as $mapping) {
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
                    // Unwrap so that foreach() does not initialize
                    $relatedDocuments = $relatedDocuments->unwrap();
                }
                foreach ($relatedDocuments as $relatedDocument) {
                    $this->doRefresh($relatedDocument, $visited);
                }
            } elseif ($relatedDocuments !== null) {
                $this->doRefresh($relatedDocuments, $visited);
            }
        }
    }
1149746
public static function remove($document, $node, $type = null, $callback = null) 
    {
        $documentID = phpQuery::getDocumentID($document);
        $eventNode = self::getNode($documentID, $node);
        if (is_object($eventNode) && isset($eventNode->eventHandlers[$type])) {
            if ($callback) {
                foreach($eventNode->eventHandlers[$type] as $k => $handler) {
                    if ($handler['callback'] == $callback) {
                        unset($eventNode->eventHandlers[$type][$k]);
                    }
                }
            } else {
                unset($eventNode->eventHandlers[$type]);
            }
        }
    }
1176352
private function cascadePersist($document, array &$visited)
    {
        $class = $this->dm->getClassMetadata(get_class($document));

        $associationMetadata = array_filter(
            $class->associationMetadata,
            function ($assoc) {
                return $assoc->isCascadePersist;
            }
        );

        foreach ($associationMetadata as $attributeName => $mapping) {
            $relatedDocuments = $mapping->getValue($document);

            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
                    if ($relatedDocuments->getOwner() !== $document) {
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $mapping);
                    }
                    // Unwrap so that foreach() does not initialize
                    $relatedDocuments = $relatedDocuments->unwrap();
                }

                $count = 0;
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
                    if ($mapping->embedded) {
                        list(, $knownParent, ,) = $this->getParentAssociation($relatedDocument);

                        if ($knownParent && $knownParent !== $document) {
                            $relatedDocument               = clone $relatedDocument;
                            $relatedDocuments[$relatedKey] = $relatedDocument;
                        }

                        $pathKey = $mapping->isNumericIndexed() ? $count++ : (string)$relatedKey;
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping->name, $pathKey);
                    }
                    $this->doPersist($relatedDocument, $visited);
                }
            } elseif ($relatedDocuments !== null) {
                if ($mapping->embedded) {
                    list(, $knownParent, ,) = $this->getParentAssociation($relatedDocuments);
                    if ($knownParent && $knownParent !== $document) {
                        $relatedDocuments = clone $relatedDocuments;
                        $mapping->setValueAndWakeProxy($document, $relatedDocuments);
                    }
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping->name);
                }
                $this->doPersist($relatedDocuments, $visited);
            }
        }
    }
327837
private function cascadePersist($entity, array &$visited)
    {
        $class = $this->em->getClassMetadata(get_class($entity));

        if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
            // nothing to do - proxy is not initialized, therefore we don't do anything with it
            return;
        }

        foreach ($class->getDeclaredPropertiesIterator() as $association) {
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade(), true))) {
                continue;
            }

            /** @var AssociationMetadata $association */
            $relatedEntities = $association->getValue($entity);
            $targetEntity    = $association->getTargetEntity();

            switch (true) {
                case $relatedEntities instanceof PersistentCollection:
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                    // break; is commented intentionally!

                case $relatedEntities instanceof Collection:
                case is_array($relatedEntities):
                    if (! ($association instanceof ToManyAssociationMetadata)) {
                        throw ORMInvalidArgumentException::invalidAssociation(
                            $this->em->getClassMetadata($targetEntity),
                            $association,
                            $relatedEntities
                        );
                    }

                    foreach ($relatedEntities as $relatedEntity) {
                        $this->doPersist($relatedEntity, $visited);
                    }

                    break;

                case $relatedEntities !== null:
                    if (! $relatedEntities instanceof $targetEntity) {
                        throw ORMInvalidArgumentException::invalidAssociation(
                            $this->em->getClassMetadata($targetEntity),
                            $association,
                            $relatedEntities
                        );
                    }

                    $this->doPersist($relatedEntities, $visited);
                    break;

                default:
                    // Do nothing
            }
        }
    }
747980
private function doRefresh($entity, array &$visited)
    {
        $oid = spl_object_hash($entity);

        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // mark visited

        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
            throw OGMInvalidArgumentException::entityNotManaged($entity);
        }

        $this->getPersister(get_class($entity))->refresh($this->entityIds[$oid], $entity);

        $this->cascadeRefresh($entity, $visited);
    }
1238338
private function cascadeRefresh($entity, array &$visited)
    {
        $class = $this->em->getClassMetadata(get_class($entity));

        foreach ($class->getDeclaredPropertiesIterator() as $association) {
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade(), true))) {
                continue;
            }

            $relatedEntities = $association->getValue($entity);

            switch (true) {
                case $relatedEntities instanceof PersistentCollection:
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                    // break; is commented intentionally!

                case $relatedEntities instanceof Collection:
                case is_array($relatedEntities):
                    foreach ($relatedEntities as $relatedEntity) {
                        $this->doRefresh($relatedEntity, $visited);
                    }
                    break;

                case $relatedEntities !== null:
                    $this->doRefresh($relatedEntities, $visited);
                    break;

                default:
                    // Do nothing
            }
        }
    }
96535
private function executeRemovals($documents)
    {
        foreach ($documents as $oid => $document) {
            if (empty($this->documentIds[$oid])) {
                continue;
            }

            $class = $this->dm->getClassMetadata(get_class($document));
            $id = $this->getDocumentId($document);

            try {
                $node = $this->session->getNode($id);
                $this->doRemoveAllTranslations($document, $class);
                $node->remove();
            } catch (PathNotFoundException $e) {
            }

            $this->unregisterDocument($document);
            $this->purgeChildren($document);

            if ($invoke = $this->eventListenersInvoker->getSubscribedSystems($class, Event::postRemove)) {
                $this->eventListenersInvoker->invoke(
                    $class,
                    Event::postRemove,
                    $document,
                    new LifecycleEventArgs($document, $this->dm),
                    $invoke
                );
            }
        }
    }
471069
private function doDetach($entity, array &$visited, $noCascade = false)
    {
        $oid = spl_object_hash($entity);

        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // mark visited

        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
            case self::STATE_MANAGED:
                if ($this->isManaged($entity)) {
                    $this->removeManaged($entity);
                }

                unset(
                    $this->nodesScheduledForCreate[$oid],
                    $this->nodesScheduledForUpdate[$oid],
                    $this->nodesScheduledForDelete[$oid],
                    $this->entityStates[$oid]
                );
                break;
            case self::STATE_NEW:
            case self::STATE_DETACHED:
                return;
        }

        $this->entityStates[$oid] = self::STATE_DETACHED;

        if (!$noCascade) {
            $this->cascadeDetach($entity, $visited);
        }
    }
857063
private function cascadeMerge(ClassMetadata $class, $document, $managedCopy, array &$visited)
    {
        foreach (array_merge($class->referenceMappings, $class->referrersMappings) as $fieldName) {
            $mapping = $class->mappings[$fieldName];
            if (!($mapping['cascade'] & ClassMetadata::CASCADE_MERGE)) {
                continue;
            }
            $related = $class->reflFields[$fieldName]->getValue($document);
            if ($related instanceof Collection || is_array($related)) {
                if ($related instanceof PersistentCollection) {
                    // Unwrap so that foreach () does not initialize
                    $related = $related->unwrap();
                }
                foreach ($related as $relatedDocument) {
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $mapping);
                }
            } elseif ($related !== null) {
                $this->doMerge($related, $visited, $managedCopy, $mapping);
            }
        }
    }
461451
private function cascadeRefresh($entity, array &$visited)
    {
        $class = $this->entityManager->getClassMetadata(get_class($entity));

        foreach ($class->getRelationships() as $relationship) {
            $value = $relationship->getValue($entity);

            switch (true) {
                case $value instanceof Collection:
                case is_array($value):
                    foreach ($value as $relatedEntity) {
                        $this->doRefresh($relatedEntity, $visited);
                    }
                    break;
                case $value !== null:
                    $this->doRefresh($value, $visited);
                    break;
                default:
                    // Do nothing
            }
        }
    }
1174279
private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
    {
        $oid = spl_object_hash($document);

        if (isset($visited[$oid])) {
            return $visited[$oid]; // Prevent infinite recursion
        }

        $visited[$oid] = $document; // mark visited

        $class = $this->dm->getClassMetadata(get_class($document));

        /* First we assume DETACHED, although it can still be NEW but we can
         * avoid an extra DB round trip this way. If it is not MANAGED but has
         * an identity, we need to fetch it from the DB anyway in order to
         * merge. MANAGED documents are ignored by the merge operation.
         */
        $managedCopy = $document;

        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
                $document->initializeProxy();
            }

            $identifier = $class->getIdentifier();
            // We always have one element in the identifier array but it might be null
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
            $managedCopy = null;

            // Try to fetch document from the database
            if (! $class->isEmbeddedDocument && $id !== null) {
                $managedCopy = $this->dm->find($class->name, $id);

                // Managed copy may be removed in which case we can't merge
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
                }

                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
                    $managedCopy->initializeProxy();
                }
            }

            if ($managedCopy === null) {
                // Create a new managed instance
                $managedCopy = $class->newInstance();
                if ($id !== null) {
                    $class->setIdentifierValue($managedCopy, $id);
                }
                $this->persistNew($class, $managedCopy);
            }

            if ($class->isVersioned) {
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);

                // Throw exception if versions don't match
                if ($managedCopyVersion !== $documentVersion) {
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
                }
            }

            // Merge state of $document into existing (managed) document
            foreach ($class->reflClass->getProperties() as $prop) {
                $name = $prop->name;
                $prop->setAccessible(true);
                if (! isset($class->associationMappings[$name])) {
                    if (! $class->isIdentifier($name)) {
                        $prop->setValue($managedCopy, $prop->getValue($document));
                    }
                } else {
                    $assoc2 = $class->associationMappings[$name];

                    if ($assoc2['type'] === 'one') {
                        $other = $prop->getValue($document);

                        if ($other === null) {
                            $prop->setValue($managedCopy, null);
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
                            // Do not merge fields marked lazy that have not been fetched
                            continue;
                        } elseif (! $assoc2['isCascadeMerge']) {
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
                                /** @var ClassMetadata $targetClass */
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
                                $relatedId   = $targetClass->getIdentifierObject($other);

                                if ($targetClass->subClasses) {
                                    $other = $this->dm->find($targetClass->name, $relatedId);
                                } else {
                                    $other = $this
                                        ->dm
                                        ->getProxyFactory()
                                        ->getProxy($targetClass, $relatedId);
                                    $this->registerManaged($other, $relatedId, []);
                                }
                            }

                            $prop->setValue($managedCopy, $other);
                        }
                    } else {
                        $mergeCol = $prop->getValue($document);

                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
                            /* Do not merge fields marked lazy that have not
                             * been fetched. Keep the lazy persistent collection
                             * of the managed copy.
                             */
                            continue;
                        }

                        $managedCol = $prop->getValue($managedCopy);

                        if (! $managedCol) {
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
                            $managedCol->setOwner($managedCopy, $assoc2);
                            $prop->setValue($managedCopy, $managedCol);
                            $this->originalDocumentData[$oid][$name] = $managedCol;
                        }

                        /* Note: do not process association's target documents.
                         * They will be handled during the cascade. Initialize
                         * and, if necessary, clear $managedCol for now.
                         */
                        if ($assoc2['isCascadeMerge']) {
                            $managedCol->initialize();

                            // If $managedCol differs from the merged collection, clear and set dirty
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
                                $managedCol->unwrap()->clear();
                                $managedCol->setDirty(true);

                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
                                    $this->scheduleForSynchronization($managedCopy);
                                }
                            }
                        }
                    }
                }

                if (! $class->isChangeTrackingNotify()) {
                    continue;
                }

                // Just treat all properties as changed, there is no other choice.
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
            }

            if ($class->isChangeTrackingDeferredExplicit()) {
                $this->scheduleForSynchronization($document);
            }
        }

        if ($prevManagedCopy !== null) {
            $assocField = $assoc['fieldName'];
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));

            if ($assoc['type'] === 'one') {
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
            } else {
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);

                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
                }
            }
        }

        // Mark the managed copy visited as well
        $visited[spl_object_hash($managedCopy)] = $managedCopy;

        $this->cascadeMerge($document, $managedCopy, $visited);

        return $managedCopy;
    }
615079
public function remove($documents)
    {
        if (!is_array($documents)) {
            $documents = array($documents);
        }

        $remove =& Archive::getByRef($this, 'remove', array());
        foreach ($documents as $document) {
            $remove[] = $document;
        }
    }
797514
private function removeRoute(StructureBehavior $document)
    {
        foreach ($this->documentInspector->getReferrers($document) as $referrer) {
            if ($referrer instanceof RouteBehavior) {
                $this->documentManager->remove($referrer);
            }
        }
    }
1343593
private function addBreadcrumb(StructureBehavior $document, VisitorInterface $visitor)
    {
        $items = [];
        $parentDocument = $this->inspector->getParent($document);
        while ($parentDocument instanceof StructureBehavior) {
            $item = [];
            if ($parentDocument instanceof UuidBehavior) {
                $item['uuid'] = $parentDocument->getUuid();
            }

            $item['title'] = $parentDocument->getStructure()->getProperty('title')->getValue();

            $items[] = $item;

            $parentDocument = $this->inspector->getParent($parentDocument);
        }

        $items = array_reverse($items);

        array_walk(
            $items,
            function(&$item, $index) {
                $item['depth'] = $index;
            }
        );

        $visitor->addData('breadcrumb', $items);
    }
729993
private function collectNodes(NodeInterface $node, array $visited_nodes): array
    {
        if (in_array($node, $visited_nodes, true)) {
            return $visited_nodes;
        }

        $visited_nodes[] = $node;
        foreach ($node->getChildren() as $child) {
            $visited_nodes = $this->collectNodes($child, $visited_nodes);
        }

        return $visited_nodes;
    }