Skip to content

Commit 0f2a323

Browse files
authored
Merge pull request #4892 from mhsdesign/feature/overhaulNodeUriBuilding
!!! FEATURE: Overhaul node uri building
2 parents 56a9172 + a69fed4 commit 0f2a323

22 files changed

+748
-365
lines changed

Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php

-5
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,6 @@ final public static function fromLegacyDimensionArray(array $legacyDimensionValu
8888
return self::instance($coordinates);
8989
}
9090

91-
final public static function fromUriRepresentation(string $encoded): self
92-
{
93-
return self::instance(json_decode(base64_decode($encoded), true));
94-
}
95-
9691
/**
9792
* Varies a dimension space point in a single coordinate
9893
*/

Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,21 @@ public static function fromNode(Node $node): self
6565
public static function fromArray(array $array): self
6666
{
6767
return new self(
68-
ContentRepositoryId::fromString($array['contentRepositoryId']),
69-
WorkspaceName::fromString($array['workspaceName']),
70-
DimensionSpacePoint::fromArray($array['dimensionSpacePoint']),
71-
NodeAggregateId::fromString($array['aggregateId'])
68+
ContentRepositoryId::fromString($array['contentRepositoryId'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "contentRepositoryId" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478573)),
69+
WorkspaceName::fromString($array['workspaceName'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "workspaceName" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478580)),
70+
DimensionSpacePoint::fromArray($array['dimensionSpacePoint'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "dimensionSpacePoint" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478584)),
71+
NodeAggregateId::fromString($array['aggregateId'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "aggregateId" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478588))
7272
);
7373
}
7474

7575
public static function fromJsonString(string $jsonString): self
7676
{
77-
return self::fromArray(\json_decode($jsonString, true, JSON_THROW_ON_ERROR));
77+
try {
78+
$jsonArray = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
79+
} catch (\JsonException $e) {
80+
throw new \InvalidArgumentException(sprintf('Failed to JSON-decode NodeAddress: %s', $e->getMessage()), 1716478364, $e);
81+
}
82+
return self::fromArray($jsonArray);
7883
}
7984

8085
public function withAggregateId(NodeAggregateId $aggregateId): self
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\Core\Tests\Unit\SharedModel\Node;
6+
7+
/*
8+
* This file is part of the Neos.ContentRepository package.
9+
*
10+
* (c) Contributors of the Neos Project - www.neos.io
11+
*
12+
* This package is Open Source Software. For the full copyright and license
13+
* information, please view the LICENSE file which was distributed with this
14+
* source code.
15+
*/
16+
17+
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
18+
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
19+
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
20+
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
21+
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
22+
use PHPUnit\Framework\TestCase;
23+
24+
class NodeAddressTest extends TestCase
25+
{
26+
public static function jsonSerialization(): iterable
27+
{
28+
yield 'no dimensions' => [
29+
'nodeAddress' => NodeAddress::create(
30+
ContentRepositoryId::fromString('default'),
31+
WorkspaceName::forLive(),
32+
DimensionSpacePoint::createWithoutDimensions(),
33+
NodeAggregateId::fromString('marcus-heinrichus')
34+
),
35+
'serialized' => '{"contentRepositoryId":"default","workspaceName":"live","dimensionSpacePoint":[],"aggregateId":"marcus-heinrichus"}'
36+
];
37+
38+
yield 'one dimension' => [
39+
'nodeAddress' => NodeAddress::create(
40+
ContentRepositoryId::fromString('default'),
41+
WorkspaceName::fromString('user-mh'),
42+
DimensionSpacePoint::fromArray(['language' => 'de']),
43+
NodeAggregateId::fromString('79e69d1c-b079-4535-8c8a-37e76736c445')
44+
),
45+
'serialized' => '{"contentRepositoryId":"default","workspaceName":"user-mh","dimensionSpacePoint":{"language":"de"},"aggregateId":"79e69d1c-b079-4535-8c8a-37e76736c445"}'
46+
];
47+
48+
yield 'two dimensions' => [
49+
'nodeAddress' => NodeAddress::create(
50+
ContentRepositoryId::fromString('second'),
51+
WorkspaceName::fromString('user-mh'),
52+
DimensionSpacePoint::fromArray(['language' => 'en_US', 'audience' => 'nice people']),
53+
NodeAggregateId::fromString('my-node-id')
54+
),
55+
'serialized' => '{"contentRepositoryId":"second","workspaceName":"user-mh","dimensionSpacePoint":{"language":"en_US","audience":"nice people"},"aggregateId":"my-node-id"}'
56+
];
57+
}
58+
59+
/**
60+
* @dataProvider jsonSerialization
61+
* @test
62+
*/
63+
public function serialization(NodeAddress $nodeAddress, string $expected): void
64+
{
65+
self::assertEquals($expected, $nodeAddress->toJson());
66+
}
67+
68+
/**
69+
* @dataProvider jsonSerialization
70+
* @test
71+
*/
72+
public function deserialization(NodeAddress $expectedNodeAddress, string $encoded): void
73+
{
74+
$nodeAddress = NodeAddress::fromJsonString($encoded);
75+
self::assertInstanceOf(NodeAddress::class, $nodeAddress);
76+
self::assertTrue($expectedNodeAddress->equals($nodeAddress));
77+
}
78+
}

