Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

!!! FEATURE: Overhaul node uri building #4892

Merged
merged 21 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ba13da3
WIP: New `NodeUriBuilder` and `NodeUriSpecification`
mhsdesign Feb 16, 2024
cf8c35b
WIP: Adjust to use new NodeAddress
mhsdesign May 13, 2024
c824604
FEATURE: Allow new nodeAddress to be serialized into uri
mhsdesign May 18, 2024
9cfc16f
TASK: Use new NodeAddress in preview action
mhsdesign May 18, 2024
c1a33c1
TASK: Use NodeAddress Uri serialization instead of json format for ro…
mhsdesign May 19, 2024
754f735
TASK: Introduce NodeUri Options in favour of `NodeUriSpecification`
mhsdesign May 19, 2024
3ebf6bb
TASK: Serialize NodeAddress as json for uris
mhsdesign May 23, 2024
f175873
TASK: Fix preview uri building to ignore custom options
mhsdesign May 25, 2024
e855be8
TASK: Document NodeUriBuilder and align behaviour to flows UriBuilder
mhsdesign May 25, 2024
47a4a13
Merge remote-tracking branch 'origin/9.0' into feature/overhaulNodeUr…
mhsdesign May 28, 2024
2c4670c
TASK: Dont use `appendExceedingArguments` for preview action
mhsdesign May 28, 2024
ceb88ab
TASK: Don't use options for `previewUriFor` but always build absolute…
mhsdesign May 28, 2024
2afdf6b
TASK: Fix WorkspacesController to redirect to node uris correctly
mhsdesign May 28, 2024
8887e77
TASK: Refine and document format and routingArguments node uri buildi…
mhsdesign May 28, 2024
c34eb9b
TASK: Document when `NoMatchingRouteException` will be thrown
mhsdesign May 28, 2024
95f1c5e
TASK: Further improve comments and exception handling
mhsdesign May 28, 2024
b08d248
TASK: Pass ActionRequest instead of destructuring HttpRequest
mhsdesign Jun 5, 2024
18f8724
TASK: Place UriBuilder directly in `FrontendRouting`
mhsdesign Jun 14, 2024
f0ba008
Merge remote-tracking branch 'origin/9.0' into feature/overhaulNodeUr…
mhsdesign Jun 14, 2024
95fa4cb
TASK: Restore feature to keep current format of action request when b…
mhsdesign Jun 16, 2024
a69fed4
TASK: Introduce dedicated `Options::withForceAbsolute`
mhsdesign Jun 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@ final public static function fromLegacyDimensionArray(array $legacyDimensionValu
return self::instance($coordinates);
}

final public static function fromUriRepresentation(string $encoded): self
{
return self::instance(json_decode(base64_decode($encoded), true));
}

/**
* Varies a dimension space point in a single coordinate
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,21 @@ public static function fromNode(Node $node): self
public static function fromArray(array $array): self
{
return new self(
ContentRepositoryId::fromString($array['contentRepositoryId']),
WorkspaceName::fromString($array['workspaceName']),
DimensionSpacePoint::fromArray($array['dimensionSpacePoint']),
NodeAggregateId::fromString($array['aggregateId'])
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)),
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)),
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)),
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))
);
}

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

public function withAggregateId(NodeAggregateId $aggregateId): self
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Tests\Unit\SharedModel\Node;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use PHPUnit\Framework\TestCase;

class NodeAddressTest extends TestCase
{
public static function jsonSerialization(): iterable
{
yield 'no dimensions' => [
'nodeAddress' => NodeAddress::create(
ContentRepositoryId::fromString('default'),
WorkspaceName::forLive(),
DimensionSpacePoint::createWithoutDimensions(),
NodeAggregateId::fromString('marcus-heinrichus')
),
'serialized' => '{"contentRepositoryId":"default","workspaceName":"live","dimensionSpacePoint":[],"aggregateId":"marcus-heinrichus"}'
];

yield 'one dimension' => [
'nodeAddress' => NodeAddress::create(
ContentRepositoryId::fromString('default'),
WorkspaceName::fromString('user-mh'),
DimensionSpacePoint::fromArray(['language' => 'de']),
NodeAggregateId::fromString('79e69d1c-b079-4535-8c8a-37e76736c445')
),
'serialized' => '{"contentRepositoryId":"default","workspaceName":"user-mh","dimensionSpacePoint":{"language":"de"},"aggregateId":"79e69d1c-b079-4535-8c8a-37e76736c445"}'
];

yield 'two dimensions' => [
'nodeAddress' => NodeAddress::create(
ContentRepositoryId::fromString('second'),
WorkspaceName::fromString('user-mh'),
DimensionSpacePoint::fromArray(['language' => 'en_US', 'audience' => 'nice people']),
NodeAggregateId::fromString('my-node-id')
),
'serialized' => '{"contentRepositoryId":"second","workspaceName":"user-mh","dimensionSpacePoint":{"language":"en_US","audience":"nice people"},"aggregateId":"my-node-id"}'
];
}

/**
* @dataProvider jsonSerialization
* @test
*/
public function serialization(NodeAddress $nodeAddress, string $expected): void
{
self::assertEquals($expected, $nodeAddress->toJson());
}

