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

Open
wants to merge 7 commits into
base: 9.0
Choose a base branch
from
Open
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
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
@@ -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
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\NodeUri\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->forRequest($this->request->getHttpRequest())
->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
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,13 +264,12 @@ 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);
$resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName);
} catch (NodeNotFoundException | InvalidShortcutException $exception) {
// TODO log exception
return false;
Expand All @@ -284,23 +286,19 @@ 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
*/
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 +316,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
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
*
* **See {@see EventSourcedFrontendNodeRoutePartHandler} documentation for a
* detailed explanation of the Frontend Routing process.**
*/
Expand Down