diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 5d26c53b9e3..3c3f1d9010f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -24,6 +24,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountBackReferencesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountChildNodesFilter; @@ -195,17 +196,26 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node return $this->fetchNode($queryBuilder); } - public function findNodeByPath(NodePath $path, ?NodeAggregateId $startingNodeAggregateId): ?Node + public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node + { + $currentNode = $this->findNodeById($startingNodeAggregateId); + + return $currentNode + ? $this->findNodeByPathFromStartingNode($currentNode, $path) + : null; + } + + public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node + { + $currentNode = $this->findRootNodeByType($path->rootNodeTypeName); + + return $currentNode + ? $this->findNodeByPathFromStartingNode($currentNode, $path->path) + : null; + } + + private function findNodeByPathFromStartingNode(Node $currentNode, NodePath $path): ?Node { - $currentNode = null; - if ($path->rootNodeTypeName) { - $currentNode = $this->findRootNodeByType($path->rootNodeTypeName); - } elseif ($startingNodeAggregateId) { - $currentNode = $this->findNodeById($startingNodeAggregateId); - } - if ($currentNode === null) { - return null; - } foreach ($path->getParts() as $edgeName) { // id exists here :) $currentNode = $this->findChildNodeConnectedThroughEdgeName($currentNode->nodeAggregateId, $edgeName); @@ -243,7 +253,7 @@ public function findPrecedingSiblingNodes(NodeAggregateId $siblingNodeAggregateI return $this->fetchNodes($queryBuilder); } - public function retrieveNodePath(NodeAggregateId $nodeAggregateId): NodePath + public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNodePath { $queryBuilderInitial = $this->createQueryBuilder() ->select('h.name, h.parentnodeanchor, h.childnodeanchor') @@ -307,7 +317,10 @@ public function retrieveNodePath(NodeAggregateId $nodeAggregateId): NodePath } } - return NodePath::fromPathSegments($pathSegments, $rootNodeTypeName); + return AbsoluteNodePath::fromComponents( + $rootNodeTypeName, + NodePath::fromPathSegments($pathSegments) + ); } public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFilter $filter): ?Subtree diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php index 755a4802765..8268b0827aa 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php @@ -26,6 +26,7 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindBackReferencesFilter; @@ -254,30 +255,33 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node ) : null; } - public function findNodeByPath( - NodePath $path, - ?NodeAggregateId $startingNodeAggregateId - ): ?Node { - $currentNode = null; - if ($path->rootNodeTypeName) { - $currentNode = $this->findRootNodeByType($path->rootNodeTypeName); - } elseif ($startingNodeAggregateId) { - $currentNode = $this->findNodeById($startingNodeAggregateId); - } - if ($currentNode === null) { - return null; - } + public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node + { + $currentNode = $this->findNodeById($startingNodeAggregateId); + + return $currentNode + ? $this->findNodeByPathFromStartingNode($currentNode, $path) + : null; + } + public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node + { + $currentNode = $this->findRootNodeByType($path->rootNodeTypeName); + + return $currentNode + ? $this->findNodeByPathFromStartingNode($currentNode, $path->path) + : null; + } + + private function findNodeByPathFromStartingNode(Node $currentNode, NodePath $path): ?Node + { foreach ($path->getParts() as $edgeName) { - $currentNode = $this->findChildNodeConnectedThroughEdgeName( - $currentNode->nodeAggregateId, - $edgeName - ); - if (!$currentNode) { + // id exists here :) + $currentNode = $this->findChildNodeConnectedThroughEdgeName($currentNode->nodeAggregateId, $edgeName); + if ($currentNode === null) { return null; } } - return $currentNode; } @@ -360,9 +364,9 @@ private function findAnySiblings( return $this->nodeFactory->mapNodeRowsToNodes($siblingsRows, $this->visibilityConstraints); } - public function retrieveNodePath(NodeAggregateId $nodeAggregateId): NodePath + public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNodePath { - return NodePath::fromString('/'); + return AbsoluteNodePath::fromString('/'); } public function findSubtree( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Dto/NodeAggregateIdsByNodePaths.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Dto/NodeAggregateIdsByNodePaths.php index a16c60024a9..f35eb3a1723 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Dto/NodeAggregateIdsByNodePaths.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Dto/NodeAggregateIdsByNodePaths.php @@ -47,7 +47,7 @@ public function __construct(array $nodeAggregateIds) ); } - $this->nodeAggregateIds[$nodePath->__toString()] = $nodeAggregateId; + $this->nodeAggregateIds[$nodePath->serializeToString()] = $nodeAggregateId; } } @@ -100,13 +100,13 @@ public function merge(self $other): self public function getNodeAggregateId(NodePath $nodePath): ?NodeAggregateId { - return $this->nodeAggregateIds[$nodePath->__toString()] ?? null; + return $this->nodeAggregateIds[$nodePath->serializeToString()] ?? null; } public function add(NodePath $nodePath, NodeAggregateId $nodeAggregateId): self { $nodeAggregateIds = $this->nodeAggregateIds; - $nodeAggregateIds[$nodePath->__toString()] = $nodeAggregateId; + $nodeAggregateIds[$nodePath->serializeToString()] = $nodeAggregateId; return new self($nodeAggregateIds); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php new file mode 100644 index 00000000000..6994e9a9768 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php @@ -0,0 +1,111 @@ +'); + if ($pivot > 0) { + $nodeTypeName = NodeTypeName::fromString(\mb_substr($value, 2, $pivot - 2)); + $path = \mb_substr($value, $pivot + 2) ?: '/'; + } else { + throw AbsoluteNodePathIsInvalid::becauseItDoesNotMatchTheRequiredPattern($value); + } + } else { + throw AbsoluteNodePathIsInvalid::becauseItDoesNotMatchTheRequiredPattern($value); + } + return new self($nodeTypeName, NodePath::fromString($path)); + } + + public static function tryFromString(string $string): ?self + { + try { + return self::fromString($string); + } catch (AbsoluteNodePathIsInvalid) { + return null; + } + } + + public function appendPathSegment(NodeName $nodeName): self + { + return new self( + $this->rootNodeTypeName, + $this->path->appendPathSegment($nodeName) + ); + } + + public function isRoot(): bool + { + return $this->path->value === '/'; + } + + /** + * @return array + */ + public function getParts(): array + { + return $this->path->getParts(); + } + + public function getDepth(): int + { + return count($this->path->getParts()); + } + + public function equals(AbsoluteNodePath $other): bool + { + return $this->path === $other->path + && $this->rootNodeTypeName->equals($other->rootNodeTypeName); + } + + public function serializeToString(): string + { + return rtrim('/<' . $this->rootNodeTypeName->value . '>/' . (ltrim($this->path->value, '/')), '/'); + } + + public function jsonSerialize(): string + { + return $this->serializeToString(); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php index 12dfd4612d5..1e2d6167be4 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountBackReferencesFilter; @@ -156,12 +157,18 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node return $parentNode; } - public function findNodeByPath(NodePath $path, ?NodeAggregateId $startingNodeAggregateId): ?Node + public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node { // TODO implement runtime caches return $this->wrappedContentSubgraph->findNodeByPath($path, $startingNodeAggregateId); } + public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node + { + // TODO implement runtime caches + return $this->wrappedContentSubgraph->findNodeByAbsolutePath($path); + } + public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $edgeName): ?Node { $namedChildNodeCache = $this->inMemoryCache->getNamedChildNodeByNodeIdCache(); @@ -189,7 +196,7 @@ public function findPrecedingSiblingNodes(NodeAggregateId $siblingNodeAggregateI return $this->wrappedContentSubgraph->findPrecedingSiblingNodes($siblingNodeAggregateId, $filter); } - public function retrieveNodePath(NodeAggregateId $nodeAggregateId): NodePath + public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNodePath { $nodePathCache = $this->inMemoryCache->getNodePathCache(); $cachedNodePath = $nodePathCache->get($nodeAggregateId); diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/InMemoryCache/NodePathCache.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/InMemoryCache/NodePathCache.php index 513e842ece1..eb33e23a7a1 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/InMemoryCache/NodePathCache.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/InMemoryCache/NodePathCache.php @@ -14,8 +14,8 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\InMemoryCache; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; /** * Node ID -> Node Path cache @@ -25,7 +25,7 @@ final class NodePathCache { /** - * @var array + * @var array */ protected array $nodePaths = []; @@ -36,7 +36,7 @@ public function __construct(bool $isEnabled) $this->isEnabled = $isEnabled; } - public function add(NodeAggregateId $nodeAggregateId, NodePath $nodePath): void + public function add(NodeAggregateId $nodeAggregateId, AbsoluteNodePath $nodePath): void { if ($this->isEnabled === false) { return; @@ -44,7 +44,7 @@ public function add(NodeAggregateId $nodeAggregateId, NodePath $nodePath): void $this->nodePaths[$nodeAggregateId->value] = $nodePath; } - public function get(NodeAggregateId $nodeAggregateId): ?NodePath + public function get(NodeAggregateId $nodeAggregateId): ?AbsoluteNodePath { if ($this->isEnabled === false) { return null; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php index c42a024e90f..58d5ae2dc56 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php @@ -151,15 +151,20 @@ public function findBackReferences(NodeAggregateId $nodeAggregateId, Filter\Find public function countBackReferences(NodeAggregateId $nodeAggregateId, Filter\CountBackReferencesFilter $filter): int; /** - * Find a single node underneath - * - the given root node type name if defined in the path - * - the given starting node aggregate ID else - * that matches the specified $path + * Find a single node underneath $startingNodeAggregateId that matches the specified $path * * NOTE: This operation is most likely to be deprecated since the concept of node paths is not really used in the core, and it has some logical issues * @return Node|null the node that matches the given $path, or NULL if no node on that path is accessible */ - public function findNodeByPath(NodePath $path, ?NodeAggregateId $startingNodeAggregateId): ?Node; + public function findNodeByPath(NodePath $path, NodeAggregateId $startingNodeAggregateId): ?Node; + + /** + * Find a single node underneath that matches the specified absolute $path + * + * NOTE: This operation is most likely to be deprecated since the concept of node paths is not really used in the core, and it has some logical issues + * @return Node|null the node that matches the given $path, or NULL if no node on that path is accessible + */ + public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node; /** * Determine the absolute path of a node @@ -167,7 +172,7 @@ public function findNodeByPath(NodePath $path, ?NodeAggregateId $startingNodeAgg * NOTE: This operation is most likely to be deprecated since the concept of node paths is not really used in the core, and it has some logical issues * @throws \InvalidArgumentException if the node path could not be retrieved because it is inaccessible or contains no valid path. The latter can happen if any node in the hierarchy has no name */ - public function retrieveNodePath(NodeAggregateId $nodeAggregateId): NodePath; + public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNodePath; /** * Count all nodes in this subgraph, including inaccessible ones! diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php index ad6ee55d65c..41ccaf8d8a9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; -use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** @@ -26,17 +25,16 @@ final class NodePath implements \JsonSerializable { private function __construct( - public readonly ?NodeTypeName $rootNodeTypeName, - public readonly string $path + public readonly string $value ) { - if ($this->path !== '/') { - $pathParts = explode('/', ltrim($this->path, '/')); + if ($this->value !== '/') { + $pathParts = explode('/', ltrim($this->value, '/')); foreach ($pathParts as $pathPart) { if (preg_match(NodeName::PATTERN, $pathPart) !== 1) { throw new \InvalidArgumentException(sprintf( 'The path "%s" is no valid NodePath because it contains a segment "%s"' . ' that is no valid NodeName', - $this->path, + $this->value, $pathPart ), 1548157108); } @@ -46,19 +44,7 @@ private function __construct( public static function fromString(string $path): self { - if (\str_starts_with($path, '/<')) { - $pivot = \mb_strpos($path, '>'); - $nodeTypeName = NodeTypeName::fromString( - \mb_substr($path, 2, $pivot - 2) - ); - $path = \mb_substr($path, $pivot + 2); - if (empty($path)) { - $path = '/'; - } - } else { - $nodeTypeName = null; - } - return new self($nodeTypeName, $path); + return new self($path); } public static function tryFromString(string $string): ?self @@ -73,22 +59,17 @@ public static function tryFromString(string $string): ?self /** * @param array $pathSegments */ - public static function fromPathSegments(array $pathSegments, ?NodeTypeName $rootNodeTypeName = null): self + public static function fromPathSegments(array $pathSegments): self { if ($pathSegments === []) { - return new self($rootNodeTypeName, '/'); + return new self('/'); } - return new self($rootNodeTypeName, '/' . implode('/', $pathSegments)); + return new self('/' . implode('/', $pathSegments)); } public function isRoot(): bool { - return $this->path === '/'; - } - - public function isAbsolute(): bool - { - return $this->rootNodeTypeName instanceof NodeTypeName; + return $this->value === '/'; } /** @@ -96,47 +77,41 @@ public function isAbsolute(): bool */ public function appendPathSegment(NodeName $nodeName): self { - return new self($this->rootNodeTypeName, $this->path . '/' . $nodeName->value); + return new self($this->value . '/' . $nodeName->value); } /** - * @return NodeName[] + * @return array */ public function getParts(): array { if ($this->isRoot()) { return []; } - $pathParts = explode('/', ltrim($this->path, '/')); + $pathParts = explode('/', ltrim($this->value, '/')); return array_map(static fn (string $pathPart) => NodeName::fromString($pathPart), $pathParts); } public function getDepth(): int { - if (!$this->isAbsolute()) { - throw new \RuntimeException(sprintf( - 'Depth of relative node path "%s" cannot be determined', - $this->path - ), 1548162166); - } - return count($this->getParts()); + throw new \RuntimeException(sprintf( + 'Depth of relative node path "%s" cannot be determined', + $this->value + ), 1548162166); } public function equals(NodePath $other): bool { - return $this->path === $other->path - && $this->rootNodeTypeName?->value === $other->rootNodeTypeName?->value; + return $this->value === $other->value; } - public function __toString(): string + public function serializeToString(): string { - return $this->rootNodeTypeName - ? rtrim('/<' . $this->rootNodeTypeName->value . '>/' . (ltrim($this->path, '/')), '/') - : $this->path; + return $this->value; } public function jsonSerialize(): string { - return $this->__toString(); + return $this->serializeToString(); } } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/AbsoluteNodePathIsInvalid.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/AbsoluteNodePathIsInvalid.php new file mode 100644 index 00000000000..aebe251b703 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/AbsoluteNodePathIsInvalid.php @@ -0,0 +1,32 @@ +" ,"' + . $attemptedValue . '" does not', + 1687207234 + ); + } +} diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php index 453628ea6e6..e46369a0006 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/NodeTraversalTrait.php @@ -228,7 +228,7 @@ public function iExecuteTheRetrieveNodePathQueryIExpectTheFollowingNodes(string if ($expectedExceptionMessage !== null) { Assert::fail('Expected an exception but none was thrown'); } - Assert::assertSame($expectedPathSerialized, $actualNodePath->__toString()); + Assert::assertSame($expectedPathSerialized, $actualNodePath->serializeToString()); } } diff --git a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php index 2204b2604e9..e33288bf11a 100644 --- a/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php +++ b/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/ProjectedNodeTrait.php @@ -167,7 +167,7 @@ public function iExpectPathToLeadToNode(string $serializedNodePath, string $seri $expectedDiscriminator = NodeDiscriminator::fromShorthand($serializedNodeDiscriminator); $this->initializeCurrentNodesFromContentSubgraphs(function (ContentSubgraphInterface $subgraph, string $adapterName) use ($nodePath, $expectedDiscriminator) { $currentNode = $subgraph->findNodeByPath($nodePath, $this->getRootNodeAggregateId()); - Assert::assertNotNull($currentNode, 'No node could be found by node path "' . $nodePath->__toString() . '" in content subgraph "' . $this->dimensionSpacePoint->toJson() . '@' . $this->contentStreamId->value . '" and adapter "' . $adapterName . '"'); + Assert::assertNotNull($currentNode, 'No node could be found by node path "' . $nodePath->serializeToString() . '" in content subgraph "' . $this->dimensionSpacePoint->toJson() . '@' . $this->contentStreamId->value . '" and adapter "' . $adapterName . '"'); $actualDiscriminator = NodeDiscriminator::fromNode($currentNode); Assert::assertTrue($expectedDiscriminator->equals($actualDiscriminator), 'Node discriminators do not match. Expected was ' . json_encode($expectedDiscriminator) . ' , given was ' . json_encode($actualDiscriminator) . ' in adapter "' . $adapterName . '"'); return $currentNode; @@ -187,7 +187,7 @@ public function iExpectPathToLeadToNoNode(string $serializedNodePath): void $nodePath = NodePath::fromString($serializedNodePath); foreach ($this->getCurrentSubgraphs() as $adapterName => $subgraph) { $nodeByPath = $subgraph->findNodeByPath($nodePath, $this->getRootNodeAggregateId()); - Assert::assertNull($nodeByPath, 'A node was found by node path "' . $nodePath->__toString() . '" in content subgraph "' . $this->dimensionSpacePoint->toJson() . '@' . $this->contentStreamId->value . '" and adapter "' . $adapterName . '"'); + Assert::assertNull($nodeByPath, 'A node was found by node path "' . $nodePath->serializeToString() . '" in content subgraph "' . $this->dimensionSpacePoint->toJson() . '@' . $this->contentStreamId->value . '" and adapter "' . $adapterName . '"'); } } diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/AbsoluteNodePathTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/AbsoluteNodePathTest.php new file mode 100644 index 00000000000..a08cced76e5 --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/AbsoluteNodePathTest.php @@ -0,0 +1,68 @@ + $expectedParts */ + array $expectedParts, + ?int $expectedDepth + ): void { + $subject = AbsoluteNodePath::fromString($serializedPath); + + self::assertSame($expectedRelativePath, $subject->path->value); + self::assertSame($expectedRootNodeTypeName, $subject->rootNodeTypeName); + self::assertSame($expectedRootState, $subject->isRoot()); + self::assertEquals($expectedParts, $subject->getParts()); + if (!is_null($expectedDepth)) { + self::assertSame($expectedDepth, $subject->getDepth()); + } + self::assertSame($serializedPath, $subject->serializeToString()); + } + + public static function serializedPathProvider(): iterable + { + yield 'emptyAbsolute' => [ + 'serializedPath' => '/', + 'expectedRelativePath' => '/', + 'expectedRootNodeTypeName' => NodeTypeName::fromString('Neos.ContentRepository:Root'), + 'expectedRootState' => true, + 'expectedParts' => [], + 'expectedDepth' => 0 + ]; + + yield 'absolute' => [ + 'serializedPath' => '//child/grandchild', + 'expectedRelativePath' => 'child/grandchild', + 'expectedRootNodeTypeName' => NodeTypeName::fromString('Neos.ContentRepository:Root'), + 'expectedRootState' => false, + 'expectedParts' => [ + NodeName::fromString('child'), + NodeName::fromString('grandchild'), + ], + 'expectedDepth' => 2 + ]; + } +} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php index 8fb860aa9f0..10d8b2c3a81 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use PHPUnit\Framework\TestCase; @@ -24,62 +23,35 @@ class NodePathTest extends TestCase public function testDeserialization( string $serializedPath, string $expectedRelativePath, - ?NodeTypeName $expectedRootNodeTypeName, bool $expectedRootState, - bool $expectedAbsoluteState, /** @var array $expectedParts */ array $expectedParts, - ?int $expectedDepth ): void { $subject = NodePath::fromString($serializedPath); - self::assertSame($expectedRelativePath, $subject->path); - self::assertSame($expectedRootNodeTypeName, $subject->rootNodeTypeName); + self::assertSame($expectedRelativePath, $subject->value); self::assertSame($expectedRootState, $subject->isRoot()); - self::assertSame($expectedAbsoluteState, $subject->isAbsolute()); self::assertEquals($expectedParts, $subject->getParts()); - if (!is_null($expectedDepth)) { - self::assertSame($expectedDepth, $subject->getDepth()); - } - self::assertSame($serializedPath, $subject->__toString()); + self::assertSame($serializedPath, $subject->serializeToString()); } public static function serializedPathProvider(): iterable { - yield 'relative' => [ + yield 'nonRoot' => [ 'serializedPath' => 'child/grandchild', 'expectedRelativePath' => 'child/grandchild', - 'expectedRootNodeTypeName' => null, 'expectedRootState' => false, - 'expectedAbsoluteState' => false, 'expectedParts' => [ NodeName::fromString('child'), NodeName::fromString('grandchild'), - ], - 'expectedDepth' => null + ] ]; - yield 'emptyAbsolute' => [ - 'serializedPath' => '/', + yield 'root' => [ + 'serializedPath' => '/', 'expectedRelativePath' => '/', - 'expectedRootNodeTypeName' => NodeTypeName::fromString('Neos.ContentRepository:Root'), 'expectedRootState' => true, - 'expectedAbsoluteState' => true, - 'expectedParts' => [], - 'expectedDepth' => 0 - ]; - - yield 'absolute' => [ - 'serializedPath' => '//child/grandchild', - 'expectedRelativePath' => 'child/grandchild', - 'expectedRootNodeTypeName' => NodeTypeName::fromString('Neos.ContentRepository:Root'), - 'expectedRootState' => false, - 'expectedAbsoluteState' => true, - 'expectedParts' => [ - NodeName::fromString('child'), - NodeName::fromString('grandchild'), - ], - 'expectedDepth' => 2 + 'expectedParts' => [] ]; } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/VisitedNodeAggregates.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/VisitedNodeAggregates.php index 822815d55c5..19891f15d31 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/VisitedNodeAggregates.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/VisitedNodeAggregates.php @@ -41,9 +41,9 @@ public function add(NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $co } foreach ($coveredDimensionSpacePoints as $dimensionSpacePoint) { $visitedNodeAggregate->addVariant(OriginDimensionSpacePoint::fromDimensionSpacePoint($dimensionSpacePoint), $parentNodeAggregateId); - $pathAndDimensionSpacePointHash = $nodePath->__toString() . '__' . $dimensionSpacePoint->hash; + $pathAndDimensionSpacePointHash = $nodePath->serializeToString() . '__' . $dimensionSpacePoint->hash; if (isset($this->byPathAndDimensionSpacePoint[$pathAndDimensionSpacePointHash])) { - throw new MigrationException(sprintf('Node "%s" with path "%s" and dimension space point "%s" was already visited before', $nodeAggregateId->value, $nodePath->__toString(), $dimensionSpacePoint->toJson()), 1655900356); + throw new MigrationException(sprintf('Node "%s" with path "%s" and dimension space point "%s" was already visited before', $nodeAggregateId->value, $nodePath->serializeToString(), $dimensionSpacePoint->toJson()), 1655900356); } $this->byPathAndDimensionSpacePoint[$pathAndDimensionSpacePointHash] = $visitedNodeAggregate; } @@ -71,11 +71,11 @@ public function alreadyVisitedOriginDimensionSpacePoints(NodeAggregateId $nodeAg public function findMostSpecificParentNodeInDimensionGraph(NodePath $nodePath, OriginDimensionSpacePoint $originDimensionSpacePoint, InterDimensionalVariationGraph $interDimensionalVariationGraph): ?VisitedNodeAggregate { $dimensionSpacePoint = $originDimensionSpacePoint->toDimensionSpacePoint(); - $nodePathParts = explode('/', ltrim($nodePath->__toString(), '/')); + $nodePathParts = explode('/', ltrim($nodePath->serializeToString(), '/')); array_pop($nodePathParts); $parentPath = NodePath::fromPathSegments($nodePathParts); while ($dimensionSpacePoint !== null) { - $parentPathAndDimensionSpacePointHash = strtolower($parentPath->__toString()) . '__' . $dimensionSpacePoint->hash; + $parentPathAndDimensionSpacePointHash = strtolower($parentPath->serializeToString()) . '__' . $dimensionSpacePoint->hash; if (isset($this->byPathAndDimensionSpacePoint[$parentPathAndDimensionSpacePointHash])) { return $this->byPathAndDimensionSpacePoint[$parentPathAndDimensionSpacePointHash]; } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php index d7cd24c9236..66990775f16 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php @@ -12,6 +12,7 @@ */ use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; @@ -140,7 +141,8 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $filterResults = $this->addNodesById($nodeAggregateId, $entryPoints, $filterResults); $generatedNodes = true; } elseif (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) { - $nodePath = NodePath::fromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']); + $nodePath = AbsoluteNodePath::tryFromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']) + ?: NodePath::fromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']); $filterResults = $this->addNodesByPath($nodePath, $entryPoints, $filterResults); $generatedNodes = true; } @@ -232,21 +234,15 @@ protected function addNodesById( * @param array $result * @return array */ - protected function addNodesByPath(NodePath $nodePath, array $entryPoints, array $result): array + protected function addNodesByPath(NodePath|AbsoluteNodePath $nodePath, array $entryPoints, array $result): array { foreach ($entryPoints as $entryPoint) { /** @var ContentSubgraphInterface $subgraph */ $subgraph = $entryPoint['subgraph']; foreach ($entryPoint['nodes'] as $node) { /** @var Node $node */ - if ($nodePath->isAbsolute()) { - $rootNode = $node; - while ($rootNode instanceof Node && !$rootNode->classification->isRoot()) { - $rootNode = $subgraph->findParentNode($rootNode->nodeAggregateId); - } - if ($rootNode instanceof Node) { - $nodeByPath = $subgraph->findNodeByPath($nodePath, $rootNode->nodeAggregateId); - } + if ($nodePath instanceof AbsoluteNodePath) { + $nodeByPath = $subgraph->findNodeByAbsolutePath($nodePath); } else { $nodeByPath = $subgraph->findNodeByPath($nodePath, $node->nodeAggregateId); } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php index 7eba58ad1b1..04e606b8e9e 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php @@ -92,7 +92,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): mixed $element = $context[0]; if ($propertyName === '_path') { $subgraph = $this->contentRepositoryRegistry->subgraphForNode($element); - return $subgraph->retrieveNodePath($element->nodeAggregateId)->__toString(); + return $subgraph->retrieveNodePath($element->nodeAggregateId)->serializeToString(); } if ($propertyName === '_identifier') { // TODO: deprecated (Neos <9 case) diff --git a/Neos.ContentRepository.Security/Classes/Authorization/Privilege/Node/NodePrivilegeContext.php b/Neos.ContentRepository.Security/Classes/Authorization/Privilege/Node/NodePrivilegeContext.php index 59d0fb3e206..3652b743bec 100644 --- a/Neos.ContentRepository.Security/Classes/Authorization/Privilege/Node/NodePrivilegeContext.php +++ b/Neos.ContentRepository.Security/Classes/Authorization/Privilege/Node/NodePrivilegeContext.php @@ -61,7 +61,7 @@ public function isAncestorNodeOf(string $nodePathOrIdentifier): bool return $nodePathOrResult; } - return str_starts_with($nodePathOrResult, $this->getSubgraph()->retrieveNodePath($this->node->nodeAggregateId)->__toString()); + return str_starts_with($nodePathOrResult, $this->getSubgraph()->retrieveNodePath($this->node->nodeAggregateId)->serializeToString()); } /** @@ -80,7 +80,7 @@ public function isDescendantNodeOf(string $nodePathOrIdentifier): bool return $nodePathOrResult; } - return str_starts_with($this->getSubgraph()->retrieveNodePath($this->node->nodeAggregateId)->__toString(), $nodePathOrResult); + return str_starts_with($this->getSubgraph()->retrieveNodePath($this->node->nodeAggregateId)->serializeToString(), $nodePathOrResult); } /** @@ -186,7 +186,7 @@ protected function resolveNodePathOrResult(string $nodePathOrIdentifier): bool|s if (is_null($otherNode)) { return false; } - return $this->getSubgraph()->retrieveNodePath($otherNode->nodeAggregateId)->__toString() . '/'; + return $this->getSubgraph()->retrieveNodePath($otherNode->nodeAggregateId)->serializeToString() . '/'; } catch (\InvalidArgumentException $e) { return rtrim($nodePathOrIdentifier, '/') . '/'; } diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index e4fa4dee4f9..acb25af5020 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -15,6 +15,7 @@ namespace Neos\Neos\Controller\Frontend; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\InMemoryCache; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -348,7 +349,7 @@ private function fillCacheWithContentNodes( private static function fillCacheInternal( Subtree $subtree, Node $parentNode, - NodePath $parentNodePath, + AbsoluteNodePath $parentNodePath, InMemoryCache $inMemoryCache ): void { $node = $subtree->node; diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index 1d852dee517..cc399b5fd80 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -778,7 +778,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos $node = $subgraph->findNodeById($change->nodeAggregateId); if ($node) { - $pathParts = explode('/', $subgraph->retrieveNodePath($node->nodeAggregateId)->__toString()); + $pathParts = explode('/', $subgraph->retrieveNodePath($node->nodeAggregateId)->serializeToString()); if (count($pathParts) > 2) { $siteNodeName = $pathParts[2]; $document = null; @@ -799,12 +799,12 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos assert($document instanceof Node); $documentPath = implode('/', array_slice(explode( '/', - $subgraph->retrieveNodePath($document->nodeAggregateId)->__toString() + $subgraph->retrieveNodePath($document->nodeAggregateId)->serializeToString() ), 3)); $relativePath = str_replace( sprintf('//%s/%s', $siteNodeName, $documentPath), '', - $subgraph->retrieveNodePath($node->nodeAggregateId)->__toString() + $subgraph->retrieveNodePath($node->nodeAggregateId)->serializeToString() ); if (!isset($siteChanges[$siteNodeName]['siteNode'])) { $siteChanges[$siteNodeName]['siteNode'] diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index e47f2780dd0..acb9f4b4ddc 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; @@ -118,7 +119,7 @@ public function indexAction( $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); $nodePath = $contextNode - ? NodePath::tryFromString($contextNode) + ? AbsoluteNodePath::tryFromString($contextNode) : null; $nodeAddress = null; if (!$nodePath) { @@ -155,8 +156,8 @@ public function indexAction( if (!is_null($nodeAddress)) { $entryNode = $subgraph->findNodeById($nodeAddress->nodeAggregateId); } else { - /** @var NodePath $nodePath */ - $entryNode = $subgraph->findNodeByPath($nodePath, null); + /** @var AbsoluteNodePath $nodePath */ + $entryNode = $subgraph->findNodeByAbsolutePath($nodePath); } $nodes = !is_null($entryNode) ? $subgraph->findDescendantNodes( diff --git a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php index 882d9afe1b7..71e2e3f8cdc 100644 --- a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php @@ -14,6 +14,7 @@ namespace Neos\Neos\Fusion\Helper; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; @@ -53,10 +54,13 @@ public function nearestContentCollection(Node $node, string $nodePath): Node $contentCollectionType ), 1409300545); } - $subNode = $this->contentRepositoryRegistry->subgraphForNode($node)->findNodeByPath( - NodePath::fromString($nodePath), - $node->nodeAggregateId - ); + $nodePath = AbsoluteNodePath::tryFromString($nodePath) + ?: NodePath::fromString($nodePath); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + + $subNode = $nodePath instanceof AbsoluteNodePath + ? $subgraph->findNodeByAbsolutePath($nodePath) + : $subgraph->findNodeByPath($nodePath, $node->nodeAggregateId); if ($subNode !== null && $subNode->nodeType->isOfType($contentCollectionType)) { return $subNode; @@ -68,7 +72,7 @@ public function nearestContentCollection(Node $node, string $nodePath): Node . ' You might want to adjust your node type configuration and create the missing child node' . ' through the "flow node:repair --node-type %s" command.', $contentCollectionType, - $nodePathOfNode->__toString(), + $nodePathOfNode->serializeToString(), $nodePath, $node->nodeType->name->value ), 1389352984); @@ -116,7 +120,7 @@ public function depth(Node $node): int public function path(Node $node): string { $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); - return $subgraph->retrieveNodePath($node->nodeAggregateId)->__toString(); + return $subgraph->retrieveNodePath($node->nodeAggregateId)->serializeToString(); } public function isLive(Node $node): bool diff --git a/Neos.Neos/Classes/Service/View/NodeView.php b/Neos.Neos/Classes/Service/View/NodeView.php index 0740fc8c918..e2a25536fa1 100644 --- a/Neos.Neos/Classes/Service/View/NodeView.php +++ b/Neos.Neos/Classes/Service/View/NodeView.php @@ -285,8 +285,8 @@ protected function collectChildNodeData( $expand === false && $untilNode !== null && str_starts_with( - $subgraph->retrieveNodePath($untilNode->nodeAggregateId)->__toString(), - $subgraph->retrieveNodePath($childNode->nodeAggregateId)->__toString() + $subgraph->retrieveNodePath($untilNode->nodeAggregateId)->serializeToString(), + $subgraph->retrieveNodePath($childNode->nodeAggregateId)->serializeToString() ) && $childNode !== $untilNode ) { @@ -351,11 +351,11 @@ protected function collectChildNodeData( public function collectParentNodeData(Node $rootNode, Nodes $nodes): array { $subgraph = $this->contentRepositoryRegistry->subgraphForNode($rootNode); - $rootNodePath = $subgraph->retrieveNodePath($rootNode->nodeAggregateId)->__toString(); + $rootNodePath = $subgraph->retrieveNodePath($rootNode->nodeAggregateId)->serializeToString(); $nodeCollection = []; $addNode = function (Node $node, bool $matched) use ($subgraph, $rootNodePath, &$nodeCollection) { - $nodePath = $subgraph->retrieveNodePath($node->nodeAggregateId)->__toString(); + $nodePath = $subgraph->retrieveNodePath($node->nodeAggregateId)->serializeToString(); $path = str_replace('/', '.children.', substr($nodePath, strlen($rootNodePath) + 1)); if ($path !== '') { $nodeCollection = Arrays::setValueByPath($nodeCollection, $path . '.node', $node);