/**
* @dataProvider jsonSerialization
* @test
*/
public function deserialization(NodeAddress $expectedNodeAddress, string $encoded): void
{
$nodeAddress = NodeAddress::fromJsonString($encoded);
self::assertInstanceOf(NodeAddress::class, $nodeAddress);
self::assertTrue($expectedNodeAddress->equals($nodeAddress));
}
}
67 changes: 34 additions & 33 deletions Neos.Neos/Classes/Controller/Frontend/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

namespace Neos\Neos\Controller\Frontend;

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\InMemoryCache;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
Expand All @@ -25,6 +24,7 @@
use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
Expand All @@ -39,10 +39,8 @@
use Neos\Neos\Domain\Service\RenderingModeService;
use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException;
use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException;
use Neos\Neos\FrontendRouting\NodeAddress;
use Neos\Neos\FrontendRouting\NodeAddressFactory;
use Neos\Neos\FrontendRouting\NodeShortcutResolver;
use Neos\Neos\FrontendRouting\NodeUriBuilder;
use Neos\Neos\FrontendRouting\NodeUriBuilderFactory;
use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult;
use Neos\Neos\Utility\NodeTypeWithFallbackProvider;
use Neos\Neos\View\FusionView;
Expand Down Expand Up @@ -106,6 +104,9 @@ class NodeController extends ActionController
#[Flow\InjectConfiguration(path: "frontend.shortcutRedirectHttpStatusCode", package: "Neos.Neos")]
protected int $shortcutRedirectHttpStatusCode;

#[Flow\Inject]
protected NodeUriBuilderFactory $nodeUriBuilderFactory;

