diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index a3af95d66c0..1ccdb4a01a0 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -103,6 +103,31 @@ public function iAddTheFollowingHierarchyRelation(TableNode $payloadTable): void ); } + /** + * @When /^I change the following hierarchy relation's parent:$/ + * @throws DBALException + */ + public function iChangeTheFollowingHierarchyRelationsParent(TableNode $payloadTable): void + { + $dataset = $this->transformPayloadTableToDataset($payloadTable); + $record = $this->transformDatasetToHierarchyRelationRecord($dataset); + unset($record['position']); + + $newParentHierarchyRelation = $this->findHierarchyRelationByIds( + ContentStreamId::fromString($dataset['contentStreamId']), + DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']), + NodeAggregateId::fromString($dataset['newParentNodeAggregateId']) + ); + + $this->dbal->update( + $this->tableNames()->hierarchyRelation(), + [ + 'parentnodeanchor' => $newParentHierarchyRelation['childnodeanchor'] + ], + $record + ); + } + /** * @When /^I change the following hierarchy relation's dimension space point hash:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature similarity index 66% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature rename to Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature index 79cfb53db26..1871e3d53e4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature @@ -10,6 +10,9 @@ Feature: Run projection integrity violation detection regarding root connection | language | de, gsw | gsw->de | And using the following node types: """yaml + 'Neos.ContentRepository.Testing:Root': + superTypes: + 'Neos.ContentRepository:Root': true 'Neos.ContentRepository.Testing:Document': [] """ And using identifier "default", I define a content repository @@ -20,18 +23,16 @@ Feature: Run projection integrity violation detection regarding root connection | newContentStreamId | "cs-identifier" | Scenario: Create a cycle - When the event RootNodeAggregateWithNodeWasCreated was published with payload: + When the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "live" | - | contentStreamId | "cs-identifier" | | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | nodeTypeName | "Neos.ContentRepository.Testing:Root" | | coveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | | nodeAggregateClassification | "root" | - When the event NodeAggregateWithNodeWasCreated was published with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "live" | - | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | originDimensionSpacePoint | {"language":"de"} | @@ -39,10 +40,9 @@ Feature: Run projection integrity violation detection regarding root connection | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "document" | | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "live" | - | contentStreamId | "cs-identifier" | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | originDimensionSpacePoint | {"language":"de"} | @@ -50,15 +50,27 @@ Feature: Run projection integrity violation detection regarding root connection | parentNodeAggregateId | "sir-david-nodenborough" | | nodeName | "child-document" | | nodeAggregateClassification | "regular" | - And the event NodeAggregateWasMoved was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | newParentNodeAggregateId | "nody-mc-nodeface" | - | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId": null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId": null}] | + + When I change the following hierarchy relation's parent: + | Key | Value | + | contentStreamId | "cs-identifier" | + | dimensionSpacePoint | {"language":"de"} | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | childNodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "nody-mc-nodeface" | + And I run integrity violation detection + Then I expect the integrity violation detection result to contain exactly 1 errors + And I expect integrity violation detection result error number 1 to have code 1597754245 + + # Another error. One error per subgraph + When I change the following hierarchy relation's parent: + | Key | Value | + | contentStreamId | "cs-identifier" | + | dimensionSpacePoint | {"language":"gsw"} | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | childNodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "nody-mc-nodeface" | And I run integrity violation detection - # one error per subgraph Then I expect the integrity violation detection result to contain exactly 2 errors And I expect integrity violation detection result error number 1 to have code 1597754245 And I expect integrity violation detection result error number 2 to have code 1597754245 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index d88070f7351..7220c896a0b 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -35,6 +35,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -54,6 +55,7 @@ * - cn -> child node * - h -> the hierarchy edge connecting parent and child * - ph -> the hierarchy edge incoming to the parent (sometimes relevant) + * - ch -> the hierarchy edge of the child (sometimes relevant) * - dsp -> dimension space point, resolves hashes to full dimension coordinates * - cdsp -> child dimension space point, same as dsp for child queries * - pdsp -> parent dimension space point, same as dsp for parent queries @@ -187,6 +189,38 @@ public function findParentNodeAggregates( return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } + public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregateId): NodeAggregateIds + { + $queryBuilderInitial = $this->createQueryBuilder() + ->select('ch.parentnodeanchor') + ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') + ->where('ch.contentstreamid = :contentStreamId') + ->andWhere('c.nodeaggregateid = :entryNodeAggregateId'); + + $queryBuilderRecursive = $this->createQueryBuilder() + ->select('ph.parentnodeanchor') + ->from('ancestry', 'ch') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = ch.parentnodeanchor') + ->where('ph.contentstreamid = :contentStreamId'); + + $queryBuilderCte = $this->createQueryBuilder() + ->select('n.nodeAggregateId') + ->from('ancestry', 'a') + ->innerJoin('a', $this->nodeQueryBuilder->tableNames->node(), 'n', 'n.relationanchorpoint = a.parentnodeanchor') + ->setParameter('contentStreamId', $this->contentStreamId->value) + ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + + $nodeAggregateIdRows = $this->fetchCteResults( + $queryBuilderInitial, + $queryBuilderRecursive, + $queryBuilderCte, + 'ancestry' + ); + + return NodeAggregateIds::fromArray(array_map(fn(array $row) => NodeAggregateId::fromString($row['nodeAggregateId']), $nodeAggregateIdRows)); + } + public function findChildNodeAggregates( NodeAggregateId $parentNodeAggregateId ): NodeAggregates { @@ -324,6 +358,28 @@ private function fetchRows(QueryBuilder $queryBuilder): array } } + /** + * @return array> + */ + private function fetchCteResults(QueryBuilder $queryBuilderInitial, QueryBuilder $queryBuilderRecursive, QueryBuilder $queryBuilderCte, string $cteTableName = 'cte'): array + { + $query = <<getSQL()} + UNION + {$queryBuilderRecursive->getSQL()} + ) + {$queryBuilderCte->getSQL()} + SQL; + $parameters = array_merge($queryBuilderInitial->getParameters(), $queryBuilderRecursive->getParameters(), $queryBuilderCte->getParameters()); + $parameterTypes = array_merge($queryBuilderInitial->getParameterTypes(), $queryBuilderRecursive->getParameterTypes(), $queryBuilderCte->getParameterTypes()); + try { + return $this->dbal->fetchAllAssociative($query, $parameters, $parameterTypes); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Failed to fetch CTE result: %s', $e->getMessage()), 1678358108, $e); + } + } + public function getContentStreamId(): ContentStreamId { return $this->contentStreamId; diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 810a095069f..b273c36a402 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -74,6 +74,7 @@ * - cn -> child node * - h -> the hierarchy edge connecting parent and child * - ph -> the hierarchy edge incoming to the parent (sometimes relevant) + * - ch -> the hierarchy edge of the child (sometimes relevant) * * - if more than one node (source-destination) * - sn -> source node @@ -570,8 +571,8 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId $queryBuilderRecursive = $this->createQueryBuilder() ->select('pn.*, h.subtreetags, h.parentnodeanchor') - ->from('ancestry', 'cn') - ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->from('ancestry', 'ch') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = ch.parentnodeanchor') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php index 29cc36eb21b..5b51f4cf54b 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php @@ -32,6 +32,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -183,6 +184,19 @@ public function findParentNodeAggregates( ); } + public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregateId): NodeAggregateIds + { + $stack = iterator_to_array($this->findParentNodeAggregates($entryNodeAggregateId)); + + $ancestorNodeAggregateIds = []; + while ($stack !== []) { + $nodeAggregate = array_shift($stack); + $ancestorNodeAggregateIds[] = $nodeAggregate->nodeAggregateId; + array_push($stack, ...iterator_to_array($this->findParentNodeAggregates($nodeAggregate->nodeAggregateId))); + } + return NodeAggregateIds::fromArray($ancestorNodeAggregateIds); + } + public function findChildNodeAggregates( NodeAggregateId $parentNodeAggregateId ): NodeAggregates { diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature index 2faa910009e..490119dab21 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature @@ -1,6 +1,6 @@ @contentrepository @adapters=DoctrineDBAL # TODO implement for Postgres -Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes queries +Feature: Find and count nodes using the findAncestorNodes, countAncestorNodes and findAncestorNodeAggregateIds queries Background: Given using the following content dimensions: @@ -64,6 +64,7 @@ Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes | a2a2 | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {} | {} | | a2a2a | a2a2a | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} | | a2a2b | a2a2b | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} | + | a2a2c | a2a2c | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} | | a2b | a2b | Neos.ContentRepository.Testing:Page | a2 | {} | {} | | a2b1 | a2b1 | Neos.ContentRepository.Testing:Page | a2b | {} | {} | | b | b | Neos.ContentRepository.Testing:Page | home | {} | {} | @@ -75,8 +76,15 @@ Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes | Key | Value | | nodeAggregateId | "a2b" | | nodeVariantSelectionStrategy | "allVariants" | - + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | workspaceName | "live" | + | dimensionSpacePoint | {"language": "ch"} | + | relationDistributionStrategy | "scatter" | + | nodeAggregateId | "a2a2c" | + | newParentNodeAggregateId | "b" | Scenario: + Subgraph queries # findAncestorNodes queries without results When I execute the findAncestorNodes query for entry node aggregate id "non-existing" I expect no nodes to be returned # a2a2a is disabled @@ -87,3 +95,23 @@ Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes # findAncestorNodes queries with results When I execute the findAncestorNodes query for entry node aggregate id "a2a2b" I expect the nodes "a2a2,a2a,a2,a,home,lady-eleonode-rootford" to be returned and the total count to be 6 When I execute the findAncestorNodes query for entry node aggregate id "a2a2b" and filter '{"nodeTypes": "Neos.ContentRepository.Testing:Page"}' I expect the nodes "a2a2,a2,a" to be returned and the total count to be 3 + + # a2a2c lives in dimension space ch beneath b + When I execute the findAncestorNodes query for entry node aggregate id "a2a2c" I expect the nodes "a2a2,a2a,a2,a,home,lady-eleonode-rootford" to be returned + And I am in dimension space point {"language":"ch"} + When I execute the findAncestorNodes query for entry node aggregate id "a2a2c" I expect the nodes "b,home,lady-eleonode-rootford" to be returned + + Scenario: + Contentgraph queries + # findAncestorNodes queries without results + When I execute the findAncestorNodeAggregateIds query for entry node aggregate id "non-existing" I expect no nodes to be returned + + # findAncestorNodes queries with results + # a2a2a is disabled + When I execute the findAncestorNodeAggregateIds query for entry node aggregate id "a2a2a" I expect the nodes "a2a2,a2a,a2,a,home,lady-eleonode-rootford" to be returned in any order + # a2b is disabled + When I execute the findAncestorNodeAggregateIds query for entry node aggregate id "a2b1" I expect the nodes "a2b,a2,a,home,lady-eleonode-rootford" to be returned in any order + # a2a2c lives in dimension space ch beneath b + When I execute the findAncestorNodeAggregateIds query for entry node aggregate id "a2a2c" I expect the nodes "a2a2,a2a,a2,b,a,home,lady-eleonode-rootford" to be returned in any order + + When I execute the findAncestorNodeAggregateIds query for entry node aggregate id "a2a2b" I expect the nodes "a2a2,a2a,a2,a,home,lady-eleonode-rootford" to be returned in any order diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index e8cac17eb67..91d35a9e4ad 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -23,6 +23,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -108,6 +109,13 @@ public function findParentNodeAggregates( NodeAggregateId $childNodeAggregateId ): NodeAggregates; + /** + * @internal the returned order of node aggregate ids is undefined and not to be relied upon + */ + public function findAncestorNodeAggregateIds( + NodeAggregateId $entryNodeAggregateId + ): NodeAggregateIds; + /** * @internal only for consumption inside the Command Handler */ diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index ffaf8f77780..dd899ee4e24 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -134,7 +134,7 @@ protected function publishEvent(string $eventType, StreamName $streamName, array /** @var EventPersister $eventPersister */ $eventPersister = (new \ReflectionClass($this->currentContentRepository))->getProperty('eventPersister') ->getValue($this->currentContentRepository); - /** @var EventNormalizer $eventPersister */ + /** @var EventNormalizer $eventNormalizer */ $eventNormalizer = (new \ReflectionClass($eventPersister))->getProperty('eventNormalizer') ->getValue($eventPersister); $event = $eventNormalizer->denormalize($artificiallyConstructedEvent); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php index 46b55f80fef..52e90318601 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php @@ -293,6 +293,18 @@ public function iExecuteTheFindAncestorNodesQueryIExpectTheFollowingNodes(string Assert::assertSame($expectedTotalCount ?? count($expectedNodeIds), $actualCount, 'countAncestorNodes returned an unexpected result'); } + /** + * @When /^I execute the findAncestorNodeAggregateIds query for entry node aggregate id "(?[^"]*)" I expect (?:the nodes "(?[^"]*)" to be returned in any order|no nodes to be returned)$/ + */ + public function iExecuteTheFindAncestorNodeAggregateIdsQueryIExpectTheFollowingNodes(string $entryNodeIdSerialized, string $expectedNodeIdsSerialized = ''): void + { + $entryNodeAggregateId = NodeAggregateId::fromString($entryNodeIdSerialized); + $expectedNodeIds = array_filter(explode(',', $expectedNodeIdsSerialized)); + $contentGraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName); + $actualNodeIds = $contentGraph->findAncestorNodeAggregateIds($entryNodeAggregateId)->toStringArray(); + Assert::assertEqualsCanonicalizing($expectedNodeIds, $actualNodeIds, 'findAncestorNodeAggregateIds returned an unexpected result'); + } + /** * @When /^I execute the findClosestNode query for entry node aggregate id "(?[^"]*)"(?: and filter '(?[^']*)')? I expect (?:the node "(?[^"]*)"|no node) to be returned?$/ */ diff --git a/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php index 13a240c39d7..02ba6d75ea8 100644 --- a/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php +++ b/Neos.Neos/Classes/Fusion/Cache/AssetChangeHandlerForCacheFlushing.php @@ -5,13 +5,8 @@ namespace Neos\Neos\Fusion\Cache; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; -use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Core\SharedModel\Workspace\Workspaces; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Media\Domain\Model\AssetInterface; @@ -58,7 +53,8 @@ public function registerAssetChange(AssetInterface $asset): void $workspaceNames = $this->getWorkspaceNameAndChildWorkspaceNames($contentRepository, $usage->workspaceName); foreach ($workspaceNames as $workspaceName) { - $nodeAggregate = $contentRepository->getContentGraph($workspaceName)->findNodeAggregateById($usage->nodeAggregateId); + $contentGraph = $contentRepository->getContentGraph($workspaceName); + $nodeAggregate = $contentGraph->findNodeAggregateById($usage->nodeAggregateId); if ($nodeAggregate === null) { continue; } @@ -67,7 +63,7 @@ public function registerAssetChange(AssetInterface $asset): void $workspaceName, $nodeAggregate->nodeAggregateId, $nodeAggregate->nodeTypeName, - $this->determineAncestorNodeAggregateIds($contentRepository, $workspaceName, $nodeAggregate->nodeAggregateId), + $contentGraph->findAncestorNodeAggregateIds($nodeAggregate->nodeAggregateId), ); $this->contentCacheFlusher->flushNodeAggregate($flushNodeAggregateRequest, CacheFlushingStrategy::ON_SHUTDOWN); @@ -76,25 +72,6 @@ public function registerAssetChange(AssetInterface $asset): void } } - private function determineAncestorNodeAggregateIds(ContentRepository $contentRepository, WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId): NodeAggregateIds - { - $contentGraph = $contentRepository->getContentGraph($workspaceName); - $stack = iterator_to_array($contentGraph->findParentNodeAggregates($childNodeAggregateId)); - - $ancestorNodeAggregateIds = []; - while ($stack !== []) { - $nodeAggregate = array_shift($stack); - - // Prevent infinite loops - if (!in_array($nodeAggregate->nodeAggregateId, $ancestorNodeAggregateIds, false)) { - $ancestorNodeAggregateIds[] = $nodeAggregate->nodeAggregateId; - array_push($stack, ...iterator_to_array($contentGraph->findParentNodeAggregates($nodeAggregate->nodeAggregateId))); - } - } - - return NodeAggregateIds::fromArray($ancestorNodeAggregateIds); - } - /** * @return WorkspaceName[] */ diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheFlushingStrategy.php b/Neos.Neos/Classes/Fusion/Cache/CacheFlushingStrategy.php index 2584f4fd717..d57f2f2694f 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheFlushingStrategy.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheFlushingStrategy.php @@ -6,6 +6,12 @@ enum CacheFlushingStrategy { + /** + * All content changes in the content repository are persisted immediately and thus an immediate flush is also required. + */ case IMMEDIATE; + /** + * Changes like from assets (changing a title) will only be persisted when flow is shutting down. Thus we delay the flush as well. + */ case ON_SHUTDOWN; } diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php index 349d4eb3ac7..4acf758411e 100644 --- a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php +++ b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php @@ -15,7 +15,6 @@ */ use Neos\ContentRepository\Core\EventStore\EventInterface; -use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamId; use Neos\ContentRepository\Core\Feature\Common\EmbedsNodeAggregateId; use Neos\ContentRepository\Core\Feature\Common\EmbedsWorkspaceName; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; @@ -40,7 +39,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Model\EventEnvelope; @@ -165,15 +163,14 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even } catch (WorkspaceDoesNotExist) { return; } - $nodeAggregate = $contentGraph->findNodeAggregateById( $eventInstance->getNodeAggregateId() ); if ($nodeAggregate) { $this->scheduleCacheFlushJobForNodeAggregate( $this->contentRepositoryId, - $eventInstance->workspaceName, - $nodeAggregate + $nodeAggregate, + $contentGraph->findAncestorNodeAggregateIds($eventInstance->getNodeAggregateId()), ); } } @@ -200,22 +197,22 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event } elseif ( !($eventInstance instanceof NodeAggregateWasRemoved) && $eventInstance instanceof EmbedsNodeAggregateId - && $eventInstance instanceof EmbedsContentStreamId && $eventInstance instanceof EmbedsWorkspaceName ) { try { - $nodeAggregate = $this->contentGraphReadModel->getContentGraph($eventInstance->getWorkspaceName())->findNodeAggregateById( - $eventInstance->getNodeAggregateId() - ); + $contentGraph = $this->contentGraphReadModel->getContentGraph($eventInstance->getWorkspaceName()); } catch (WorkspaceDoesNotExist) { return; } + $nodeAggregate = $contentGraph->findNodeAggregateById( + $eventInstance->getNodeAggregateId() + ); if ($nodeAggregate) { $this->scheduleCacheFlushJobForNodeAggregate( $this->contentRepositoryId, - $eventInstance->getWorkspaceName(), - $nodeAggregate + $nodeAggregate, + $contentGraph->findAncestorNodeAggregateIds($eventInstance->getNodeAggregateId()) ); } } @@ -223,16 +220,16 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event private function scheduleCacheFlushJobForNodeAggregate( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, - NodeAggregate $nodeAggregate + NodeAggregate $nodeAggregate, + NodeAggregateIds $ancestorNodeAggregateIds ): void { // we store this in an associative array deduplicate. - $this->flushNodeAggregateRequestsOnAfterCatchUp[$workspaceName->value . '__' . $nodeAggregate->nodeAggregateId->value] = FlushNodeAggregateRequest::create( + $this->flushNodeAggregateRequestsOnAfterCatchUp[$nodeAggregate->workspaceName->value . '__' . $nodeAggregate->nodeAggregateId->value] = FlushNodeAggregateRequest::create( $contentRepositoryId, - $workspaceName, + $nodeAggregate->workspaceName, $nodeAggregate->nodeAggregateId, $nodeAggregate->nodeTypeName, - $this->determineAncestorNodeAggregateIds($workspaceName, $nodeAggregate->nodeAggregateId) + $ancestorNodeAggregateIds ); } @@ -247,25 +244,6 @@ private function scheduleCacheFlushJobForWorkspaceName( ); } - private function determineAncestorNodeAggregateIds(WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId): NodeAggregateIds - { - $contentGraph = $this->contentGraphReadModel->getContentGraph($workspaceName); - $stack = iterator_to_array($contentGraph->findParentNodeAggregates($childNodeAggregateId)); - - $ancestorNodeAggregateIds = []; - while ($stack !== []) { - $nodeAggregate = array_shift($stack); - - // Prevent infinite loops - if (!in_array($nodeAggregate->nodeAggregateId, $ancestorNodeAggregateIds, false)) { - $ancestorNodeAggregateIds[] = $nodeAggregate->nodeAggregateId; - array_push($stack, ...iterator_to_array($contentGraph->findParentNodeAggregates($nodeAggregate->nodeAggregateId))); - } - } - - return NodeAggregateIds::fromArray($ancestorNodeAggregateIds); - } - public function onBeforeBatchCompleted(): void { }