Commit 8fde6812 authored by Christian Kuhn's avatar Christian Kuhn Committed by Benni Mack
Browse files

[FEATURE] Extbase Request implements ServerRequestInterface

The patch changes the extbase Mvc/Request to implement
PSR-7 ServerRequestInterface: The former extbase request
details like 'controllerAction' are changed to an
PSR-7 attribute and the Request class is now a
decorator - it receives the original PSR-7 request
as constructor argument, then implements both the
extbase RequestInterface plus PSR-7 ServerRequestInterface.

This way, the Request object itself does not hold
state (except the original request), but channels
all get* and with* calls to the PSR-7 request object.
This avoids creating a new standalone object and moving
all data from the PSR-7 object into the new object.

The patch is relatively conservative. The extbase
related set* methods are kept even though they're
all @internal and violate immutability, and various
method signatures are not adapted towards strict
typing, yet. The reason for that are various
chain- and loop dependencies especially in fluid view
that should be solved with single patches before extbase
Request can be streamlined further. Current usages also
rely on fallback layers within __construct(), which can
be solved when consuming places are adapted - The patch
would have become much bigger if that would be mixed in.

Change-Id: I218de0ee30d16245e7d562d0aba2795ccc439901
Resolves: #94428
Releases: master
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69545

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 6449b06b
......@@ -254,3 +254,8 @@ parameters:
message: "#^Parameter \\#1 \\$result of method TYPO3\\\\CMS\\\\Backend\\\\Controller\\\\File\\\\FileController\\:\\:flattenResultDataValue\\(\\) expects bool\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile given\\.$#"
count: 1
path: typo3/sysext/backend/Classes/Controller/File/FileController.php
-
# Obsolete in v12, when either entire Request or __construct() are declared final
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 23
path: typo3/sysext/extbase/Classes/Mvc/Request.php
.. include:: ../../Includes.txt
===================================================================
Feature: #94428 - Extbase Request implements ServerRequestInterface
===================================================================
See :issue:`94428`
Description
===========
The extbase :php:`TYPO3\CMS\Extbase\Mvc\Request` now implements
the PSR-7 :php:`ServerRequestInterface` and thus holds all request
related information of the main core request in addition to the
plugin namespace specific extbase arguments.
Impact
======
This allows getting information of the main request especially within
extbase controllers from :php:`$this->request`.
Developers of fluid view helpers can now retrieve the main PSR-7 request
in many contexts from :php:`$renderingContext->getRequest()`, in addition
to the extbase specific information specified by
:php:`TYPO3\CMS\Extbase\Mvc\Request\RequestInterface`.
Note that with future patches, the request assigned to view helper
:php:`RenderingContext` may NOT implement extbase
:php:`TYPO3\CMS\Extbase\Mvc\Request\RequestInterface` anymore, and
only PSR-7 :php:`ServerRequestInterface`. This will be the case when the
view helper is not called from within an extbase plugin, but when fluid
is started as "standalone view" in non-extbase based plugins: Often in
backend scenarios like toolbars, doc headers, non-extbase modules, etc.
Extensions should thus test for instance of extbase :php:`RequestInterface`
if they don't know the context and rely on extbase specific request data.
.. index:: PHP-API, ext:extbase
<?php
/*
* 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\Extbase\Mvc;
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
/**
* Extbase request related state.
* Attached as 'extbase' attribute to PSR-7 ServerRequestInterface.
*
* @internal Set up extbase internally, use TYPO3\CMS\Extbase\Mvc\Request instead.
*/
class ExtbaseRequestParameters
{
/**
* @var string Key of the plugin which identifies the plugin. It must be a string containing [a-z0-9]
*/
protected $pluginName = '';
/**
* @var string Name of the extension which is supposed to handle this request. This is the extension name converted to UpperCamelCase
*
* @todo: Should probably at least init to empty string.
*/
protected $controllerExtensionName;
/**
* @var string
*
* @todo: Should probably at least init to empty string.
*/
protected $controllerObjectName;
/**
* @var string Object name of the controller which is supposed to handle this request.
*/
protected $controllerName = 'Standard';
/**
* @var string Name of the action the controller is supposed to take.
*/
protected $controllerActionName = 'index';
/**
* @var array The arguments for this request
*/
protected $arguments = [];
/**
* Framework-internal arguments for this request, such as __referrer.
* All framework-internal arguments start with double underscore (__),
* and are only used from within the framework. Not for user consumption.
* Internal Arguments can be objects, in contrast to public arguments
*
* @var array
*/
protected array $internalArguments = [];
/**
* @var string The requested representation format
*/
protected $format = 'html';
/**
* @var bool If this request has been changed and needs to be dispatched again
* @deprecated since v11, will be removed in v12.
*/
protected $dispatched = false;
/**
* If this request is a forward because of an error, the original request gets filled.
*
* @var \TYPO3\CMS\Extbase\Mvc\Request|null
*/
protected $originalRequest;
/**
* If the request is a forward because of an error, these mapping results get filled here.
*
* @var \TYPO3\CMS\Extbase\Error\Result|null
*/
protected $originalRequestMappingResults;
/**
* Sets the dispatched flag
*
* @param bool $flag If this request has been dispatched
* @deprecated since v11, will be removed in v12.
*/
public function setDispatched($flag)
{
$this->dispatched = (bool)$flag;
}
/**
* If this request has been dispatched and addressed by the responsible
* controller and the response is ready to be sent.
*
* The dispatcher will try to dispatch the request again if it has not been
* addressed yet.
*
* @return bool TRUE if this request has been dispatched successfully
* @deprecated since v11, will be removed in v12.
*/
public function isDispatched()
{
return $this->dispatched;
}
/**
* @param string $controllerClassName
*/
public function __construct(string $controllerClassName = '')
{
$this->controllerObjectName = $controllerClassName;
}
/**
* @return string
*/
public function getControllerObjectName(): string
{
return $this->controllerObjectName;
}
/**
* Explicitly sets the object name of the controller
*
* @param string $controllerObjectName The fully qualified controller object name
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setControllerObjectName($controllerObjectName)
{
$nameParts = ClassNamingUtility::explodeObjectControllerName($controllerObjectName);
$this->controllerExtensionName = $nameParts['extensionName'];
$this->controllerName = $nameParts['controllerName'];
return $this;
}
/**
* Sets the plugin name.
*
* @param string|null $pluginName
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setPluginName($pluginName = null)
{
if ($pluginName !== null) {
$this->pluginName = $pluginName;
}
return $this;
}
/**
* Returns the plugin key.
*
* @return string The plugin key
*/
public function getPluginName()
{
return $this->pluginName;
}
/**
* Sets the extension name of the controller.
*
* @param string $controllerExtensionName The extension name.
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException if the extension name is not valid
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setControllerExtensionName($controllerExtensionName): self
{
if ($controllerExtensionName !== null) {
$this->controllerExtensionName = $controllerExtensionName;
}
return $this;
}
/**
* Returns the extension name of the specified controller.
*
* @return string The extension name
*/
public function getControllerExtensionName()
{
return $this->controllerExtensionName;
}
/**
* Returns the extension name of the specified controller.
*
* @return string The extension key
*/
public function getControllerExtensionKey()
{
return GeneralUtility::camelCaseToLowerCaseUnderscored($this->controllerExtensionName);
}
/**
* @var array
*/
protected $controllerAliasToClassNameMapping = [];
/**
* @param array $controllerAliasToClassNameMapping
*/
public function setControllerAliasToClassNameMapping(array $controllerAliasToClassNameMapping)
{
// this is only needed as long as forwarded requests are altered and unless there
// is no new request object created by the request builder.
$this->controllerAliasToClassNameMapping = $controllerAliasToClassNameMapping;
return $this;
}
/**
* Sets the name of the controller which is supposed to handle the request.
* Note: This is not the object name of the controller!
*
* @param string $controllerName Name of the controller
* @throws Exception\InvalidControllerNameException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setControllerName($controllerName): self
{
if (!is_string($controllerName) && $controllerName !== null) {
throw new InvalidControllerNameException('The controller name must be a valid string, ' . gettype($controllerName) . ' given.', 1187176358);
}
if ($controllerName !== null) {
$this->controllerName = $controllerName;
$this->controllerObjectName = $this->controllerAliasToClassNameMapping[$controllerName] ?? '';
// There might be no Controller Class, for example for Fluid Templates.
}
return $this;
}
/**
* Returns the object name of the controller supposed to handle this request, if one
* was set already (if not, the name of the default controller is returned)
*
* @return string Object name of the controller
*/
public function getControllerName()
{
return $this->controllerName;
}
/**
* Sets the name of the action contained in this request.
*
* Note that the action name must start with a lower case letter and is case sensitive.
*
* @param string $actionName Name of the action to execute by the controller
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException if the action name is not valid
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setControllerActionName($actionName): self
{
if (!is_string($actionName) && $actionName !== null) {
throw new InvalidActionNameException('The action name must be a valid string, ' . gettype($actionName) . ' given (' . $actionName . ').', 1187176359);
}
if ($actionName[0] !== strtolower($actionName[0]) && $actionName !== null) {
throw new InvalidActionNameException('The action name must start with a lower case letter, "' . $actionName . '" does not match this criteria.', 1218473352);
}
if ($actionName !== null) {
$this->controllerActionName = $actionName;
}
return $this;
}
/**
* Returns the name of the action the controller is supposed to execute.
*
* @return string Action name
*/
public function getControllerActionName(): string
{
$controllerObjectName = $this->getControllerObjectName();
if ($controllerObjectName !== '' && $this->controllerActionName === strtolower($this->controllerActionName)) {
// todo: this is nonsense! We can detect a non existing method in
// todo: \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin, if necessary.
// todo: At this point, we want to have a getter for a fixed value.
$actionMethodName = $this->controllerActionName . 'Action';
$classMethods = get_class_methods($controllerObjectName);
if (is_array($classMethods)) {
foreach ($classMethods as $existingMethodName) {
if (strtolower($existingMethodName) === strtolower($actionMethodName)) {
$this->controllerActionName = substr($existingMethodName, 0, -6);
break;
}
}
}
}
return $this->controllerActionName;
}
/**
* Sets the value of the specified argument
*
* @param string $argumentName Name of the argument to set
* @param mixed $value The new value
* @throws InvalidArgumentNameException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setArgument(string $argumentName, $value): self
{
if ($argumentName === '') {
throw new InvalidArgumentNameException('Invalid argument name.', 1210858767);
}
if ($argumentName[0] === '_' && $argumentName[1] === '_') {
$this->internalArguments[$argumentName] = $value;
return $this;
}
if (!in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)) {
$this->arguments[$argumentName] = $value;
}
return $this;
}
/**
* Sets the whole arguments array and therefore replaces any arguments
* which existed before.
*
* @param array $arguments An array of argument names and their values
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setArguments(array $arguments): self
{
$this->arguments = [];
foreach ($arguments as $argumentName => $argumentValue) {
$this->setArgument($argumentName, $argumentValue);
}
return $this;
}
/**
* Returns an array of arguments and their values
*
* @return array Associative array of arguments and their values (which may be arguments and values as well)
*/
public function getArguments()
{
return $this->arguments;
}
/**
* Returns the value of the specified argument
*
* @param string $argumentName Name of the argument
*
* @return string|array Value of the argument
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException if such an argument does not exist
*/
public function getArgument($argumentName)
{
if (!isset($this->arguments[$argumentName])) {
throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist for this request.', 1176558158);
}
return $this->arguments[$argumentName];
}
/**
* Checks if an argument of the given name exists (is set)
*
* @param string $argumentName Name of the argument to check
*
* @return bool TRUE if the argument is set, otherwise FALSE
*/
public function hasArgument($argumentName)
{
return isset($this->arguments[$argumentName]);
}
/**
* Sets the requested representation format
*
* @param string $format The desired format, something like "html", "xml", "png", "json" or the like. Can even be something like "rss.xml".
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setFormat(string $format): self
{
$this->format = $format;
return $this;
}
/**
* Returns the requested representation format
*
* @return string The desired format, something like "html", "xml", "png", "json" or the like.
*/
public function getFormat()
{
return $this->format;
}
/**
* Returns the original request. Filled only if a property mapping error occurred.
*
* @return \TYPO3\CMS\Extbase\Mvc\Request|null the original request.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getOriginalRequest(): ?Request
{
return $this->originalRequest;
}
/**
* @param \TYPO3\CMS\Extbase\Mvc\Request $originalRequest
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setOriginalRequest(\TYPO3\CMS\Extbase\Mvc\Request $originalRequest)
{
$this->originalRequest = $originalRequest;
}
/**
* Get the request mapping results for the original request.
*
* @return \TYPO3\CMS\Extbase\Error\Result
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getOriginalRequestMappingResults(): Result
{
if ($this->originalRequestMappingResults === null) {
return new Result();
}
return $this->originalRequestMappingResults;
}
/**
* @param \TYPO3\CMS\Extbase\Error\Result $originalRequestMappingResults
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function setOriginalRequestMappingResults(Result $originalRequestMappingResults)
{
$this->originalRequestMappingResults = $originalRequestMappingResults;
}
/**
* Get the internal arguments of the request, i.e. every argument starting
* with two underscores.
*
* @return array
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getInternalArguments(): array
{
return $this->internalArguments;
}
/**
* Returns the value of the specified argument
*
* @param string $argumentName Name of the argument
* @return string|null Value of the argument, or NULL if not set.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getInternalArgument($argumentName)
{
if (!isset($this->internalArguments[$argumentName])) {
return null;
}
return $this->internalArguments[$argumentName];
}
}
......@@ -15,77 +15,156 @@
namespace TYPO3\CMS\Extbase\Mvc;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
/**
* Contract for a request.
* Contract for an extbase request.
*/
interface RequestInterface
{
/**
* Sets the dispatched flag
* Return the original PSR-7 request.
*
* @param bool $flag If this request has been dispatched
* @deprecated since v11, will be removed in v12.
* @return ServerRequestInterface
* @todo v12: Enable
*/
// public function getServerRequest(): ServerRequestInterface;
/**
* Returns the plugin key.
* @todo v12: Enable
*/
public function setDispatched($flag);
// public function getPluginName(): string;
/**
* If this request has been dispatched and addressed by the responsible
* controller and the response is ready to be sent.
* Return an instance with the specified plugin name set.
*
* The dispatcher will try to dispatch the request again if it has not been
* addressed yet.
* @param string|null Plugin name
* @return self
* @todo v12: Enable
*/
// public function withPluginName($pluginName = null): self;
/**
* Returns the extension name of the specified controller.
*
* @return bool TRUE if this request has been dispatched successfully
* @deprecated since v11, will be removed in v12.
* @return string|null
* @todo v12: Enable
*/
public function isDispatched();
// public function getControllerExtensionName(): ?string;
/**
* Returns the object name of the controller defined by the package key and
* controller name
* Return an instance with the specified controller extension name set.
*
* @param string|null Extension name
* @return self