Neos.Neos/Classes/Controller/Frontend/NodeController.php

+34-33
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
namespace Neos\Neos\Controller\Frontend;
1616

17-
use Neos\ContentRepository\Core\ContentRepository;
1817
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches;
1918
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\InMemoryCache;
2019
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
@@ -25,6 +24,7 @@
2524
use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree;
2625
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
2726
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
27+
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
2828
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
2929
use Neos\Flow\Annotations as Flow;
3030
use Neos\Flow\Mvc\Controller\ActionController;
@@ -39,10 +39,8 @@
3939
use Neos\Neos\Domain\Service\RenderingModeService;
4040
use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException;
4141
use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException;
42-
use Neos\Neos\FrontendRouting\NodeAddress;
43-
use Neos\Neos\FrontendRouting\NodeAddressFactory;
4442
use Neos\Neos\FrontendRouting\NodeShortcutResolver;
45-
use Neos\Neos\FrontendRouting\NodeUriBuilder;
43+
use Neos\Neos\FrontendRouting\NodeUriBuilderFactory;
4644
use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult;
4745
use Neos\Neos\Utility\NodeTypeWithFallbackProvider;
4846
use Neos\Neos\View\FusionView;
@@ -106,6 +104,9 @@ class NodeController extends ActionController
106104
#[Flow\InjectConfiguration(path: "frontend.shortcutRedirectHttpStatusCode", package: "Neos.Neos")]
107105
protected int $shortcutRedirectHttpStatusCode;
108106

107+
#[Flow\Inject]
108+
protected NodeUriBuilderFactory $nodeUriBuilderFactory;
109+
109110
/**
110111
* @param string $node
111112
* @throws NodeNotFoundException
@@ -130,21 +131,14 @@ public function previewAction(string $node): void
130131
$siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest());
131132
$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);
132133

133-
$nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node);
134+
$nodeAddress = NodeAddress::fromJsonString($node);
134135

135136
$subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph(
136137
$nodeAddress->dimensionSpacePoint,
137138
$visibilityConstraints
138139
);
139140

140-
$site = $subgraph->findClosestNode($nodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
141-
if ($site === null) {
142-
throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for address " . $nodeAddress);
143-
}
144-
145-
$this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph);
146-
147-
$nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId);
141+
$nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId);
148142

149143
if (is_null($nodeInstance)) {
150144
throw new NodeNotFoundException(
@@ -153,12 +147,19 @@ public function previewAction(string $node): void
153147
);
154148
}
155149

150+
$site = $subgraph->findClosestNode($nodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
151+
if ($site === null) {
152+
throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for identity " . $nodeAddress->toJson());
153+
}
154+
155+
$this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph);
156+
156157
if (
157158
$this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)
158159
&& !$renderingMode->isEdit
159160
&& $nodeAddress->workspaceName->isLive() // shortcuts are only resolvable for the live workspace
160161
) {
161-
$this->handleShortcutNode($nodeAddress, $contentRepository);
162+
$this->handleShortcutNode($nodeAddress);
162163
}
163164

164165
$this->view->setOption('renderingModeName', $renderingMode->name);
@@ -192,33 +193,33 @@ public function previewAction(string $node): void
192193
*/
193194
public function showAction(string $node): void
194195
{
195-
$siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest());
196-
$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);
196+
$nodeAddress = NodeAddress::fromJsonString($node);
197+
unset($node);
197198

