diff --git a/Neos.Flow/Classes/Error/AbstractExceptionHandler.php b/Neos.Flow/Classes/Error/AbstractExceptionHandler.php index f86f95fb49..1e753eb284 100644 --- a/Neos.Flow/Classes/Error/AbstractExceptionHandler.php +++ b/Neos.Flow/Classes/Error/AbstractExceptionHandler.php @@ -162,12 +162,14 @@ protected function buildView(\Throwable $exception, array $renderingOptions): Vi $request->setControllerPackageKey('Neos.Flow'); $uriBuilder = new UriBuilder(); $uriBuilder->setRequest($request); - $view->setControllerContext(new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - )); + if (method_exists($view, 'setControllerContext')) { + $view->setControllerContext(new ControllerContext( + $request, + new ActionResponse(), + new Arguments([]), + $uriBuilder + )); + } if (isset($renderingOptions['variables'])) { $view->assignMultiple($renderingOptions['variables']); diff --git a/Neos.Flow/Classes/Error/DebugExceptionHandler.php b/Neos.Flow/Classes/Error/DebugExceptionHandler.php index f64b7b4f0a..3718a0e289 100644 --- a/Neos.Flow/Classes/Error/DebugExceptionHandler.php +++ b/Neos.Flow/Classes/Error/DebugExceptionHandler.php @@ -15,6 +15,7 @@ use Neos\Flow\Core\Bootstrap; use Neos\Flow\Http\Helper\ResponseInformationHelper; use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Psr\Http\Message\ResponseInterface; /** * A basic but solid exception handler which catches everything which @@ -76,7 +77,15 @@ protected function echoExceptionWeb($exception) } try { - echo $this->buildView($exception, $this->renderingOptions)->render(); + $stream = $this->buildView($exception, $this->renderingOptions)->render(); + if ($stream instanceof ResponseInterface) { + /** + * The http status code will already be sent, and we are only currently interested in the content stream + * Thus, we unwrap the repose here: + */ + $stream = $stream->getBody(); + } + ResponseInformationHelper::sendStream($stream); } catch (\Throwable $throwable) { $this->renderStatically($statusCode, $throwable); } diff --git a/Neos.Flow/Classes/Error/ProductionExceptionHandler.php b/Neos.Flow/Classes/Error/ProductionExceptionHandler.php index 9a588de1e1..a637f90e0e 100644 --- a/Neos.Flow/Classes/Error/ProductionExceptionHandler.php +++ b/Neos.Flow/Classes/Error/ProductionExceptionHandler.php @@ -13,6 +13,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\Helper\ResponseInformationHelper; +use Psr\Http\Message\ResponseInterface; /** * A quite exception handler which catches but ignores any exception. @@ -39,7 +40,15 @@ protected function echoExceptionWeb($exception) try { if ($this->useCustomErrorView()) { try { - echo $this->buildView($exception, $this->renderingOptions)->render(); + $stream = $this->buildView($exception, $this->renderingOptions)->render(); + if ($stream instanceof ResponseInterface) { + /** + * The http status code will already be sent, and we are only currently interested in the content stream + * Thus, we unwrap the repose here: + */ + $stream = $stream->getBody(); + } + ResponseInformationHelper::sendStream($stream); } catch (\Throwable $throwable) { $this->renderStatically($statusCode, $throwable); } diff --git a/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php b/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php index 1858a2b88e..f120eba029 100644 --- a/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php +++ b/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php @@ -18,6 +18,7 @@ use Neos\Flow\Http\CacheControlDirectives; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * Helper to extract various information from PSR-7 responses. @@ -242,4 +243,15 @@ public static function makeStandardsCompliant(ResponseInterface $response, Reque return $response; } + + public static function sendStream(StreamInterface $stream): void + { + $body = $stream->detach() ?: $stream->getContents(); + if (is_resource($body)) { + fpassthru($body); + fclose($body); + } else { + echo $body; + } + } } diff --git a/Neos.Flow/Classes/Http/RequestHandler.php b/Neos.Flow/Classes/Http/RequestHandler.php index 2a65d6ad29..249780b287 100644 --- a/Neos.Flow/Classes/Http/RequestHandler.php +++ b/Neos.Flow/Classes/Http/RequestHandler.php @@ -171,12 +171,6 @@ protected function sendResponse(ResponseInterface $response) ob_end_flush(); } - $body = $response->getBody()->detach() ?: $response->getBody()->getContents(); - if (is_resource($body)) { - fpassthru($body); - fclose($body); - } else { - echo $body; - } + ResponseInformationHelper::sendStream($response->getBody()); } } diff --git a/Neos.Flow/Classes/Mvc/Controller/ActionController.php b/Neos.Flow/Classes/Mvc/Controller/ActionController.php index 7ebb5aa034..0e4323a325 100644 --- a/Neos.Flow/Classes/Mvc/Controller/ActionController.php +++ b/Neos.Flow/Classes/Mvc/Controller/ActionController.php @@ -246,7 +246,10 @@ public function processRequest(ActionRequest $request, ActionResponse $response) } if ($this->view !== null) { $this->view->assign('settings', $this->settings); - $this->view->setControllerContext($this->controllerContext); + $this->view->assign('request', $this->request); + if (method_exists($this->view, 'setControllerContext')) { + $this->view->setControllerContext($this->controllerContext); + } $this->initializeView($this->view); } @@ -820,14 +823,6 @@ protected function renderView() { $result = $this->view->render(); - if (is_string($result)) { - $this->response->setContent($result); - } - - if ($result instanceof ActionResponse) { - $result->mergeIntoParentResponse($this->response); - } - if ($result instanceof ResponseInterface) { $this->response->replaceHttpResponse($result); if ($result->hasHeader('Content-Type')) { @@ -835,10 +830,6 @@ protected function renderView() } } - if (is_object($result) && is_callable([$result, '__toString'])) { - $this->response->setContent((string)$result); - } - if ($result instanceof StreamInterface) { $this->response->setContent($result); } diff --git a/Neos.Flow/Classes/Mvc/View/AbstractView.php b/Neos.Flow/Classes/Mvc/View/AbstractView.php index 858d11ee70..ea85dfbd00 100644 --- a/Neos.Flow/Classes/Mvc/View/AbstractView.php +++ b/Neos.Flow/Classes/Mvc/View/AbstractView.php @@ -52,6 +52,8 @@ abstract class AbstractView implements ViewInterface /** * @var ControllerContext + * @deprecated if you absolutely need access to the current request please assign a variable. + * when using the action controller the request is directly available at "request" */ protected $controllerContext; @@ -59,9 +61,9 @@ abstract class AbstractView implements ViewInterface * Factory method to create an instance with given options. * * @param array $options - * @return ViewInterface + * @return static */ - public static function createWithOptions(array $options) + public static function createWithOptions(array $options): self { return new static($options); } @@ -141,10 +143,10 @@ public function setOption($optionName, $value) * * @param string $key Key of variable * @param mixed $value Value of object - * @return AbstractView an instance of $this, to enable chaining + * @return $this for chaining * @api */ - public function assign($key, $value) + public function assign(string $key, mixed $value): self { $this->variables[$key] = $value; return $this; @@ -154,10 +156,10 @@ public function assign($key, $value) * Add multiple variables to $this->variables. * * @param array $values array in the format array(key1 => value1, key2 => value2) - * @return AbstractView an instance of $this, to enable chaining + * @return $this for chaining * @api */ - public function assignMultiple(array $values) + public function assignMultiple(array $values): self { foreach ($values as $key => $value) { $this->assign($key, $value); @@ -168,26 +170,13 @@ public function assignMultiple(array $values) /** * Sets the current controller context * - * @param ControllerContext $controllerContext + * @deprecated if you absolutely need access to the current request please assign a variable. + * when using the action controller the request is directly available at "request" + * @param ControllerContext $controllerContext Context of the controller associated with this view * @return void - * @api */ public function setControllerContext(ControllerContext $controllerContext) { $this->controllerContext = $controllerContext; } - - /** - * Tells if the view implementation can render the view for the given context. - * - * By default we assume that the view implementation can handle all kinds of - * contexts. Override this method if that is not the case. - * - * @param ControllerContext $controllerContext - * @return boolean true if the view has something useful to display, otherwise false - */ - public function canRender(ControllerContext $controllerContext) - { - return true; - } } diff --git a/Neos.Flow/Classes/Mvc/View/JsonView.php b/Neos.Flow/Classes/Mvc/View/JsonView.php index 40844a72e2..6935c669cf 100644 --- a/Neos.Flow/Classes/Mvc/View/JsonView.php +++ b/Neos.Flow/Classes/Mvc/View/JsonView.php @@ -12,19 +12,23 @@ * source code. */ +use GuzzleHttp\Psr7\Response; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Persistence\PersistenceManagerInterface; +use Neos\Http\Factories\StreamFactoryTrait; use Neos\Utility\ObjectAccess; use Neos\Utility\TypeHandling; +use Psr\Http\Message\ResponseInterface; /** * A JSON view * - * @api + * @deprecated please use json_encode instead */ class JsonView extends AbstractView { + use StreamFactoryTrait; + /** * Supported options * @var array @@ -50,11 +54,6 @@ class JsonView extends AbstractView */ const EXPOSE_CLASSNAME_UNQUALIFIED = 2; - /** - * @var ControllerContext - */ - protected $controllerContext; - /** * Only variables whose name is contained in this array will be rendered * @@ -195,15 +194,17 @@ public function setConfiguration(array $configuration) * array represantion using a YAML view configuration and JSON encodes * the result. * - * @return string The JSON encoded variables + * @return ResponseInterface The JSON encoded variables * @api */ - public function render() + public function render(): ResponseInterface { - $this->controllerContext->getResponse()->setContentType('application/json'); + $response = new Response(); + $response = $response->withHeader('Content-Type', 'application/json'); $propertiesToRender = $this->renderArray(); $options = $this->getOption('jsonEncodingOptions'); - return json_encode($propertiesToRender, JSON_THROW_ON_ERROR | $options); + $value = json_encode($propertiesToRender, JSON_THROW_ON_ERROR | $options); + return $response->withBody($this->createStream($value)); } /** diff --git a/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php b/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php index 1bbe0c104f..5454cea8e8 100644 --- a/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php +++ b/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php @@ -11,7 +11,9 @@ * source code. */ +use Neos\Http\Factories\StreamFactoryTrait; use Neos\Utility\ObjectAccess; +use Psr\Http\Message\StreamInterface; /** * An abstract View @@ -20,6 +22,8 @@ */ class SimpleTemplateView extends AbstractView { + use StreamFactoryTrait; + /** * @var array */ @@ -31,10 +35,9 @@ class SimpleTemplateView extends AbstractView /** * Renders the view * - * @return string The rendered view * @api */ - public function render() + public function render(): StreamInterface { $source = $this->getOption('templateSource'); $templatePathAndFilename = $this->getOption('templatePathAndFilename'); @@ -42,8 +45,10 @@ public function render() $source = file_get_contents($templatePathAndFilename); } - return preg_replace_callback('/\{([a-zA-Z0-9\-_.]+)\}/', function ($matches) { + $content = preg_replace_callback('/\{([a-zA-Z0-9\-_.]+)\}/', function ($matches) { return ObjectAccess::getPropertyPath($this->variables, $matches[1]); }, $source); + + return $this->createStream($content); } } diff --git a/Neos.Flow/Classes/Mvc/View/ViewInterface.php b/Neos.Flow/Classes/Mvc/View/ViewInterface.php index 45f544256a..838b401ace 100644 --- a/Neos.Flow/Classes/Mvc/View/ViewInterface.php +++ b/Neos.Flow/Classes/Mvc/View/ViewInterface.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Controller\ControllerContext; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; @@ -26,53 +25,45 @@ interface ViewInterface /** * Sets the current controller context * + * @deprecated if you absolutely need access to the current request please assign a variable. + * when using the action controller the request is directly available at "request" * @param ControllerContext $controllerContext Context of the controller associated with this view * @return void - * @api */ - public function setControllerContext(ControllerContext $controllerContext); + // public function setControllerContext(ControllerContext $controllerContext): void; /** * Add a variable to the view data collection. - * Can be chained, so $this->view->assign(..., ...)->assign(..., ...); is possible + * Can be chained: $this->view->assign(..., ...)->assign(..., ...); * * @param string $key Key of variable * @param mixed $value Value of object - * @return ViewInterface an instance of $this, to enable chaining + * @return $this for chaining * @api */ - public function assign($key, $value); + public function assign(string $key, mixed $value): self; /** * Add multiple variables to the view data collection * - * @param array $values array in the format array(key1 => value1, key2 => value2) - * @return ViewInterface an instance of $this, to enable chaining + * @param array $values associative array with the key being its name + * @return $this for chaining * @api */ - public function assignMultiple(array $values); - - /** - * Tells if the view implementation can render the view for the given context. - * - * @param ControllerContext $controllerContext - * @return boolean true if the view has something useful to display, otherwise false - */ - public function canRender(ControllerContext $controllerContext); + public function assignMultiple(array $values): self; /** * Renders the view * - * @return string|ActionResponse|ResponseInterface|StreamInterface|object The rendered result; object is only handled if __toString() exists! * @api */ - public function render(); + public function render(): ResponseInterface|StreamInterface; /** * Factory method to create an instance with given options. * - * @param array $options - * @return ViewInterface + * @param array $options + * @return static */ - public static function createWithOptions(array $options); + public static function createWithOptions(array $options): self; } diff --git a/Neos.Flow/Tests/Functional/Mvc/ViewsConfiguration/Fixtures/TemplateView.php b/Neos.Flow/Tests/Functional/Mvc/ViewsConfiguration/Fixtures/TemplateView.php index 3fa5e5b4ef..a5c26b44c1 100644 --- a/Neos.Flow/Tests/Functional/Mvc/ViewsConfiguration/Fixtures/TemplateView.php +++ b/Neos.Flow/Tests/Functional/Mvc/ViewsConfiguration/Fixtures/TemplateView.php @@ -10,8 +10,9 @@ * information, please view the LICENSE file which was distributed with this * source code. */ -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\AbstractView; +use Neos\Http\Factories\StreamFactoryTrait; +use Psr\Http\Message\StreamInterface; /** * An empty view - a special case. @@ -19,6 +20,8 @@ */ final class TemplateView extends AbstractView { + use StreamFactoryTrait; + /** * @var array */ @@ -39,17 +42,6 @@ final class TemplateView extends AbstractView 'layoutPathAndFilename' => [null, 'Path and filename of the layout file. If set, overrides the layoutPathAndFilenamePattern', 'string'], ]; - /** - * Dummy method to satisfy the ViewInterface - * - * @param ControllerContext $controllerContext - * @return void - * @api - */ - public function setControllerContext(ControllerContext $controllerContext) - { - } - /** * Dummy method to satisfy the ViewInterface * @@ -58,7 +50,7 @@ public function setControllerContext(ControllerContext $controllerContext) * @return self instance of $this to allow chaining * @api */ - public function assign($key, $value) + public function assign(string $key, mixed $value): self { return $this; } @@ -70,31 +62,19 @@ public function assign($key, $value) * @return self instance of $this to allow chaining * @api */ - public function assignMultiple(array $values) + public function assignMultiple(array $values): self { return $this; } - /** - * This view can be used in any case. - * - * @param ControllerContext $controllerContext - * @return boolean true - * @api - */ - public function canRender(ControllerContext $controllerContext) - { - return true; - } - /** * Renders the empty view * * @return string An empty string */ - public function render() + public function render(): StreamInterface { - return get_class($this); + return $this->createStream(get_class($this)); } /** diff --git a/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php b/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php index 523ece5c81..e8b7c58465 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php @@ -201,45 +201,14 @@ public function processRequestThrowsExceptionIfRequestedActionIsNotPublic() /** * @test */ - public function processRequestInjectsControllerContextToView() - { - $this->actionController = $this->getAccessibleMock(ActionController::class, ['resolveActionMethodName', 'initializeActionMethodArguments', 'initializeActionMethodValidators', 'resolveView', 'callActionMethod', 'initializeController']); - $this->actionController->method('resolveActionMethodName')->willReturn('indexAction'); - - $this->inject($this->actionController, 'objectManager', $this->mockObjectManager); - $this->inject($this->actionController, 'controllerContext', $this->mockControllerContext); - $this->inject($this->actionController, 'request', $this->mockRequest); - - $this->inject($this->actionController, 'arguments', new Arguments([])); - - $mockMvcPropertyMappingConfigurationService = $this->createMock(Mvc\Controller\MvcPropertyMappingConfigurationService::class); - $this->inject($this->actionController, 'mvcPropertyMappingConfigurationService', $mockMvcPropertyMappingConfigurationService); - - $mockHttpRequest = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $this->mockRequest->expects(self::any())->method('getHttpRequest')->will(self::returnValue($mockHttpRequest)); - - $mockResponse = new Mvc\ActionResponse; - $mockResponse->setContentType('text/plain'); - $this->inject($this->actionController, 'response', $mockResponse); - - $mockView = $this->createMock(Mvc\View\ViewInterface::class); - $mockView->expects(self::once())->method('setControllerContext')->with($this->mockControllerContext); - $this->actionController->expects(self::once())->method('resolveView')->will(self::returnValue($mockView)); - $this->actionController->expects(self::once())->method('resolveActionMethodName')->will(self::returnValue('someAction')); - - $this->actionController->processRequest($this->mockRequest, $mockResponse); - } - - /** - * @test - */ - public function processRequestInjectsSettingsToView() + public function processRequestInjectsSettingsAndRequestToView() { $this->actionController = $this->getAccessibleMock(ActionController::class, ['resolveActionMethodName', 'initializeActionMethodArguments', 'initializeActionMethodValidators', 'resolveView', 'callActionMethod']); $this->actionController->method('resolveActionMethodName')->willReturn('indexAction'); $this->inject($this->actionController, 'objectManager', $this->mockObjectManager); $this->inject($this->actionController, 'controllerContext', $this->mockControllerContext); + $this->inject($this->actionController, 'request', $this->mockRequest); $mockSettings = ['foo', 'bar']; $this->inject($this->actionController, 'settings', $mockSettings); @@ -253,7 +222,7 @@ public function processRequestInjectsSettingsToView() $mockResponse = new Mvc\ActionResponse; $mockView = $this->createMock(Mvc\View\ViewInterface::class); - $mockView->expects(self::once())->method('assign')->with('settings', $mockSettings); + $mockView->expects(self::exactly(2))->method('assign')->withConsecutive(['settings', $mockSettings], ['request', $this->mockRequest]); $this->actionController->expects(self::once())->method('resolveView')->will(self::returnValue($mockView)); $this->actionController->expects(self::once())->method('resolveActionMethodName')->will(self::returnValue('someAction')); diff --git a/Neos.Flow/Tests/Unit/Mvc/View/JsonViewTest.php b/Neos.Flow/Tests/Unit/Mvc/View/JsonViewTest.php index e2d74af3ba..f187479c8d 100644 --- a/Neos.Flow/Tests/Unit/Mvc/View/JsonViewTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/View/JsonViewTest.php @@ -267,7 +267,7 @@ public function renderSetsContentTypeHeader() { $this->response->expects(self::once())->method('setHeader')->with('Content-Type', 'application/json'); - $this->view->render(); + $this->view->render()->getBody()->getContents(); } /** @@ -280,7 +280,7 @@ public function renderReturnsJsonRepresentationOfAssignedObject() $this->view->assign('value', $object); $expectedResult = '{"foo":"Foo"}'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -293,7 +293,7 @@ public function renderReturnsJsonRepresentationOfAssignedArray() $this->view->assign('value', $array); $expectedResult = '{"foo":"Foo","bar":"Bar"}'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -306,7 +306,7 @@ public function renderReturnsJsonRepresentationOfAssignedSimpleValue() $this->view->assign('value', $value); $expectedResult = '"Foo"'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -319,7 +319,7 @@ public function renderReturnsNullIfNameOfAssignedVariableIsNotEqualToValue() $this->view->assign('foo', $value); $expectedResult = 'null'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -333,7 +333,7 @@ public function renderOnlyRendersVariableWithTheNameValue() ->assign('someOtherVariable', 'Foo'); $expectedResult = '"Value"'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -347,7 +347,7 @@ public function setVariablesToRenderOverridesValueToRender() $this->view->setVariablesToRender(['foo']); $expectedResult = '"Foo"'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -363,7 +363,7 @@ public function renderRendersMultipleValuesIfTheyAreSpecifiedAsVariablesToRender $this->view->setVariablesToRender(['value', 'secondValue']); $expectedResult = '{"value":"Value1","secondValue":"Value2"}'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -383,7 +383,7 @@ public function renderCanRenderMultipleComplexObjects() $this->view->setVariablesToRender(['array', 'object']); $expectedResult = '{"array":{"foo":{"bar":"Baz"}},"object":{"foo":"Foo"}}'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -404,7 +404,7 @@ public function renderCanRenderPlainArray() ]); $expectedResult = '[{"name":"Foo"},{"name":"Bar"}]'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -425,7 +425,7 @@ public function descendAllKeepsArrayIndexes() ]); $expectedResult = '[{"name":"Foo","secret":true},{"name":"Bar","secret":true}]'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -445,7 +445,7 @@ public function renderTransformsJsonSerializableValues() ]); $expectedResult = '{"name":"Foo"}'; - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); } @@ -462,7 +462,7 @@ public function viewAcceptsJsonEncodingOptions() $expectedResult = json_encode($array, JSON_PRETTY_PRINT); - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); self::assertEquals($expectedResult, $actualResult); $unexpectedResult = json_encode($array); @@ -482,7 +482,7 @@ public function viewObeysDateTimeFormatOption() $expectedResult = json_encode(['foo' => '2021-05-02 13:00:00 GMT+0000']); - $actualResult = $this->view->render(); + $actualResult = $this->view->render()->getBody()->getContents(); $this->assertEquals($expectedResult, $actualResult); } } diff --git a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php index 7c2a3219eb..bb92d96905 100644 --- a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php +++ b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php @@ -16,6 +16,8 @@ use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\ViewInterface; use Neos\FluidAdaptor\Core\Rendering\RenderingContext; +use Neos\Http\Factories\StreamFactoryTrait; +use Psr\Http\Message\StreamInterface; /** * The abstract base of all Fluid views. @@ -23,6 +25,8 @@ */ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTemplateView implements ViewInterface { + use StreamFactoryTrait; + /** * This contains the supported options, their default values, descriptions and types. * Syntax example: @@ -106,13 +110,33 @@ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTempl */ protected $controllerContext; + /** + * @phpstan-ignore-next-line we are incompatible with the fluid view and should use composition instead + */ + public function render($actionName = null): StreamInterface + { + return $this->createStream(parent::render($actionName)); + } + + public function assign($key, $value): self + { + // layer to fix incompatibility error with typo3 fluid interface + return parent::assign($key, $value); + } + + public function assignMultiple(array $values): self + { + // layer to fix incompatibility error with typo3 fluid interface + return parent::assignMultiple($values); + } + /** * Factory method to create an instance with given options. * * @param array $options - * @return AbstractTemplateView + * @return static */ - public static function createWithOptions(array $options) + public static function createWithOptions(array $options): self { return new static($options); } @@ -177,15 +201,6 @@ public function setControllerContext(ControllerContext $controllerContext) $this->baseRenderingContext->setControllerAction($request->getControllerActionName()); } - /** - * @param ControllerContext $controllerContext - * @return boolean - */ - public function canRender(ControllerContext $controllerContext) - { - return true; - } - /** * Renders a given section. * diff --git a/Neos.FluidAdaptor/Classes/View/StandaloneView.php b/Neos.FluidAdaptor/Classes/View/StandaloneView.php index e738464e68..34252beb07 100644 --- a/Neos.FluidAdaptor/Classes/View/StandaloneView.php +++ b/Neos.FluidAdaptor/Classes/View/StandaloneView.php @@ -65,9 +65,9 @@ class StandaloneView extends AbstractTemplateView * Factory method to create an instance with given options. * * @param array $options - * @return StandaloneView + * @return static */ - public static function createWithOptions(array $options) + public static function createWithOptions(array $options): self { return new static(null, $options); } diff --git a/Neos.FluidAdaptor/Tests/Functional/Core/Fixtures/ViewHelpers/Controller/View/CustomView.php b/Neos.FluidAdaptor/Tests/Functional/Core/Fixtures/ViewHelpers/Controller/View/CustomView.php index 9ec175bb04..a963e9aee6 100644 --- a/Neos.FluidAdaptor/Tests/Functional/Core/Fixtures/ViewHelpers/Controller/View/CustomView.php +++ b/Neos.FluidAdaptor/Tests/Functional/Core/Fixtures/ViewHelpers/Controller/View/CustomView.php @@ -13,10 +13,11 @@ use GuzzleHttp\Psr7\Response; use Neos\Flow\Mvc\View\AbstractView; +use Psr\Http\Message\ResponseInterface; class CustomView extends AbstractView { - public function render() + public function render(): ResponseInterface { return new Response(418, ['X-Flow-Special-Header' => 'YEAH!'], 'Hello World!'); } diff --git a/Neos.FluidAdaptor/Tests/Functional/View/StandaloneViewTest.php b/Neos.FluidAdaptor/Tests/Functional/View/StandaloneViewTest.php index 7f3a7fb781..ff4e32dace 100644 --- a/Neos.FluidAdaptor/Tests/Functional/View/StandaloneViewTest.php +++ b/Neos.FluidAdaptor/Tests/Functional/View/StandaloneViewTest.php @@ -58,7 +58,7 @@ public function inlineTemplateIsEvaluatedCorrectly(): void $standaloneView->setTemplateSource('This is my cool {foo} template!'); $expected = 'This is my cool bar template!'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -89,7 +89,7 @@ public function renderThrowsExceptionIfNeitherTemplateSourceNorTemplatePathAndFi $actionRequest = ActionRequest::fromHttpRequest($httpRequest); $standaloneView = new StandaloneView($actionRequest, $this->standaloneViewNonce); - $standaloneView->render(); + $standaloneView->render()->getContents(); } /** @@ -103,7 +103,7 @@ public function renderThrowsExceptionSpecifiedTemplatePathAndFilenameDoesNotExis $standaloneView = new StandaloneView($actionRequest, $this->standaloneViewNonce); $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/NonExistingTemplate.txt'); - $standaloneView->render(); + $standaloneView->render()->getContents(); } /** @@ -117,7 +117,7 @@ public function renderThrowsExceptionIfWrongEnctypeIsSetForFormUpload(): void $standaloneView = new StandaloneView($actionRequest, $this->standaloneViewNonce); $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplateWithFormUpload.txt'); - $standaloneView->render(); + $standaloneView->render()->getContents(); } /** @@ -131,7 +131,7 @@ public function renderThrowsExceptionIfSpecifiedTemplatePathAndFilenamePointsToA $standaloneView = new StandaloneView($actionRequest, $this->standaloneViewNonce); $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures'); - $standaloneView->render(); + $standaloneView->render()->getContents(); } /** @@ -148,7 +148,7 @@ public function templatePathAndFilenameIsLoaded(): void $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplate.txt'); $expected = 'This is a test template. Hello Robert.'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -162,7 +162,7 @@ public function variablesAreEscapedByDefault(): void $standaloneView->setTemplateSource('Hello {name}.'); $expected = 'Hello Sebastian <script>alert("dangerous");</script>.'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -176,7 +176,7 @@ public function variablesAreNotEscapedIfEscapingIsDisabled(): void $standaloneView->setTemplateSource('{escapingEnabled=false}Hello {name}.'); $expected = 'Hello Sebastian .'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -192,7 +192,7 @@ public function variablesCanBeNested() $standaloneView->setTemplateSource('{config.{type}.value.{flavor}}'); $expected = 'Okayish'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); $this->assertSame($expected, $actual); } @@ -209,7 +209,7 @@ public function partialWithDefaultLocationIsUsedIfNoPartialPathIsSetExplicitly() $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplateWithPartial.txt'); $expected = 'This is a test template. Hello Robert.'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -227,7 +227,7 @@ public function explicitPartialPathIsUsed(): void $standaloneView->setPartialRootPath(__DIR__ . '/Fixtures/SpecialPartialsDirectory'); $expected = 'This is a test template. Hello Karsten.'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -244,7 +244,7 @@ public function layoutWithDefaultLocationIsUsedIfNoLayoutPathIsSetExplicitly(): $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplateWithLayout.txt'); $expected = 'Hey HEY HO'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -261,7 +261,7 @@ public function explicitLayoutPathIsUsed(): void $standaloneView->setLayoutRootPath(__DIR__ . '/Fixtures/SpecialLayouts'); $expected = 'Hey -- overridden -- HEY HO'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -278,7 +278,7 @@ public function viewThrowsExceptionWhenUnknownViewHelperIsCalled(): void $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplateWithUnknownViewHelper.txt'); $standaloneView->setLayoutRootPath(__DIR__ . '/Fixtures/SpecialLayouts'); - $standaloneView->render(); + $standaloneView->render()->getContents(); } /** @@ -294,7 +294,7 @@ public function xmlNamespacesCanBeIgnored(): void $standaloneView->setLayoutRootPath(__DIR__ . '/Fixtures/SpecialLayouts'); $expected = 'foobar'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } @@ -372,7 +372,7 @@ public function formViewHelpersOutsideOfFormWork(): void $standaloneView->setTemplatePathAndFilename(__DIR__ . '/Fixtures/TestTemplateWithFormField.txt'); $expected = 'This is a test template.'; - $actual = $standaloneView->render(); + $actual = $standaloneView->render()->getContents(); self::assertSame($expected, $actual); } } diff --git a/Neos.FluidAdaptor/Tests/Unit/View/AbstractTemplateViewTest.php b/Neos.FluidAdaptor/Tests/Unit/View/AbstractTemplateViewTest.php index 3e6e66715b..5e61f82073 100644 --- a/Neos.FluidAdaptor/Tests/Unit/View/AbstractTemplateViewTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/View/AbstractTemplateViewTest.php @@ -52,7 +52,7 @@ protected function setUp(): void $this->renderingContext = $this->getMockBuilder(RenderingContext::class)->setMethods(['getViewHelperVariableContainer', 'getVariableProvider'])->disableOriginalConstructor()->getMock(); $this->renderingContext->expects(self::any())->method('getViewHelperVariableContainer')->will(self::returnValue($this->viewHelperVariableContainer)); $this->renderingContext->expects(self::any())->method('getVariableProvider')->will(self::returnValue($this->templateVariableContainer)); - $this->view = $this->getMockBuilder(AbstractTemplateView::class)->setMethods(['getTemplateSource', 'getLayoutSource', 'getPartialSource', 'canRender', 'getTemplateIdentifier', 'getLayoutIdentifier', 'getPartialIdentifier'])->getMock(); + $this->view = $this->getMockBuilder(AbstractTemplateView::class)->setMethods(['getTemplateSource', 'getLayoutSource', 'getPartialSource', 'getTemplateIdentifier', 'getLayoutIdentifier', 'getPartialIdentifier'])->getMock(); $this->view->setRenderingContext($this->renderingContext); } diff --git a/Neos.Kickstarter/Classes/Service/GeneratorService.php b/Neos.Kickstarter/Classes/Service/GeneratorService.php index 240b97cd95..e872b5f181 100644 --- a/Neos.Kickstarter/Classes/Service/GeneratorService.php +++ b/Neos.Kickstarter/Classes/Service/GeneratorService.php @@ -583,7 +583,7 @@ protected function renderTemplate($templatePathAndFilename, array $contextVariab $standaloneView = new StandaloneView(); $standaloneView->setTemplatePathAndFilename($templatePathAndFilename); $standaloneView->assignMultiple($contextVariables); - return $standaloneView->render(); + return $standaloneView->render()->getContents(); } /**