Skip to content

Commit c1a1061

Browse files
authored
Merge pull request #209 from nicolas-grekas/global-request
Add generic Psr17Factory
2 parents b3130e9 + 6193f9d commit c1a1061

7 files changed

+630
-0
lines changed

.github/workflows/installation.yml

+7
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,10 @@ jobs:
9292
- name: Check Install
9393
run: |
9494
tests/install.sh ${{ matrix.expect }} "${{ matrix.method }}" "${{ matrix.requirements }}"
95+
96+
- name: Run Tests
97+
run: |
98+
composer remove --dev --no-update php-http/httplug php-http/message-factory
99+
composer require --dev ${{ matrix.requirements }}
100+
vendor/bin/simple-phpunit
101+
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'"

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## 1.15.0 - 2023-01-XX
4+
5+
- [#209](https://github.com/php-http/discovery/pull/209) - Add generic `Psr17Factory` class
6+
37
## 1.14.3 - 2022-07-11
48

59
- [#207](https://github.com/php-http/discovery/pull/207) - Updates Exception to extend Throwable solving static analysis errors for consumers

src/Psr17Factory.php

+282
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
3+
namespace Http\Discovery;
4+
5+
use Psr\Http\Message\RequestFactoryInterface;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ResponseFactoryInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestFactoryInterface;
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Psr\Http\Message\StreamFactoryInterface;
12+
use Psr\Http\Message\StreamInterface;
13+
use Psr\Http\Message\UploadedFileFactoryInterface;
14+
use Psr\Http\Message\UploadedFileInterface;
15+
use Psr\Http\Message\UriFactoryInterface;
16+
use Psr\Http\Message\UriInterface;
17+
18+
/**
19+
* A generic PSR-17 implementation.
20+
*
21+
* You can create this class with concrete factory instances or let
22+
* it use discovery to find suitable implementations as needed.
23+
*
24+
* This class also provides two additional methods that are not in PSR17,
25+
* to help with creating PSR-7 objects from PHP superglobals:
26+
* - createServerRequestFromGlobals()
27+
* - createUriFromGlobals()
28+
*
29+
* The code in this class is inspired by the "nyholm/psr7", "guzzlehttp/psr7"
30+
* and "symfony/http-foundation" packages, all licenced under MIT.
31+
*
32+
* Copyright (c) 2004-2023 Fabien Potencier <[email protected]>
33+
* Copyright (c) 2015 Michael Dowling <[email protected]>
34+
* Copyright (c) 2015 Márk Sági-Kazár <[email protected]>
35+
* Copyright (c) 2015 Graham Campbell <[email protected]>
36+
* Copyright (c) 2016 Tobias Schultze <[email protected]>
37+
* Copyright (c) 2016 George Mponos <[email protected]>
38+
* Copyright (c) 2016-2018 Tobias Nyholm <[email protected]>
39+
*
40+
* @author Nicolas Grekas <[email protected]>
41+
*/
42+
class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
43+
{
44+
private $requestFactory;
45+
private $responseFactory;
46+
private $serverRequestFactory;
47+
private $streamFactory;
48+
private $uploadedFileFactory;
49+
private $uriFactory;
50+
51+
public function __construct(
52+
RequestFactoryInterface $requestFactory = null,
53+
ResponseFactoryInterface $responseFactory = null,
54+
ServerRequestFactoryInterface $serverRequestFactory = null,
55+
StreamFactoryInterface $streamFactory = null,
56+
UploadedFileFactoryInterface $uploadedFileFactory = null,
57+
UriFactoryInterface $uriFactory = null
58+
) {
59+
$this->requestFactory = $requestFactory;
60+
$this->responseFactory = $responseFactory;
61+
$this->serverRequestFactory = $serverRequestFactory;
62+
$this->streamFactory = $streamFactory;
63+
$this->uploadedFileFactory = $uploadedFileFactory;
64+
$this->uriFactory = $uriFactory;
65+
66+
$this->setFactory($requestFactory);
67+
$this->setFactory($responseFactory);
68+
$this->setFactory($serverRequestFactory);
69+
$this->setFactory($streamFactory);
70+
$this->setFactory($uploadedFileFactory);
71+
$this->setFactory($uriFactory);
72+
}
73+
74+
/**
75+
* @param UriInterface|string $uri
76+
*/
77+
public function createRequest(string $method, $uri): RequestInterface
78+
{
79+
$factory = $this->requestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findRequestFactory());
80+
81+
return $factory->createRequest(...\func_get_args());
82+
}
83+
84+
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
85+
{
86+
$factory = $this->responseFactory ?? $this->setFactory(Psr17FactoryDiscovery::findResponseFactory());
87+
88+
return $factory->createResponse(...\func_get_args());
89+
}
90+
91+
/**
92+
* @param UriInterface|string $uri
93+
*/
94+
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
95+
{
96+
$factory = $this->serverRequestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findServerRequestFactory());
97+
98+
return $factory->createServerRequest(...\func_get_args());
99+
}
100+
101+
public function createServerRequestFromGlobals(array $server = null, array $get = null, array $post = null, array $cookie = null, array $files = null, StreamInterface $body = null): ServerRequestInterface
102+
{
103+
$server = $server ?? $_SERVER;
104+
$request = $this->createServerRequest($server['REQUEST_METHOD'] ?? 'GET', $this->createUriFromGlobals($server), $server);
105+
106+
return $this->buildServerRequestFromGlobals($request, $server, $files ?? $_FILES)
107+
->withQueryParams($get ?? $_GET)
108+
->withParsedBody($post ?? $_POST)
109+
->withCookieParams($cookie ?? $_COOKIE)
110+
->withBody($body ?? $this->createStreamFromFile('php://input', 'r+'));
111+
}
112+
113+
public function createStream(string $content = ''): StreamInterface
114+
{
115+
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
116+
117+
return $factory->createStream($content);
118+
}
119+
120+
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
121+
{
122+
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
123+
124+
return $factory->createStreamFromFile($filename, $mode);
125+
}
126+
127+
/**
128+
* @param resource $resource
129+
*/
130+
public function createStreamFromResource($resource): StreamInterface
131+
{
132+
$factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
133+
134+
return $factory->createStreamFromResource($resource);
135+
}
136+
137+
public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
138+
{
139+
$factory = $this->uploadedFileFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUploadedFileFactory());
140+
141+
return $factory->createUploadedFile(...\func_get_args());
142+
}
143+
144+
public function createUri(string $uri = ''): UriInterface
145+
{
146+
$factory = $this->uriFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUriFactory());
147+
148+
return $factory->createUri(...\func_get_args());
149+
}
150+
151+
public function createUriFromGlobals(array $server = null): UriInterface
152+
{
153+
return $this->buildUriFromGlobals($this->createUri(''), $server ?? $_SERVER);
154+
}
155+
156+
private function setFactory($factory)
157+
{
158+
if (!$this->requestFactory && $factory instanceof RequestFactoryInterface) {
159+
$this->requestFactory = $factory;
160+
}
161+
if (!$this->responseFactory && $factory instanceof ResponseFactoryInterface) {
162+
$this->responseFactory = $factory;
163+
}
164+
if (!$this->serverRequestFactory && $factory instanceof ServerRequestFactoryInterface) {
165+
$this->serverRequestFactory = $factory;
166+
}
167+
if (!$this->streamFactory && $factory instanceof StreamFactoryInterface) {
168+
$this->streamFactory = $factory;
169+
}
170+
if (!$this->uploadedFileFactory && $factory instanceof UploadedFileFactoryInterface) {
171+
$this->uploadedFileFactory = $factory;
172+
}
173+
if (!$this->uriFactory && $factory instanceof UriFactoryInterface) {
174+
$this->uriFactory = $factory;
175+
}
176+
177+
return $factory;
178+
}
179+
180+
private function buildServerRequestFromGlobals(ServerRequestInterface $request, array $server, array $files): ServerRequestInterface
181+
{
182+
$request = $request
183+
->withProtocolVersion(isset($server['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1')
184+
->withUploadedFiles($this->normalizeFiles($files));
185+
186+
$headers = [];
187+
foreach ($server as $key => $value) {
188+
if (0 === strpos($key, 'HTTP_')) {
189+
$key = substr($key, 5);
190+
} elseif (!\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
191+
continue;
192+
}
193+
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
194+
195+
$headers[$key] = $value;
196+
}
197+
198+
if (!isset($headers['Authorization'])) {
199+
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
200+
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
201+
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
202+
$headers['Authorization'] = 'Basic '.base64_encode($_SERVER['PHP_AUTH_USER'].':'.($_SERVER['PHP_AUTH_PW'] ?? ''));
203+
} elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
204+
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
205+
}
206+
}
207+
208+
foreach ($headers as $key => $value) {
209+
try {
210+
$request = $request->withHeader($key, $value);
211+
} catch (\InvalidArgumentException $e) {
212+
// ignore invalid headers
213+
}
214+
}
215+
216+
return $request;
217+
}
218+
219+
private function buildUriFromGlobals(UriInterface $uri, array $server): UriInterface
220+
{
221+
$uri = $uri->withScheme(!empty($server['HTTPS']) && 'off' !== strtolower($server['HTTPS']) ? 'https' : 'http');
222+
223+
$hasPort = false;
224+
if (isset($server['HTTP_HOST'])) {
225+
$parts = parse_url('http://'.$server['HTTP_HOST']);
226+
227+
$uri = $uri->withHost($parts['host'] ?? 'localhost');
228+
229+
if ($parts['port'] ?? false) {
230+
$hasPort = true;
231+
$uri = $uri->withPort($parts['port']);
232+
}
233+
} else {
234+
$uri = $uri->withHost($server['SERVER_NAME'] ?? $server['SERVER_ADDR'] ?? 'localhost');
235+
}
236+
237+
if (!$hasPort && isset($server['SERVER_PORT'])) {
238+
$uri = $uri->withPort($server['SERVER_PORT']);
239+
}
240+
241+
$hasQuery = false;
242+
if (isset($server['REQUEST_URI'])) {
243+
$requestUriParts = explode('?', $server['REQUEST_URI'], 2);
244+
$uri = $uri->withPath($requestUriParts[0]);
245+
if (isset($requestUriParts[1])) {
246+
$hasQuery = true;
247+
$uri = $uri->withQuery($requestUriParts[1]);
248+
}
249+
}
250+
251+
if (!$hasQuery && isset($server['QUERY_STRING'])) {
252+
$uri = $uri->withQuery($server['QUERY_STRING']);
253+
}
254+
255+
return $uri;
256+
}
257+
258+
private function normalizeFiles(array $files): array
259+
{
260+
$normalized = [];
261+
262+
foreach ($files as $key => $value) {
263+
if ($value instanceof UploadedFileInterface) {
264+
$normalized[$key] = $value;
265+
} elseif (!\is_array($value)) {
266+
continue;
267+
} elseif (!isset($value['tmp_name'])) {
268+
$normalized[$key] = $this->normalizeFiles($value);
269+
} elseif (\is_array($value['tmp_name'])) {
270+
foreach ($value['tmp_name'] as $k => $v) {
271+
$file = $this->createStreamFromFile($value['tmp_name'][$k], 'r');
272+
$normalized[$key][$k] = $this->createUploadedFile($file, $value['size'][$k], $value['error'][$k], $value['name'][$k], $value['type'][$k]);
273+
}
274+
} else {
275+
$file = $this->createStreamFromFile($value['tmp_name'], 'r');
276+
$normalized[$key] = $this->createUploadedFile($file, $value['size'], $value['error'], $value['name'], $value['type']);
277+
}
278+
}
279+
280+
return $normalized;
281+
}
282+
}

tests/HttpClientDiscoveryTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class HttpClientDiscoveryTest extends TestCase
1010
{
1111
public function testFind()
1212
{
13+
if (!interface_exists(HttpClient::class)) {
14+
$this->markTestSkipped(HttpClient::class.' required.');
15+
}
16+
1317
$client = HttpClientDiscovery::find();
1418
$this->assertInstanceOf(HttpClient::class, $client);
1519
}

tests/Psr17FactoryDiscoveryTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class Psr17FactoryDiscoveryTest extends TestCase
1919
*/
2020
public function testFind($method, $interface)
2121
{
22+
if (!interface_exists(RequestFactoryInterface::class)) {
23+
$this->markTestSkipped(RequestFactoryInterface::class.' required.');
24+
}
25+
2226
$callable = [Psr17FactoryDiscovery::class, $method];
2327
$client = $callable();
2428
$this->assertInstanceOf($interface, $client);

0 commit comments

Comments
 (0)