198-
$nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node);
199-
if (!$nodeAddress->isInLiveWorkspace()) {
199+
if (!$nodeAddress->workspaceName->isLive()) {
200200
throw new NodeNotFoundException('The requested node isn\'t accessible to the current user', 1430218623);
201201
}
202202

203+
$contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId);
203204
$subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph(
204205
$nodeAddress->dimensionSpacePoint,
205206
VisibilityConstraints::frontend()
206207
);
207208

208-
$nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId);
209+
$nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId);
209210
if ($nodeInstance === null) {
210-
throw new NodeNotFoundException(sprintf('The cached node address for this uri could not be resolved. Possibly you have to flush the "Flow_Mvc_Routing_Route" cache. %s', $nodeAddress), 1707300738);
211+
throw new NodeNotFoundException(sprintf('The cached node address for this uri could not be resolved. Possibly you have to flush the "Flow_Mvc_Routing_Route" cache. %s', $nodeAddress->toJson()), 1707300738);
211212
}
212213

213-
$site = $subgraph->findClosestNode($nodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
214+
$site = $subgraph->findClosestNode($nodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
214215
if ($site === null) {
215-
throw new NodeNotFoundException(sprintf('The site node of %s could not be resolved.', $nodeAddress), 1707300861);
216+
throw new NodeNotFoundException(sprintf('The site node of %s could not be resolved.', $nodeAddress->toJson()), 1707300861);
216217
}
217218

218-
$this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph);
219+
$this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph);
219220

220221
if ($this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) {
221-
$this->handleShortcutNode($nodeAddress, $contentRepository);
222+
$this->handleShortcutNode($nodeAddress);
222223
}
223224

