Commit 71884c8f authored by Benjamin Franzke's avatar Benjamin Franzke

[BUGFIX] Use PSR-17 interfaces in Extbase

In order to strengthen TYPO3's focus on PSR standards, this change
uses PSR-17 interfaces instead of the custom ResponseFactoryInterface
which was added solely for extbase in #92784.

The interface was added as part of the #92784 deprecation, but it
actually contradicts with the ideas of interchangable PSR interfaces
and therefore we strive for native PSR-17 usage, instead of wrapping
PSR interfaces, now.
The Extbase ActionController::htmlResponse() method – which was
suggested to be used by #92784 – is kept as is (functionality wise [1]),
and since the interface was injected into the ActionController using a
final method, the impact of this switch is very low.

Concrete implementations of PSR interfaces are always internal api,
threfore also TYPO3\CMS\Core\Http\Response is switched back to be
marked as internal API.

Furthermore TYPO3\CMS\Core\Http\JsonResponse properties do not need to
be marked internal, as the entire class is internal.

[1] ActionController::htmlResponse() is adapted to avoid rewinding()
    the response body, as every usage/respond is actually expected
    to rewind or use toString(), and therefore rewind() would be
    called twice. Only functional tests where buggy in not calling
    rewind() during test assertion.

Releases: master
Resolves: #93237
Related: #92784
Change-Id: I59e5a190eaa1f0dd62f08db34987c6d4a72b73c1
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67353Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent 60022de9
......@@ -20,7 +20,7 @@ namespace TYPO3\CMS\Core\Http;
*
* Highly inspired by ZF zend-diactoros
*
* @internal Note that this is not public API yet.
* @internal Note that this is not public API, use PSR-17 interfaces instead
*/
class JsonResponse extends Response
{
......@@ -32,7 +32,6 @@ class JsonResponse extends Response
* </code>
*
* @var int
* @internal use {@see \TYPO3\CMS\Core\Http\ResponseFactory::JSON_FLAGS_RFC4627} instead
*/
const DEFAULT_JSON_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES;
......@@ -52,7 +51,6 @@ class JsonResponse extends Response
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @param int $encodingOptions JSON encoding options to use.
* @internal use {@see \TYPO3\CMS\Core\Http\ResponseFactory::createJsonResponseFromData} instead
*/
public function __construct(
$data = [],
......@@ -80,7 +78,6 @@ class JsonResponse extends Response
* @param array $data
* @param int $encodingOptions
* @return $this
* @internal use {@see \TYPO3\CMS\Core\Http\ResponseFactory::createJsonResponseFromData} instead
*/
public function setPayload(array $data = [], $encodingOptions = self::DEFAULT_JSON_FLAGS): JsonResponse
{
......
......@@ -23,6 +23,8 @@ use TYPO3\CMS\Core\Utility\MathUtility;
* Default implementation for the ResponseInterface of the PSR-7 standard.
*
* Highly inspired by https://github.com/phly/http/
*
* @internal Note that this is not public API, use PSR-17 interfaces instead
*/
class Response extends Message implements ResponseInterface
{
......
......@@ -17,22 +17,14 @@ declare(strict_types=1);
namespace TYPO3\CMS\Core\Http;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* @internal Note that this is not public API, use PSR-17 interfaces instead.
*/
class ResponseFactory implements ResponseFactoryInterface
{
/**
* Default flags for json_encode; value of:
*
* <code>
* JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
* </code>
*
* @var int
*/
public const JSON_FLAGS_RFC4627 = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES;
/**
* Create a new response.
*
......@@ -44,53 +36,4 @@ class ResponseFactory implements ResponseFactoryInterface
{
return new Response(null, $code, [], $reasonPhrase);
}
public function createHtmlResponse(string $html): ResponseInterface
{
$response = $this->createResponse();
$response->withAddedHeader('Content-Type', 'text/html; charset=utf-8');
$stream = new Stream('php://temp', 'wb+');
$stream->write($html);
$stream->rewind();
return $response->withBody($stream);
}
public function createJsonResponse(string $json): ResponseInterface
{
$response = $this->createResponse();
$response->withAddedHeader('Content-Type', 'application/json; charset=utf-8');
$stream = new Stream('php://temp', 'wb+');
$stream->write($json);
$stream->rewind();
return $response->withBody($stream);
}
/**
* Create a JSON response with the given data.
*
* Default JSON encoding is performed with the following options, which
* produces RFC4627-compliant JSON, capable of embedding into HTML.
*
* - {@see JSON_HEX_TAG}
* - {@see JSON_HEX_APOS}
* - {@see JSON_HEX_AMP}
* - {@see JSON_HEX_QUOT}
* - {@see JSON_UNESCAPED_SLASHES}
*
* @param mixed $data
* @param int $encodingOptions
* @return ResponseInterface
* @throws \JsonException
*/
public function createJsonResponseFromData($data, int $encodingOptions = self::JSON_FLAGS_RFC4627): ResponseInterface
{
return $this->createJsonResponse((string)json_encode($data, $encodingOptions | JSON_THROW_ON_ERROR));
}
public function createRedirectResponse(UriInterface $uri, int $code = 303): ResponseInterface
{
return $this->createResponse($code)->withAddedHeader('location', (string)$uri);
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
interface ResponseFactoryInterface extends \Psr\Http\Message\ResponseFactoryInterface
{
public function createHtmlResponse(string $html): ResponseInterface;
public function createJsonResponse(string $json): ResponseInterface;
public function createRedirectResponse(UriInterface $uri, int $code = 303): ResponseInterface;
}
......@@ -200,9 +200,6 @@ services:
Psr\Http\Message\ResponseFactoryInterface:
alias: TYPO3\CMS\Core\Http\ResponseFactory
public: true
TYPO3\CMS\Core\Http\ResponseFactoryInterface:
alias: TYPO3\CMS\Core\Http\ResponseFactory
public: true
Psr\Http\Message\ServerRequestFactoryInterface:
alias: TYPO3\CMS\Core\Http\ServerRequestFactory
public: true
......
......@@ -29,8 +29,9 @@ All installations that use Extbase controller actions which don't return an inst
Migration
=========
Since the core follows not only PSR-7 (https://www.php-fig.org/psr/psr-7/) but also PSR-17 (https://www.php-fig.org/psr/psr-17/),
which defines how response factories should look like, the core provides a factory to create various different response types.
Since the core follows not only PSR-7 (https://www.php-fig.org/psr/psr-7/)
but also PSR-17 (https://www.php-fig.org/psr/psr-17/),
the PSR-17 response factory should be used.
The response factory is available in all extbase controllers and can be used as a shorthand function to create responses for html and json,
the two most used content types. The factory can also be used to create a blank response object whose content and headers can be set freely.
......@@ -43,7 +44,10 @@ Example:
$items = $this->itemRepository->findAll();
$this->view->assign('items', $items);
return $this->responseFactory->createHtmlResponse($this->view->render());
$response = $this->responseFactory->createResponse()
->withAddedHeader('Content-Type', 'text/html; charset=utf-8');
$response->getBody()->write($this->view->render());
return $response;
}
This example only shows the most common use case. It causes html with a :html:`Content-Type: text/html` header and
......@@ -77,11 +81,13 @@ Example:
$items = $this->itemRepository->findAll();
$this->view->assign('items', $items);
return $this->responseFactory
->createHtmlResponse($this->view->render())
$response = $this->responseFactory
->createResponse()
->withHeader('Cache-Control', 'must-revalidate')
->withStatus(200, 'Super ok!')
;
->withHeader('Content-Type', 'text/html; charset=utf-8')
->withStatus(200, 'Super ok!');
$response->getBody()->write($this->view->render());
return $response;
}
.. tip::
......
......@@ -16,10 +16,10 @@
namespace TYPO3\CMS\Extbase\Mvc\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Http\ResponseFactoryInterface;
use TYPO3\CMS\Core\Http\Stream;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Messaging\FlashMessage;
......@@ -1141,8 +1141,9 @@ abstract class ActionController implements ControllerInterface
*/
protected function htmlResponse(string $html = null): ResponseInterface
{
return $this->responseFactory->createHtmlResponse(
$html ?? $this->view->render()
);
$response = $this->responseFactory->createResponse()
->withHeader('Content-Type', 'text/html; charset=utf-8');
$response->getBody()->write($html ?? $this->view->render());
return $response;
}
}
......@@ -77,7 +77,10 @@ class ContentController extends ActionController
]]);
$this->view->assign('value', $value);
return $this->responseFactory->createJsonResponse($this->view->render());
$response = $this->responseFactory->createResponse()
->withAddedHeader('Content-Type', 'application/json; charset=utf-8');
$response->getBody()->write($this->view->render());
return $response;
}
/**
......
......@@ -115,6 +115,7 @@ class ControllerArgumentsMappingTest extends FunctionalTestCase
$response = $this->controller->processRequest($this->request);
$response->getBody()->rewind();
self::assertEquals($expectedTitle, $response->getBody()->getContents());
}
}
......@@ -113,6 +113,7 @@ class ActionControllerValidationTest extends FunctionalTestCase
foreach ($titleErrors as $titleError) {
self::assertContains($titleError->getCode(), $expectedErrorCodes);
}
$response->getBody()->rewind();
self::assertEquals('testFormAction', $response->getBody()->getContents());
}
......@@ -178,6 +179,7 @@ class ActionControllerValidationTest extends FunctionalTestCase
}
}
$response->getBody()->rewind();
self::assertEquals('testFormAction', $response->getBody()->getContents());
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment