Skip to content

Add generic Psr17Factory #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/installation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,10 @@ jobs:
- name: Check Install
run: |
tests/install.sh ${{ matrix.expect }} "${{ matrix.method }}" "${{ matrix.requirements }}"

- name: Run Tests
run: |
composer remove --dev --no-update php-http/httplug php-http/message-factory
composer require --dev ${{ matrix.requirements }}
vendor/bin/simple-phpunit
if: "matrix.expect == 'will-find' && matrix.method != 'Http\\Discovery\\HttpClientDiscovery::find();' && matrix.method != 'Http\\Discovery\\HttpAsyncClientDiscovery::find();' && matrix.pecl != 'psr-1.0.0, phalcon-4.0.6'"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## 1.15.0 - 2023-01-XX

- [#209](https://github.com/php-http/discovery/pull/209) - Add generic `Psr17Factory` class

## 1.14.3 - 2022-07-11

- [#207](https://github.com/php-http/discovery/pull/207) - Updates Exception to extend Throwable solving static analysis errors for consumers
Expand Down
282 changes: 282 additions & 0 deletions src/Psr17Factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
<?php

namespace Http\Discovery;

use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;

/**
* A generic PSR-17 implementation.
*
* You can create this class with concrete factory instances or let
* it use discovery to find suitable implementations as needed.
*
* This class also provides two additional methods that are not in PSR17,
* to help with creating PSR-7 objects from PHP superglobals:
* - createServerRequestFromGlobals()
* - createUriFromGlobals()
*
* The code in this class is inspired by the "nyholm/psr7", "guzzlehttp/psr7"
* and "symfony/http-foundation" packages, all licenced under MIT.
*
* Copyright (c) 2004-2023 Fabien Potencier <[email protected]>
* Copyright (c) 2015 Michael Dowling <[email protected]>
* Copyright (c) 2015 Márk Sági-Kazár <[email protected]>
* Copyright (c) 2015 Graham Campbell <[email protected]>
* Copyright (c) 2016 Tobias Schultze <[email protected]>
* Copyright (c) 2016 George Mponos <[email protected]>
* Copyright (c) 2016-2018 Tobias Nyholm <[email protected]>
*
* @author Nicolas Grekas <[email protected]>
*/
class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
{
private $requestFactory;
private $responseFactory;
private $serverRequestFactory;
private $streamFactory;
private $uploadedFileFactory;
private $uriFactory;

public function __construct(
RequestFactoryInterface $requestFactory = null,
ResponseFactoryInterface $responseFactory = null,
ServerRequestFactoryInterface $serverRequestFactory = null,
StreamFactoryInterface $streamFactory = null,
UploadedFileFactoryInterface $uploadedFileFactory = null,
UriFactoryInterface $uriFactory = null
) {
$this->requestFactory = $requestFactory;
$this->responseFactory = $responseFactory;
$this->serverRequestFactory = $serverRequestFactory;
$this->streamFactory = $streamFactory;
$this->uploadedFileFactory = $uploadedFileFactory;
$this->uriFactory = $uriFactory;

$this->setFactory($requestFactory);
$this->setFactory($responseFactory);
$this->setFactory($serverRequestFactory);
$this->setFactory($streamFactory);
$this->setFactory($uploadedFileFactory);
$this->setFactory($uriFactory);
}

/**
* @param UriInterface|string $uri
*/
public function createRequest(string $method, $uri): RequestInterface
{
$factory = $this->requestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findRequestFactory());

return $factory->createRequest(...\func_get_args());
}

public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
$factory = $this->responseFactory ?? $this->setFactory(Psr17FactoryDiscovery::findResponseFactory());

return $factory->createResponse(...\func_get_args());
}

/**
* @param UriInterface|string $uri
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
$factory = $this->serverRequestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findServerRequestFactory());

return $factory->createServerRequest(...\func_get_args());
}

public function createServerRequestFromGlobals(array $server = null, array $get = null, array $post = null, array $cookie = null, array $files = null, StreamInterface $body = null): ServerRequestInterface
{
$server = $server ?? $_SERVER;
$request = $this->createServerRequest($server['REQUEST_METHOD'] ?? 'GET', $this->createUriFromGlobals($server), $server);

return $this->buildServerRequestFromGlobals($request, $server, $files ?? $_FILES)
->withQueryParams($get ?? $_GET)
->withParsedBody($post ?? $_POST)
->withCookieParams($cookie ?? $_COOKIE)
->withBody($body ?? $this->createStreamFromFile('php://input', 'r+'));
}

public function createStream(string $content = ''): StreamInterface
{
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());

return $factory->createStream($content);
}

public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());

return $factory->createStreamFromFile($filename, $mode);
}

/**
* @param resource $resource
*/
public function createStreamFromResource($resource): StreamInterface
{
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());

return $factory->createStreamFromResource($resource);
}

public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
{
$factory = $this->uploadedFileFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUploadedFileFactory());

return $factory->createUploadedFile(...\func_get_args());
}

public function createUri(string $uri = ''): UriInterface
{
$factory = $this->uriFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUriFactory());