224225
$this->view->setOption('renderingModeName', RenderingMode::FRONTEND);
@@ -266,31 +267,31 @@ protected function overrideViewVariablesFromInternalArguments()
266267
/**
267268
* Handles redirects to shortcut targets of nodes in the live workspace.
268269
*
269-
* @param NodeAddress $nodeAddress
270270
* @throws NodeNotFoundException
271271
* @throws \Neos\Flow\Mvc\Exception\StopActionException
272272
*/
273-
protected function handleShortcutNode(NodeAddress $nodeAddress, ContentRepository $contentRepository): void
273+
protected function handleShortcutNode(NodeAddress $nodeAddress): void
274274
{
275275
try {
276-
$resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress, $contentRepository);
276+
$resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress);
277277
} catch (InvalidShortcutException $e) {
278278
throw new NodeNotFoundException(sprintf(
279-
'The shortcut node target of node "%s" could not be resolved: %s',
280-
$nodeAddress,
279+
'The shortcut node target of node %s could not be resolved: %s',
280+
$nodeAddress->toJson(),
281281
$e->getMessage()
282282
), 1430218730, $e);
283283
}
284284
if ($resolvedTarget instanceof NodeAddress) {
285-
if ($resolvedTarget === $nodeAddress) {
285+
if ($nodeAddress->equals($resolvedTarget)) {
286286
return;
287287
}
288288
try {
289-
$resolvedUri = NodeUriBuilder::fromRequest($this->request)->uriFor($nodeAddress);
289+
$resolvedUri = $this->nodeUriBuilderFactory->forActionRequest($this->request)
290+
->uriFor($nodeAddress);
290291
} catch (NoMatchingRouteException $e) {
291292
throw new NodeNotFoundException(sprintf(
292-
'The shortcut node target of node "%s" could not be resolved: %s',
293-
$nodeAddress,
293+
'The shortcut node target of node %s could not be resolved: %s',
294+
$nodeAddress->toJson(),
294295
$e->getMessage()
295296
), 1599670695, $e);
296297
}

Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php

+15-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
use Neos\ContentRepository\Core\ContentRepository;
1818
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
19+
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
1920
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
2021
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
2122
use Neos\Flow\Annotations as Flow;
@@ -27,6 +28,7 @@
2728
use Neos\Flow\Mvc\Routing\DynamicRoutePartInterface;
2829
use Neos\Flow\Mvc\Routing\ParameterAwareRoutePartInterface;
2930
use Neos\Flow\Mvc\Routing\RoutingMiddleware;
31+
use Neos\Neos\Domain\Model\SiteNodeName;
3032
use Neos\Neos\Domain\Repository\SiteRepository;
3133
use Neos\Neos\FrontendRouting\CrossSiteLinking\CrossSiteLinkerInterface;
3234
use Neos\Neos\FrontendRouting\DimensionResolution\DelegatingResolver;
@@ -201,7 +203,7 @@ public function matchWithParameters(&$requestPath, RouteParameters $parameters)
201203
// TODO validate dsp == complete (ContentDimensionZookeeper::getAllowedDimensionSubspace()->contains()...)
202204
// if incomplete -> no match + log
203205

204-
$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);
206+
$contentRepository = $this->contentRepositoryRegistry->get($resolvedSite->getConfiguration()->contentRepositoryId);
205207

206208
try {
207209
$matchResult = $this->matchUriPath(
@@ -240,12 +242,13 @@ private function matchUriPath(
240242
$uriPath,
241243
$dimensionSpacePoint->hash
242244
);
243-
$nodeAddress = NodeAddressFactory::create($contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId(
244-
$documentUriPathFinder->getLiveContentStreamId(),
245+
$nodeAddress = NodeAddress::create(
246+
$contentRepository->id,
247+
WorkspaceName::forLive(),
245248
$dimensionSpacePoint,
246249
$nodeInfo->getNodeAggregateId(),
247250
);
248-
return new MatchResult($nodeAddress->serializeForUri(), $nodeInfo->getRouteTags());
251+
return new MatchResult($nodeAddress->toJson(), $nodeInfo->getRouteTags());
249252
}
250253

251254
/**
@@ -261,15 +264,14 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para
261264
$currentRequestSiteDetectionResult = SiteDetectionResult::fromRouteParameters($parameters);
262265

263266
$nodeAddress = $routeValues[$this->name];
264-
// TODO: for cross-CR links: NodeAddressInContentRepository as a new value object
265267
if (!$nodeAddress instanceof NodeAddress) {
266268
return false;
267269
}
268270

269271
try {
270-
$resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult);
271-
} catch (NodeNotFoundException | InvalidShortcutException $exception) {
272-
// TODO log exception
272+
$resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName);
273+
} catch (NodeNotFoundException | TargetSiteNotFoundException | InvalidShortcutException $exception) {
274+
// TODO log exception ... yes todo
273275
return false;
274276
}
275277

@@ -284,23 +286,20 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para
284286
* To disallow showing a node actually disabled/hidden itself has to be ensured in matching a request path,
285287
* not in building one.
286288
*
287-
* @param NodeAddress $nodeAddress
288-
* @param SiteDetectionResult $currentRequestSiteDetectionResult
289-
* @return ResolveResult
290289
* @throws InvalidShortcutException
291290
* @throws NodeNotFoundException
291+
* @throws TargetSiteNotFoundException
292292
*/
293293
private function resolveNodeAddress(
294294
NodeAddress $nodeAddress,
295-
SiteDetectionResult $currentRequestSiteDetectionResult
295+
SiteNodeName $currentRequestSiteNodeName
296296
): ResolveResult {
297-
// TODO: SOMEHOW FIND OTHER CONTENT REPOSITORY HERE FOR CROSS-CR LINKS!!
298297
$contentRepository = $this->contentRepositoryRegistry->get(
299-
$currentRequestSiteDetectionResult->contentRepositoryId
298+
$nodeAddress->contentRepositoryId
300299
);
301300
$documentUriPathFinder = $contentRepository->projectionState(DocumentUriPathFinder::class);
302301
$nodeInfo = $documentUriPathFinder->getByIdAndDimensionSpacePointHash(
303-
$nodeAddress->nodeAggregateId,
302+
$nodeAddress->aggregateId,
304303
$nodeAddress->dimensionSpacePoint->hash
305304
);
306305

@@ -318,7 +317,7 @@ private function resolveNodeAddress(
318317
}
319318

320319
$uriConstraints = UriConstraints::create();
321-
if (!$targetSite->getNodeName()->equals($currentRequestSiteDetectionResult->siteNodeName)) {
320+
if (!$targetSite->getNodeName()->equals($currentRequestSiteNodeName)) {
322321
$uriConstraints = $this->crossSiteLinker->applyCrossSiteUriConstraints(
323322
$targetSite,
324323
$uriConstraints

Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php

-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
* Marker interface which can be used to replace the currently used FrontendNodeRoutePartHandler,
2121
* to e.g. use the one with localization support.
2222
*
23-
* TODO CORE MIGRATION
24-
*
2523
* **See {@see EventSourcedFrontendNodeRoutePartHandler} documentation for a
2624
* detailed explanation of the Frontend Routing process.**
2725
*/

0 commit comments

Comments
 (0)