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
Changes from 1 commit
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
Next Next commit
WIP: New NodeUriBuilder and NodeUriSpecification
mhsdesign committed May 22, 2024
commit ba13da30b331cd08f59f03a6f83cd11cb34d9db4
152 changes: 103 additions & 49 deletions Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php
Original file line number Diff line number Diff line change
@@ -14,80 +14,134 @@

namespace Neos\Neos\FrontendRouting;

use GuzzleHttp\Psr7\Uri;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Exception\NoMatchingRouteException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\Mvc\Routing\Dto\ResolveContext;
use Neos\Flow\Mvc\Routing\Dto\RouteParameters;
use Neos\Flow\Mvc\Routing\RouterInterface;
use Psr\Http\Message\UriInterface;

/**
* Builds URIs to nodes, taking workspace (live / shared / user) into account.
* This class can also be used in order to render "preview" URLs to nodes
* that are not in the live workspace (in the Neos Backend and shared workspaces)
*/
final class NodeUriBuilder
{
private UriBuilder $uriBuilder;
/**
* @internal
*
* This context ($baseUri and $routeParameters) can be inferred from the current request.
*
* For generating node uris in cli context, you can leverage `fromBaseUri` and pass in the desired base uri,
* Wich will be used for when generating host absolute uris.
* If the base uri does not contain a host, absolute uris which would contain the host of the current request
* like from `absoluteUriFor`, will be generated without host.
*
* TODO Flows base uri configuration is ignored if not specifically added via `mergeBaseUri`
*/
public function __construct(
private readonly RouterInterface $router,
private readonly UriInterface $baseUri,
private readonly RouteParameters $routeParameters
) {
}

/**
* @Flow\Autowiring(false)
* Return human readable host relative uris if the cr of the current request matches the one of the specified node.
* For cross-links to another cr the resulting uri be absolute and contain the host of the other site's domain.
*
* As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection)
* This method requires the node to be passed to be in the live workspace and will throw otherwise.
*
* @throws NoMatchingRouteException
*/
private function __construct(UriBuilder $uriBuilder)
public function uriFor(NodeUriSpecification $specification): UriInterface
{
$this->uriBuilder = $uriBuilder;
return $this->router->resolve(
new ResolveContext(
$this->baseUri,
$this->toShowActionRouteValues($specification),
false,
ltrim($this->baseUri->getPath(), '\/'),
$this->routeParameters
)
);
}

public static function fromRequest(ActionRequest $request): self
/**
* Return human readable absolute uris with host, independent if the node is cross linked or of the current request.
* For nodes of the current cr the passed base uri will be used as host. For cross-linked nodes the host will be derived by the site's domain.
*
* As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection)
* This method requires the node to be passed to be in the live workspace and will throw otherwise.
*
* @throws NoMatchingRouteException
*/
public function absoluteUriFor(NodeUriSpecification $specification): UriInterface
{
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);

return new self($uriBuilder);
return $this->router->resolve(
new ResolveContext(
$this->baseUri,
$this->toShowActionRouteValues($specification),
true,
ltrim($this->baseUri->getPath(), '\/'),
$this->routeParameters
)
);
}

public static function fromUriBuilder(UriBuilder $uriBuilder): self
/**
* Returns a host relative uri with fully qualified node as query parameter encoded.
*/
public function previewUriFor(NodeUriSpecification $specification): UriInterface
{
return new self($uriBuilder);
return $this->router->resolve(
new ResolveContext(
$this->baseUri,
$this->toPreviewActionRouteValues($specification),
false,
ltrim($this->baseUri->getPath(), '\/'),
$this->routeParameters
)
);
}

