Deprecation: #92784 - Extbase controller actions must return ResponseInterface

See forge#92784

Description

Until now, Extbase controller actions could return either nothing (void), null, a string, or an object that implements __toString().

From now on Extbase expects actions to return an instance of Psr\Http\Message\ResponseInterface.

Impact

All actions that do not return an instance of Psr\Http\Message\ResponseInterface trigger a PHP E_USER_DEPRECATED error and will fail as of TYPO3 v12.

Affected Installations

All installations that use Extbase controller actions which don't return an instance of Psr\Http\Message\ResponseInterface.

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/), the PSR-17 factories should be used. Both the $responseFactory as well as the $streamFactory are available in all extbase controllers. The $responseFactory can be used to create a blank response object whose content and headers can be set freely. The content can therfore be set using the $streamFactory.

Example:

public function listAction(): ResponseInterface
{
    $items = $this->itemRepository->findAll();
    $this->view->assign('items', $items);

    return $this->responseFactory->createResponse()
        ->withAddedHeader('Content-Type', 'text/html; charset=utf-8')
        ->withBody($this->streamFactory->createStream($this->view->render()));
}

This example only shows the most common use case. It causes html with a Content-Type: text/html header and http code 200 Ok returned as the response to the client.

Tip

Using the factory is a clean architectural solution but it's a lot of new code when migration from returning nothing at all. To ease the migration path, method htmlResponse(string $html = null) has been introduced which allows for a quite small change. When called without an argument, said method renders the current view.

public function listAction(): ResponseInterface
{
    $items = $this->itemRepository->findAll();
    $this->view->assign('items', $items);

    return $this->htmlResponse();
}

Of course you are free to adjust this response object before returning it.

Example:

public function listAction(): ResponseInterface
{
    $items = $this->itemRepository->findAll();
    $this->view->assign('items', $items);

    return $this->responseFactory
        ->createResponse()
        ->withHeader('Cache-Control', 'must-revalidate')
        ->withHeader('Content-Type', 'text/html; charset=utf-8')
        ->withStatus(200, 'Super ok!')
        ->withBody($this->streamFactory->createStream($this->view->render()));
}

Tip

To adjust the content of an already created PSR-7 response object, $response->getBody()->write() can be used.

Tip

Since Extbase uses PSR-7 responses, you should make yourself familiar with its API. Documentation and more information regarding PSR-7 responses can be found here: https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface

In case you are using the JsonView in your extbase controller, you may want to ease the migration path with the new jsonResponse(string $json = null) method. Similar to htmlResponse(), this method creates a PSR-7 Response with the Content-Type: application/json header and http code 200 Ok. If argument $json is omitted, the current view is rendered automatically.

Example:

public function listApiAction(): ResponseInterface
{
    $items = $this->itemRepository->findAll();
    $this->view->assign('value', [
         'items' => $items
     ]);

    return $this->jsonResponse();
}

Above example is equivalent to:

public function listApiAction(): ResponseInterface
{
    $items = $this->itemRepository->findAll();
    $this->view->assign('value', [
         'items' => $items
     ]);

    return $this->responseFactory
        ->createResponse()
        ->withHeader('Content-Type', 'application/json; charset=utf-8')
        ->withBody($this->streamFactory->createStream($this->view->render()));
}