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: http response support in fusion fusion view #4899

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b40282f
WIP: FEATURE: Fusion Runtime render directly http response
mhsdesign Jan 28, 2024
8567802
WIP: Neos Fusion View use Runtime::renderResponse
mhsdesign Jan 28, 2024
d8d16db
WIP: Hacky HttpResponseConstraints
mhsdesign Jan 28, 2024
c12b3c8
WIP: Hurray make Runtime independent of ControllerContext
mhsdesign Jan 28, 2024
143da92
WIP: Migrate PluginImplementation to use `unsafeHttpResponseConstrains`
mhsdesign Jan 28, 2024
b24aee4
WIP: FusionObject to not depend on the controller context
mhsdesign Jan 28, 2024
2d620e5
WIP: Skip non working test :(
mhsdesign Jan 28, 2024
56b460a
TASK: Migrate further fusion objects to `$this->runtime->fusionGlobal…
mhsdesign Jan 30, 2024
520b6e4
Revert introduction of `HttpResponseConstraints` and api in runtime t…
mhsdesign Jan 30, 2024
0b8c649
TASK: Migrate further fusion objects to `$this->runtime->fusionGlobal…
mhsdesign Jan 30, 2024
dcfcbb5
TASK: Document legacy Runtime::getControllerContext
mhsdesign Feb 3, 2024
cad4823
TASK: Extract controller context legacy layer into withSimulatedLegac…
mhsdesign Feb 3, 2024
1dd307c
TASK: Runtime fix `renderResponse` lock not being released
mhsdesign Feb 16, 2024
b737c01
TASK: Runtime `renderResponse` unwrap `RuntimeException` itself
mhsdesign Feb 16, 2024
310cb7d
TASK: Add Fusion RuntimeException::getWrappedException
mhsdesign Feb 18, 2024
96bcea1
TASK: Remove manual http response parsing from FusionExceptionView
mhsdesign Feb 18, 2024
3862d0f
TASK: Revert Fusion `FusionView` HttpResponse support
mhsdesign Feb 18, 2024
df8cf3c
WIP: Add Fusion `FusionView` HttpResponse support
mhsdesign Feb 18, 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
172 changes: 131 additions & 41 deletions Neos.Fusion/Classes/Core/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
* source code.
*/

use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\ResponseInterface;
use Neos\Eel\Utility as EelUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
Expand Down Expand Up @@ -109,9 +113,9 @@ class Runtime
protected $runtimeConfiguration;

/**
* @deprecated
* @deprecated legacy layer {@see self::getControllerContext()}
*/
protected ControllerContext $controllerContext;
private ?ActionResponse $legacyActionResponseForCurrentRendering = null;

/**
* @var array
Expand Down Expand Up @@ -150,44 +154,6 @@ public function __construct(
$this->fusionGlobals = $fusionGlobals;
}

/**
* @deprecated {@see self::getControllerContext()}
* @internal
*/
public function setControllerContext(ControllerContext $controllerContext): void
{
$this->controllerContext = $controllerContext;
}

/**
* Returns the context which has been passed by the currently active MVC Controller
*
* DEPRECATED CONCEPT. We only implement this as backwards-compatible layer.
*
* @deprecated use `Runtime::fusionGlobals->get('request')` instead to get the request. {@see FusionGlobals::get()}
* @internal
*/
public function getControllerContext(): ControllerContext
{
if (isset($this->controllerContext)) {
return $this->controllerContext;
}

if (!($request = $this->fusionGlobals->get('request')) instanceof ActionRequest) {
throw new Exception(sprintf('Expected Fusion variable "request" to be of type ActionRequest, got value of type "%s".', get_debug_type($request)), 1693558026485);
}

$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);

return $this->controllerContext = new ControllerContext(
$request,
new ActionResponse(),
new Arguments([]),
$uriBuilder
);
}

/**
* Inject settings of this package
*
Expand Down Expand Up @@ -298,6 +264,45 @@ public function getLastEvaluationStatus()
return $this->lastEvaluationStatus;
}

public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface
{
/** Unlike pushContextArray, we don't allow to overrule fusion globals {@see self::pushContext} */
foreach ($contextArray as $key => $_) {
if ($this->fusionGlobals->has($key)) {
throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063);
}
}
$this->pushContextArray($contextArray);

return $this->withSimulatedLegacyControllerContext(function () use ($fusionPath) {
try {
$output = $this->render($fusionPath);
} catch (RuntimeException $exception) {
throw $exception->getWrappedException();
} finally {
$this->popContext();
}

/**
* parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message"
* {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation}
*/
$outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/');
if ($outputStringHasHttpPreamble) {
return Message::parseResponse($output);
}

$stream = match (true) {
is_string($output),
$output instanceof \Stringable => Utils::streamFor((string)$output),
$output === null, $output === false => Utils::streamFor(''),
default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898)
};

return new Response(body: $stream);
});
}

/**
* Render an absolute Fusion path and return the result.
*
Expand Down Expand Up @@ -625,7 +630,7 @@ protected function prepareContextForFusionObject(AbstractFusionObject $fusionObj
$newContextArray ??= $this->currentContext;
foreach ($fusionConfiguration['__meta']['context'] as $contextKey => $contextValue) {
if ($this->fusionGlobals->has($contextKey)) {
throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1694247627130);
throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1706452069);
}
$newContextArray[$contextKey] = $this->evaluate($fusionPath . '/__meta/context/' . $contextKey, $fusionObject, self::BEHAVIOR_EXCEPTION);
}
Expand Down Expand Up @@ -929,6 +934,91 @@ protected function throwExceptionForUnrenderablePathIfNeeded($fusionPath, $fusio
}
}

/**
* Implements the legacy controller context simulation {@see self::getControllerContext()}
*
* Initially it was possible to mutate the current response of the active MVC controller though $response.
* While HIGHLY internal behaviour and ONLY to be used by Neos.Fusion.Form or Neos.Neos:Plugin
* this legacy layer is in place still allows this functionality.
*
* @param \Closure(): ResponseInterface $renderer
*/
private function withSimulatedLegacyControllerContext(\Closure $renderer): ResponseInterface
{
if ($this->legacyActionResponseForCurrentRendering !== null) {
throw new Exception('Recursion detected in `Runtime::renderResponse`. This entry point is only allowed to be invoked once per rendering.', 1706993940);
}
$this->legacyActionResponseForCurrentRendering = new ActionResponse();

// actual rendering
try {
$httpResponse = $renderer();
} finally {
$toBeMergedLegacyActionResponse = $this->legacyActionResponseForCurrentRendering;
// reset for next render
$this->legacyActionResponseForCurrentRendering = null;
}

// transfer possible headers that have been set dynamically
foreach ($toBeMergedLegacyActionResponse->buildHttpResponse()->getHeaders() as $name => $values) {
$httpResponse = $httpResponse->withAddedHeader($name, $values);
}
// if the status code is 200 we assume it's the default and will not overrule it
if ($toBeMergedLegacyActionResponse->getStatusCode() !== 200) {
$httpResponse = $httpResponse->withStatus($toBeMergedLegacyActionResponse->getStatusCode());
}

return $httpResponse;
}

/**
* The concept of the controller context inside Fusion has been deprecated.
*
* To migrate the use case of fetching the active request, please look into {@see FusionGlobals::get()} instead.
* By convention, an {@see ActionRequest} will be available as `request`:
*
* ```php
* $actionRequest = $this->runtime->fusionGlobals->get('request');
* if (!$actionRequest instanceof ActionRequest) {
* // fallback or error
* }
* ```
*
* To get an {@see UriBuilder} proceed with:
*
* ```php
* $uriBuilder = new UriBuilder();
* $uriBuilder->setRequest($actionRequest);
* ```
*
* WARNING:
* Invoking this backwards-compatible layer is possibly unsafe, if the rendering was not started
* in {@see self::renderResponse()} or no `request` global is available. This will raise an exception.
*
* @deprecated with Neos 9.0
* @internal
*/
public function getControllerContext(): ControllerContext
{
// legacy controller context layer
$actionRequest = $this->fusionGlobals->get('request');
if ($this->legacyActionResponseForCurrentRendering === null || !$actionRequest instanceof ActionRequest) {
throw new Exception(sprintf('Fusions simulated legacy controller context is only available inside `Runtime::renderResponse` and when the Fusion global "request" is an ActionRequest.'), 1706458355);
}

$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($actionRequest);

return new ControllerContext(
$actionRequest,
// expose action response to be possibly mutated in neos forms or fusion plugins.
// this behaviour is highly internal and deprecated!
$this->legacyActionResponseForCurrentRendering,
new Arguments([]),
$uriBuilder
);
}

/**
* Configures this runtime to override the default exception handler configured in the settings
* or via Fusion's \@exceptionHandler {@see AbstractRenderingExceptionHandler}.
Expand Down
29 changes: 4 additions & 25 deletions Neos.Fusion/Classes/Core/RuntimeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
use Neos\Eel\Utility as EelUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\ActionResponse;
use Neos\Flow\Mvc\Controller\Arguments;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Mvc\Routing\UriBuilder;

/**
* @Flow\Scope("singleton")
Expand All @@ -44,19 +41,18 @@ class RuntimeFactory
*/
public function create(array $fusionConfiguration, ControllerContext $controllerContext = null): Runtime
{
if ($controllerContext === null) {
$controllerContext = self::createControllerContextFromEnvironment();
}
$defaultContextVariables = EelUtility::getDefaultContextVariables(
$this->defaultContextConfiguration ?? []
);
$runtime = new Runtime(
FusionConfiguration::fromArray($fusionConfiguration),
FusionGlobals::fromArray(
['request' => $controllerContext->getRequest(), ...$defaultContextVariables]
[
'request' => $controllerContext?->getRequest() ?? ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()),
...$defaultContextVariables
]
)
);
$runtime->setControllerContext($controllerContext);
return $runtime;
}

Expand All @@ -81,21 +77,4 @@ public function createFromSourceCode(
$fusionGlobals
);
}

private static function createControllerContextFromEnvironment(): ControllerContext
{
$httpRequest = ServerRequest::fromGlobals();

$request = ActionRequest::fromHttpRequest($httpRequest);

$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);

return new ControllerContext(
$request,
new ActionResponse(),
new Arguments([]),
$uriBuilder
);
}
}
9 changes: 9 additions & 0 deletions Neos.Fusion/Classes/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ public function getFusionPath()
{
return $this->fusionPath;
}

/**
* Unwrap this Fusion RuntimeException
*/
public function getWrappedException(): \Exception
{
/** @phpstan-ignore-next-line due to overridden construction, we are sure that the previous exists. */
return $this->getPrevious();
}
}
10 changes: 6 additions & 4 deletions Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ public function evaluate()
} else {
$package = $this->getPackage();
if ($package === null) {
$controllerContext = $this->runtime->getControllerContext();
/** @var $actionRequest ActionRequest */
$actionRequest = $controllerContext->getRequest();
$package = $actionRequest->getControllerPackageKey();
$possibleRequest = $this->runtime->fusionGlobals->get('request');
if ($possibleRequest instanceof ActionRequest) {
$package = $possibleRequest->getControllerPackageKey();
} else {
throw new \RuntimeException('Could not infer package-key from action request. Please render Fusion with request or specify a package-key.', 1706624314);
}
}
}
$localize = $this->isLocalize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function getPath()
*/
public function evaluate()
{
$actionRequest = $this->runtime->getControllerContext()->getRequest();
$actionRequest = $this->runtime->fusionGlobals->get('request');
if (!$actionRequest instanceof ActionRequest) {
$actionRequest = null;
}
Expand Down
18 changes: 16 additions & 2 deletions Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* source code.
*/

use GuzzleHttp\Psr7\ServerRequest;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Routing\UriBuilder;

/**
* A Fusion UriBuilder object
Expand Down Expand Up @@ -150,8 +153,19 @@ public function isAbsolute()
*/
public function evaluate()
{
$controllerContext = $this->runtime->getControllerContext();
$uriBuilder = $controllerContext->getUriBuilder()->reset();
$uriBuilder = new UriBuilder();
$possibleRequest = $this->runtime->fusionGlobals->get('request');
if ($possibleRequest instanceof ActionRequest) {
$uriBuilder->setRequest($possibleRequest);
} else {
// unfortunately, the uri-builder always needs a request at hand and cannot build uris without
// even, if the default param merging would not be required
// this will improve with a reformed uri building:
// https://github.com/neos/flow-development-collection/pull/2744
$uriBuilder->setRequest(
ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())
);
}

$format = $this->getFormat();
if ($format !== null) {
Expand Down
Loading
Loading