/**
* Renders an URI for the given $nodeAddress
* If the node belongs to the live workspace, the public URL is generated
* Otherwise a preview URI is rendered (@see previewUriFor())
*
* Note: Shortcut nodes will be resolved in the RoutePartHandler thus the resulting URI will point
* to the shortcut target (node, asset or external URI)
*
* @param NodeAddress $nodeAddress
* @return UriInterface
* @throws NoMatchingRouteException if the node address does not exist
* @return array<string, mixed>
*/
public function uriFor(NodeAddress $nodeAddress): UriInterface
private function toShowActionRouteValues(NodeUriSpecification $specification): array
{
if (!$nodeAddress->workspaceName->isLive()) {
// we cannot build a human-readable uri using the showAction as
// the DocumentUriPathProjection only handles the live workspace
return $this->previewUriFor($nodeAddress);
$routeValues = $specification->routingArguments;
$routeValues['node'] = $specification->node;
$routeValues['@action'] = strtolower('show');
$routeValues['@controller'] = strtolower('Frontend\Node');
$routeValues['@package'] = strtolower('Neos.Neos');

if ($specification->format !== '') {
$routeValues['@format'] = $specification->format;
}
return new Uri($this->uriBuilder->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos'));
return $routeValues;
}

/**
* Renders a stable "preview" URI for the given $nodeAddress
* A preview URI is used to display a node that is not public yet (i.e. not in a live workspace).
*
* @param NodeAddress $nodeAddress
* @return UriInterface
* @throws NoMatchingRouteException if the node address does not exist
* @return array<string, mixed>
*/
public function previewUriFor(NodeAddress $nodeAddress): UriInterface
private function toPreviewActionRouteValues(NodeUriSpecification $specification): array
{
return new Uri($this->uriBuilder->uriFor(
'preview',
['node' => $nodeAddress->serializeForUri()],
'Frontend\Node',
'Neos.Neos'
));
// todo fully migrate me to NodeUriSpecification
$reg = \Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\ContentRepositoryRegistry\ContentRepositoryRegistry::class);
$ws = $reg->get($specification->node->contentRepositoryId)->getWorkspaceFinder()->findOneByName($specification->node->workspaceName);
$nodeAddress = new NodeAddress(
$ws->currentContentStreamId ?? throw new \Exception(),
$specification->node->dimensionSpacePoint,
$specification->node->aggregateId,
$specification->node->workspaceName,
);

$routeValues = $specification->routingArguments;
$routeValues['node'] = $nodeAddress->serializeForUri();
$routeValues['@action'] = strtolower('preview');
$routeValues['@controller'] = strtolower('Frontend\Node');
$routeValues['@package'] = strtolower('Neos.Neos');

if ($specification->format !== '') {
$routeValues['@format'] = $specification->format;
}
return $routeValues;
}
}
38 changes: 38 additions & 0 deletions Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Neos\Neos\FrontendRouting;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Helper\RequestInformationHelper;
use Neos\Flow\Http\ServerRequestAttributes;
use Neos\Flow\Mvc\Routing\Dto\RouteParameters;
use Neos\Flow\Mvc\Routing\RouterInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;

#[Flow\Scope('singleton')]
final class NodeUriBuilderFactory
{
public function __construct(
private RouterInterface $router
) {
}

public function forRequest(ServerRequestInterface $request): NodeUriBuilder
{
$baseUri = RequestInformationHelper::generateBaseUri($request);
$routeParameters = $request->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS)
?? RouteParameters::createEmpty();
return new NodeUriBuilder($this->router, $baseUri, $routeParameters);
}

public function forBaseUri(UriInterface $baseUri): NodeUriBuilder
{
// todo???
// $siteDetectionResult = SiteDetectionResult::fromRequest(new ServerRequest(method: 'GET', uri: $baseUri));
// $routeParameters = $siteDetectionResult->storeInRouteParameters(RouteParameters::createEmpty());
return new NodeUriBuilder($this->router, $baseUri, RouteParameters::createEmpty());
}
}
45 changes: 45 additions & 0 deletions Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Neos\Neos\FrontendRouting;

use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;

final readonly class NodeUriSpecification
{
/**
* @param array<string, mixed> $routingArguments
*/
private function __construct(
public NodeAddress $node,
public string $format,
public array $routingArguments,
) {
}

public static function create(NodeAddress $node): self
{
return new self($node, '', []);
}

public function withFormat(string $format): self
{
return new self($this->node, $format, $this->routingArguments);
}

/**
* @deprecated if you meant to append query parameters,
* please use {@see \Neos\Flow\Http\UriHelper::withAdditionalQueryParameters} instead:
*
* ```php
* UriHelper::withAdditionalQueryParameters($this->nodeUriBuilder->uriFor(...), ['q' => 'search term']);
* ```
*
* @param array<string, mixed> $routingArguments
*/
public function withRoutingArguments(array $routingArguments): self
{
return new self($this->node, $this->format, $routingArguments);
}
}