Commit 7b5612f5 authored by Benjamin Franzke's avatar Benjamin Franzke Committed by Frank Nägler
Browse files

[FEATURE] Provide implementation for PSR-17 HTTP Message Factories

Support for PSR-17 HTTP Message Factories has been added.

PSR-17 HTTP Factories are intended to be used by PSR-15 request handlers
in order to create PSR-7 compatible message objects.

Classes may use dependency injection to use any of the available PSR-17
HTTP Factory interfaces.

The Request/Response base class (Message) is adapted to be able to lazily
initialize a stream when getBody() is called.
This is done as the PSR (Stream)RequestFactoryInterface does not allow
to control Stream properties. Therefore it is a performance
optimization to defer initialization. It is likely, that a new
Stream will be added to a Request with withStream() anyway.
(Which would mean resources for the intermediate stream would have
been wasted)

Furthermore some DocBlocks are adapted to reflect the variadic
UriInterface/StreamInterface parameters that are already handled in
code but were not documented. These cases are needed/required
by the PSR-17 factory implementation now.

composer require psr/http-factory:^1.0

Releases: master
Resolves: #89018
Change-Id: Ie6b9d865679bbf6f5d3d030b0ed1a3f277c47a3d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61558


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Frank Nägler's avatarFrank Nägler <frank.naegler@typo3.org>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Frank Nägler's avatarFrank Nägler <frank.naegler@typo3.org>
parent e0fd4700
......@@ -47,6 +47,7 @@
"phpdocumentor/reflection-docblock": "^4.3",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "~1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "~1.0.0",
......@@ -93,6 +94,10 @@
"symfony/routing": "4.2.7",
"phpdocumentor/reflection-docblock": ">= 4.3.2"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"extra": {
"typo3/class-alias-loader": {
"class-alias-maps": [
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6309d4f7acafe489cc5ed464bd5b6373",
"content-hash": "504d732795cef3c4078aa9f9587ff3dd",
"packages": [
{
"name": "cogpowered/finediff",
......@@ -1050,6 +1050,58 @@
],
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"time": "2019-04-30T12:38:16+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
......@@ -6335,8 +6387,8 @@
"authors": [
{
"name": "Arne Blankerts",
"email": "arne@blankerts.de",
"role": "Developer"
"role": "Developer",
"email": "arne@blankerts.de"
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
......
......@@ -285,6 +285,9 @@ class Message implements MessageInterface
*/
public function getBody()
{
if ($this->body === null) {
$this->body = new Stream('php://temp', 'r+');
}
return $this->body;
}
......
......@@ -22,4 +22,8 @@ namespace TYPO3\CMS\Core\Http;
*/
class NullResponse extends Response
{
public function __construct()
{
parent::__construct(null);
}
}
......@@ -77,9 +77,9 @@ class Request extends Message implements RequestInterface
/**
* Constructor, the only place to set all parameters of this Request
*
* @param string|null $uri URI for the request, if any.
* @param string|UriInterface|null $uri URI for the request, if any.
* @param string|null $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param string|resource|StreamInterface|null $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @throws \InvalidArgumentException for any invalid value.
*/
......@@ -87,11 +87,11 @@ class Request extends Message implements RequestInterface
{
// Build a streamable object for the body
if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
if ($body !== null && !is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271);
}
if (!$body instanceof StreamInterface) {
if ($body !== null && !$body instanceof StreamInterface) {
$body = new Stream($body);
}
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
......@@ -17,17 +18,32 @@ namespace TYPO3\CMS\Core\Http;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class RequestFactory to create Request objects
* Returns PSR-7 Request objects (currently the Guzzle implementation).
* Returns PSR-7 Request objects
*/
class RequestFactory
class RequestFactory implements RequestFactoryInterface
{
/**
* Create a request object with our custom implementation
* Create a new request.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request.
* @return RequestInterface
*/
public function createRequest(string $method, $uri): RequestInterface
{
return new Request($uri, $method, null);
}
/**
* Create a guzzle request object with our custom implementation
*
* @param string $uri the URI to request
* @param string $method the HTTP method (defaults to GET)
......
......@@ -120,16 +120,17 @@ class Response extends Message implements ResponseInterface
* @param StreamInterface|string $body
* @param int $statusCode
* @param array $headers
* @param string $reasonPhrase
* @throws \InvalidArgumentException if any of the given arguments are given
*/
public function __construct($body = 'php://temp', $statusCode = 200, $headers = [])
public function __construct($body = 'php://temp', $statusCode = 200, $headers = [], string $reasonPhrase = '')
{
// Build a streamable object for the body
if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
if ($body !== null && !is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717277);
}
if (!$body instanceof StreamInterface) {
if ($body !== null && !$body instanceof StreamInterface) {
$body = new Stream($body, 'rw');
}
$this->body = $body;
......@@ -139,7 +140,7 @@ class Response extends Message implements ResponseInterface
}
$this->statusCode = (int)$statusCode;
$this->reasonPhrase = $this->availableStatusCodes[$this->statusCode];
$this->reasonPhrase = $reasonPhrase === '' ? $this->availableStatusCodes[$this->statusCode] : $reasonPhrase;
list($this->lowercasedHeaderNames, $headers) = $this->filterHeaders($headers);
$this->assertHeaders($headers);
$this->headers = $headers;
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
* 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!
*/
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @internal Note that this is not public API, use PSR-17 interfaces instead.
*/
class ResponseFactory implements ResponseFactoryInterface
{
/**
* Create a new response.
*
* @param int $code HTTP status code; defaults to 200
* @param string $reasonPhrase Reason phrase to associate with status code
* @return ResponseInterface
*/
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
return new Response(null, $code, [], $reasonPhrase);
}
}
......@@ -66,9 +66,9 @@ class ServerRequest extends Request implements ServerRequestInterface
/**
* Constructor, the only place to set all parameters of this Message/Request
*
* @param string|null $uri URI for the request, if any.
* @param string|UriInterface|null $uri URI for the request, if any.
* @param string|null $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param string|resource|StreamInterface|null $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @param array $serverParams Server parameters, typically from $_SERVER
* @param array $uploadedFiles Upload file information, a tree of UploadedFiles
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
......@@ -14,6 +15,8 @@ namespace TYPO3\CMS\Core\Http;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -24,8 +27,25 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*
* @internal Note that this is not public API yet.
*/
class ServerRequestFactory
class ServerRequestFactory implements ServerRequestFactoryInterface
{
/**
* Create a new server request.
*
* Note that server-params are taken precisely as given - no parsing/processing
* of the given values is performed, and, in particular, no attempt is made to
* determine the HTTP method or URI, which must be provided explicitly.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request.
* @param array $serverParams Array of SAPI parameters with which to seed the generated request instance.
* @return ServerRequestInterface
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
return new ServerRequest($uri, $method, null, [], $serverParams);
}
/**
* Create a request from the original superglobal variables.
*
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
* 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!
*/
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal Note that this is not public API, use PSR-17 interfaces instead.
*/
class StreamFactory implements StreamFactoryInterface
{
/**
* Create a new stream from a string.
*
* @param string $content String content with which to populate the stream.
* @return StreamInterface
*/
public function createStream(string $content = ''): StreamInterface
{
$stream = new Stream('php://temp', 'r+');
if ($content !== '') {
$stream->write($content);
}
return $stream;
}
/**
* Create a stream from an existing file.
*
* The `$filename` MAY be any string supported by `fopen()`.
*
* @param string $filename Filename or stream URI to use as basis of stream.
* @param string $mode Mode with which to open the underlying filename/stream.
* @return StreamInterface
* @throws \RuntimeException If the file cannot be opened.
* @throws \InvalidArgumentException If the mode is invalid.
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
$resource = @fopen($filename, $mode);
if ($resource === false) {
if ($mode === '' || in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true) === false) {
throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.', 1566823434);
}
throw new \RuntimeException('The file ' . $filename . ' cannot be opened.', 1566823435);
}
return new Stream($resource);
}
/**
* Create a new stream from an existing resource.
*
* The stream MUST be readable and may be writable.
*
* @param resource $resource PHP resource to use as basis of stream.
* @return StreamInterface
* @throws \InvalidArgumentException
*/
public function createStreamFromResource($resource): StreamInterface
{
if (!is_resource($resource) || get_resource_type($resource) !== 'stream') {
throw new \InvalidArgumentException('Invalid stream provided; must be a stream resource', 1566853697);
}
return new Stream($resource);
}
}
......@@ -67,7 +67,7 @@ class UploadedFile implements UploadedFileInterface
/**
* Constructor method
*
* @param string|resource $input is either a stream or a filename
* @param string|resource|StreamInterface $input is either a stream or a filename
* @param int $size see $_FILES['size'] from PHP
* @param int $errorStatus see $_FILES['error']
* @param string $clientFilename the original filename handed over from the client
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
* 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!
*/
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* @internal Note that this is not public API, use PSR-17 interfaces instead.
*/
class UploadedFileFactory implements UploadedFileFactoryInterface
{
/**
* Create a new uploaded file.
*
* If a size is not provided it will be determined by checking the size of
* the file.
*
* @see http://php.net/manual/features.file-upload.post-method.php
* @see http://php.net/manual/features.file-upload.errors.php
*
* @param StreamInterface $stream Underlying stream representing the uploaded file content.
* @param int $size in bytes
* @param int $error PHP file upload error
* @param string $clientFilename Filename as provided by the client, if any.
* @param string $clientMediaType Media type as provided by the client, if any.
* @return UploadedFileInterface
* @throws \InvalidArgumentException If the file resource is not readable.
*/
public function createUploadedFile(
StreamInterface $stream,
int $size = null,
int $error = \UPLOAD_ERR_OK,
string $clientFilename = null,
string $clientMediaType = null
): UploadedFileInterface {
if ($size === null) {
$size = $stream->getSize();
if ($size === null) {
throw new \InvalidArgumentException('Stream size could not be determined.', 1566823423);
}
}
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Http;
/*
* 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!
*/
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
/**
* @internal Note that this is not public API, use PSR-17 interfaces instead.
*/
class UriFactory implements UriFactoryInterface
{
/**
* Create a new URI.
*
* @param string $uri
* @return UriInterface
* @throws \InvalidArgumentException If the given URI cannot be parsed.
*/
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
}
......@@ -7,10 +7,6 @@ services:
TYPO3\CMS\Core\:
resource: '../Classes/*'
Psr\EventDispatcher\EventDispatcherInterface:
alias: TYPO3\CMS\Core\EventDispatcher\EventDispatcher
public: true
TYPO3\CMS\Core\DependencyInjection\EnvVarProcessor:
tags: ['container.env_var_processor']
......@@ -76,3 +72,26 @@ services:
class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
arguments: ['l10n']
# Interface implementations
Psr\EventDispatcher\EventDispatcherInterface:
alias: TYPO3\CMS\Core\EventDispatcher\EventDispatcher
public: true
Psr\Http\Message\RequestFactoryInterface:
alias: TYPO3\CMS\Core\Http\RequestFactory
public: true
Psr\Http\Message\ResponseFactoryInterface:
alias: TYPO3\CMS\Core\Http\ResponseFactory
public: true
Psr\Http\Message\ServerRequestFactoryInterface:
alias: TYPO3\CMS\Core\Http\ServerRequestFactory
public: true
Psr\Http\Message\StreamFactoryInterface:
alias: TYPO3\CMS\Core\Http\StreamFactory
public: true
Psr\Http\Message\UploadedFileFactoryInterface:
alias: TYPO3\CMS\Core\Http\UploadedFileFactory
public: true
Psr\Http\Message\UriFactoryInterface:
alias: TYPO3\CMS\Core\Http\UriFactory
public: true
.. include:: ../../Includes.txt
==========================================================================
Feature: #89018 - Provide implementation for PSR-17 HTTP Message Factories
==========================================================================
See :issue:`89018`
Description
===========
Support for PSR-17_ HTTP Message Factories has been added.
PSR-17 HTTP Factories are intended to be used by PSR-15_ request handlers in order to create PSR-7_
compatible message objects.
PSR-17 consists of six factory interfaces:
- :php:`Psr\Http\Message\RequestFactoryInterface:`
- :php:`Psr\Http\Message\ResponseFactoryInterface:`
- :php:`Psr\Http\Message\ServerRequestFactoryInterface:`
- :php:`Psr\Http\Message\StreamFactoryInterface:`
- :php:`Psr\Http\Message\UploadedFileFactoryInterface:`
- :php:`Psr\Http\Message\UriFactoryInterface:`
Request handlers shall use dependency injection to use any of the available PSR-17 HTTP Factory interfaces.
Impact
======
PSR-17 HTTP Fatory interfaces are provided by `psr/http-factory` and should be used as
dependencies for PSR-15 request handlers or services that need to create PSR-7 message objects.
It is discouraged to explicitly create PSR-7 instances of classes from the :php:`TYPO3\CMS\Core\Http`
namespace (they are not public API). Use type declarations against PSR-17 HTTP Message Factory interfaces
and dependency injection instead.
Example usage
-------------
A middleware that needs to send a JSON response when a certain condition is met, uses the
PSR-17 response factory interface (the concrete TYPO3 implementation is injected as constructor
dependency) to create a new PSR-7 response object:
.. code-block:: php
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class StatusCheckMiddleware implements MiddlewareInterface
{
/** @var ResponseFactoryInterface */
private $responseFactory;
public function __construct(ResponseFactoryInterface $responseFactory)
{
$this->responseFactory = $responseFactory;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getRequestTarget() === '/check') {
$data = ['status' => 'ok'];