return $factory->createUri(...\func_get_args());
}

public function createUriFromGlobals(array $server = null): UriInterface
{
return $this->buildUriFromGlobals($this->createUri(''), $server ?? $_SERVER);
}

private function setFactory($factory)
{
if (!$this->requestFactory && $factory instanceof RequestFactoryInterface) {
$this->requestFactory = $factory;
}
if (!$this->responseFactory && $factory instanceof ResponseFactoryInterface) {
$this->responseFactory = $factory;
}
if (!$this->serverRequestFactory && $factory instanceof ServerRequestFactoryInterface) {
$this->serverRequestFactory = $factory;
}
if (!$this->streamFactory && $factory instanceof StreamFactoryInterface) {
$this->streamFactory = $factory;
}
if (!$this->uploadedFileFactory && $factory instanceof UploadedFileFactoryInterface) {
$this->uploadedFileFactory = $factory;
}
if (!$this->uriFactory && $factory instanceof UriFactoryInterface) {
$this->uriFactory = $factory;
}

return $factory;
}

private function buildServerRequestFromGlobals(ServerRequestInterface $request, array $server, array $files): ServerRequestInterface
{
$request = $request
->withProtocolVersion(isset($server['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1')
->withUploadedFiles($this->normalizeFiles($files));

$headers = [];
foreach ($server as $key => $value) {
if (0 === strpos($key, 'HTTP_')) {
$key = substr($key, 5);
} elseif (!\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
continue;
}
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));

$headers[$key] = $value;
}

if (!isset($headers['Authorization'])) {
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
$headers['Authorization'] = 'Basic '.base64_encode($_SERVER['PHP_AUTH_USER'].':'.($_SERVER['PHP_AUTH_PW'] ?? ''));
} elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
}
}

foreach ($headers as $key => $value) {
try {
$request = $request->withHeader($key, $value);
} catch (\InvalidArgumentException $e) {
// ignore invalid headers
}
}

return $request;
}

private function buildUriFromGlobals(UriInterface $uri, array $server): UriInterface
{
$uri = $uri->withScheme(!empty($server['HTTPS']) && 'off' !== strtolower($server['HTTPS']) ? 'https' : 'http');

$hasPort = false;
if (isset($server['HTTP_HOST'])) {
$parts = parse_url('http://'.$server['HTTP_HOST']);

$uri = $uri->withHost($parts['host'] ?? 'localhost');

if ($parts['port'] ?? false) {
$hasPort = true;
$uri = $uri->withPort($parts['port']);
}
} else {
$uri = $uri->withHost($server['SERVER_NAME'] ?? $server['SERVER_ADDR'] ?? 'localhost');
}

if (!$hasPort && isset($server['SERVER_PORT'])) {
$uri = $uri->withPort($server['SERVER_PORT']);
}

$hasQuery = false;
if (isset($server['REQUEST_URI'])) {
$requestUriParts = explode('?', $server['REQUEST_URI'], 2);
$uri = $uri->withPath($requestUriParts[0]);
if (isset($requestUriParts[1])) {
$hasQuery = true;
$uri = $uri->withQuery($requestUriParts[1]);
}
}

if (!$hasQuery && isset($server['QUERY_STRING'])) {
$uri = $uri->withQuery($server['QUERY_STRING']);
}

return $uri;
}

private function normalizeFiles(array $files): array
{
$normalized = [];

foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
} elseif (!\is_array($value)) {
continue;
} elseif (!isset($value['tmp_name'])) {
$normalized[$key] = $this->normalizeFiles($value);
} elseif (\is_array($value['tmp_name'])) {
foreach ($value['tmp_name'] as $k => $v) {
$file = $this->createStreamFromFile($value['tmp_name'][$k], 'r');
$normalized[$key][$k] = $this->createUploadedFile($file, $value['size'][$k], $value['error'][$k], $value['name'][$k], $value['type'][$k]);
}
} else {
$file = $this->createStreamFromFile($value['tmp_name'], 'r');
$normalized[$key] = $this->createUploadedFile($file, $value['size'], $value['error'], $value['name'], $value['type']);
}
}

return $normalized;
}
}
4 changes: 4 additions & 0 deletions tests/HttpClientDiscoveryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class HttpClientDiscoveryTest extends TestCase
{
public function testFind()
{
if (!interface_exists(HttpClient::class)) {
$this->markTestSkipped(HttpClient::class.' required.');
}

$client = HttpClientDiscovery::find();
$this->assertInstanceOf(HttpClient::class, $client);
}
Expand Down
4 changes: 4 additions & 0 deletions tests/Psr17FactoryDiscoveryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class Psr17FactoryDiscoveryTest extends TestCase
*/
public function testFind($method, $interface)
{
if (!interface_exists(RequestFactoryInterface::class)) {
$this->markTestSkipped(RequestFactoryInterface::class.' required.');
}

$callable = [Psr17FactoryDiscovery::class, $method];
$client = $callable();
$this->assertInstanceOf($interface, $client);
Expand Down
Loading