/**
* @param string $node
* @throws NodeNotFoundException
Expand All @@ -130,21 +131,14 @@ public function previewAction(string $node): void
$siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest());
$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);

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

$subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph(
$nodeAddress->dimensionSpacePoint,
$visibilityConstraints
);

$site = $subgraph->findClosestNode($nodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
if ($site === null) {
throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for address " . $nodeAddress);
}

$this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph);

$nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId);
$nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId);

if (is_null($nodeInstance)) {
throw new NodeNotFoundException(
Expand All @@ -153,12 +147,19 @@ public function previewAction(string $node): void
);
}

$site = $subgraph->findClosestNode($nodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE));
if ($site === null) {
throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for identity " . $nodeAddress->toJson());
}

$this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph);

if (
$this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)
&& !$renderingMode->isEdit
&& $nodeAddress->workspaceName->isLive() // shortcuts are only resolvable for the live workspace
) {
$this->handleShortcutNode($nodeAddress, $contentRepository);
$this->handleShortcutNode($nodeAddress);
}

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

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

$contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId);
$subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph(
$nodeAddress->dimensionSpacePoint,
VisibilityConstraints::frontend()
);

$nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId);
$nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId);
if ($nodeInstance === null) {
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);
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);
}

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

$this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph);
$this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph);

if ($this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) {
$this->handleShortcutNode($nodeAddress, $contentRepository);
$this->handleShortcutNode($nodeAddress);
}

$this->view->setOption('renderingModeName', RenderingMode::FRONTEND);
Expand Down Expand Up @@ -266,31 +267,31 @@ protected function overrideViewVariablesFromInternalArguments()
/**
* Handles redirects to shortcut targets of nodes in the live workspace.
*
* @param NodeAddress $nodeAddress
* @throws NodeNotFoundException
* @throws \Neos\Flow\Mvc\Exception\StopActionException
*/
protected function handleShortcutNode(NodeAddress $nodeAddress, ContentRepository $contentRepository): void
protected function handleShortcutNode(NodeAddress $nodeAddress): void
{
try {
$resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress, $contentRepository);
$resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress);
} catch (InvalidShortcutException $e) {
throw new NodeNotFoundException(sprintf(
'The shortcut node target of node "%s" could not be resolved: %s',
$nodeAddress,
'The shortcut node target of node %s could not be resolved: %s',
$nodeAddress->toJson(),
$e->getMessage()
), 1430218730, $e);
}
if ($resolvedTarget instanceof NodeAddress) {
if ($resolvedTarget === $nodeAddress) {
if ($nodeAddress->equals($resolvedTarget)) {
return;
}
try {
$resolvedUri = NodeUriBuilder::fromRequest($this->request)->uriFor($nodeAddress);
$resolvedUri = $this->nodeUriBuilderFactory->forActionRequest($this->request)
->uriFor($nodeAddress);
} catch (NoMatchingRouteException $e) {
throw new NodeNotFoundException(sprintf(
'The shortcut node target of node "%s" could not be resolved: %s',
$nodeAddress,
'The shortcut node target of node %s could not be resolved: %s',
$nodeAddress->toJson(),
$e->getMessage()
), 1599670695, $e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Flow\Annotations as Flow;
Expand All @@ -27,6 +28,7 @@
use Neos\Flow\Mvc\Routing\DynamicRoutePartInterface;
use Neos\Flow\Mvc\Routing\ParameterAwareRoutePartInterface;
use Neos\Flow\Mvc\Routing\RoutingMiddleware;
use Neos\Neos\Domain\Model\SiteNodeName;
use Neos\Neos\Domain\Repository\SiteRepository;
use Neos\Neos\FrontendRouting\CrossSiteLinking\CrossSiteLinkerInterface;
use Neos\Neos\FrontendRouting\DimensionResolution\DelegatingResolver;
Expand Down Expand Up @@ -201,7 +203,7 @@ public function matchWithParameters(&$requestPath, RouteParameters $parameters)
// TODO validate dsp == complete (ContentDimensionZookeeper::getAllowedDimensionSubspace()->contains()...)
// if incomplete -> no match + log

$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);
$contentRepository = $this->contentRepositoryRegistry->get($resolvedSite->getConfiguration()->contentRepositoryId);

try {
$matchResult = $this->matchUriPath(
Expand Down Expand Up @@ -240,12 +242,13 @@ private function matchUriPath(
$uriPath,
$dimensionSpacePoint->hash
);
$nodeAddress = NodeAddressFactory::create($contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId(
$documentUriPathFinder->getLiveContentStreamId(),
$nodeAddress = NodeAddress::create(
$contentRepository->id,
WorkspaceName::forLive(),
$dimensionSpacePoint,
$nodeInfo->getNodeAggregateId(),
);
return new MatchResult($nodeAddress->serializeForUri(), $nodeInfo->getRouteTags());
return new MatchResult($nodeAddress->toJson(), $nodeInfo->getRouteTags());
}

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

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

try {
$resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult);
} catch (NodeNotFoundException | InvalidShortcutException $exception) {
// TODO log exception
$resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName);
} catch (NodeNotFoundException | TargetSiteNotFoundException | InvalidShortcutException $exception) {
// TODO log exception ... yes todo
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

Expand All @@ -284,23 +286,20 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para
* To disallow showing a node actually disabled/hidden itself has to be ensured in matching a request path,
* not in building one.
*
* @param NodeAddress $nodeAddress
* @param SiteDetectionResult $currentRequestSiteDetectionResult
* @return ResolveResult
* @throws InvalidShortcutException
* @throws NodeNotFoundException
* @throws TargetSiteNotFoundException
*/
private function resolveNodeAddress(
NodeAddress $nodeAddress,
SiteDetectionResult $currentRequestSiteDetectionResult
SiteNodeName $currentRequestSiteNodeName
): ResolveResult {
// TODO: SOMEHOW FIND OTHER CONTENT REPOSITORY HERE FOR CROSS-CR LINKS!!
$contentRepository = $this->contentRepositoryRegistry->get(
$currentRequestSiteDetectionResult->contentRepositoryId
$nodeAddress->contentRepositoryId
);
$documentUriPathFinder = $contentRepository->projectionState(DocumentUriPathFinder::class);
$nodeInfo = $documentUriPathFinder->getByIdAndDimensionSpacePointHash(
$nodeAddress->nodeAggregateId,
$nodeAddress->aggregateId,
$nodeAddress->dimensionSpacePoint->hash
);

Expand All @@ -318,7 +317,7 @@ private function resolveNodeAddress(
}

$uriConstraints = UriConstraints::create();
if (!$targetSite->getNodeName()->equals($currentRequestSiteDetectionResult->siteNodeName)) {
if (!$targetSite->getNodeName()->equals($currentRequestSiteNodeName)) {
$uriConstraints = $this->crossSiteLinker->applyCrossSiteUriConstraints(
$targetSite,
$uriConstraints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
* Marker interface which can be used to replace the currently used FrontendNodeRoutePartHandler,
* to e.g. use the one with localization support.
*
* TODO CORE MIGRATION
*
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
* **See {@see EventSourcedFrontendNodeRoutePartHandler} documentation for a
* detailed explanation of the Frontend Routing process.**
*/
Expand Down
Loading
Loading