Skip to content

Commit

Permalink
Merge pull request #4892 from mhsdesign/feature/overhaulNodeUriBuilding
Browse files Browse the repository at this point in the history
!!! FEATURE: Overhaul node uri building
  • Loading branch information
mhsdesign authored Jun 23, 2024
2 parents 56a9172 + a69fed4 commit 0f2a323
Show file tree
Hide file tree
Showing 22 changed files with 748 additions and 365 deletions.
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
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
*
* **See {@see EventSourcedFrontendNodeRoutePartHandler} documentation for a
* detailed explanation of the Frontend Routing process.**
*/
Expand Down
Loading

0 comments on commit 0f2a323

Please sign in to comment.