Skip to content
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

PoC for PSR-17 #2525

Draft
wants to merge 2 commits into
base: 7.5
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions composer.json
Expand Up @@ -29,13 +29,12 @@
"php": "^7.2.5",
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"guzzlehttp/psr7": "dev-master",
"psr/http-client": "^1.0"
},
"require-dev": {
"ext-curl": "*",
"ergebnis/composer-normalize": "^2.0",
"php-http/client-integration-tests": "dev-phpunit8",
"phpunit/phpunit": "^8.5",
"psr/log": "^1.1"
},
Expand Down
163 changes: 12 additions & 151 deletions src/Client.php
Expand Up @@ -5,6 +5,7 @@
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\HttpFactory;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
Expand Down Expand Up @@ -68,6 +69,12 @@ public function __construct(array $config = [])
throw new InvalidArgumentException('handler must be a callable');
}

if (!isset($config['http_factory'])) {
$config['http_factory'] = new HttpFactory();
} elseif (!$config['http_factory'] instanceof HttpFactory) {
throw new InvalidArgumentException('http_factory must be an instance of callable');
}

// Convert the base_uri to a UriInterface
if (isset($config['base_uri'])) {
$config['base_uri'] = Psr7\uri_for($config['base_uri']);
Expand Down Expand Up @@ -156,18 +163,12 @@ public function sendRequest(RequestInterface $request): ResponseInterface
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{
$options = $this->prepareDefaults($options);
// Remove request modifying parameter because it can be done up-front.
$headers = isset($options['headers']) ? $options['headers'] : [];
$body = isset($options['body']) ? $options['body'] : null;
$version = isset($options['version']) ? $options['version'] : '1.1';
// Merge the URI into the base URI.
$uri = $this->buildUri(Psr7\uri_for($uri), $options);
if (\is_array($body)) {
$this->invalidBody();
}
$request = new Psr7\Request($method, $uri, $headers, $body, $version);
// Remove the option so that they are not doubly-applied.
unset($options['headers'], $options['body'], $options['version']);

/** @var HttpFactory $factory */
$factory = $options['http_factory'];
$request = $factory->createRequest($method, $uri);

return $this->transfer($request, $options);
}
Expand Down Expand Up @@ -329,7 +330,7 @@ private function prepareDefaults(array $options): array
*/
private function transfer(RequestInterface $request, array $options): PromiseInterface
{
$request = $this->applyOptions($request, $options);
$request = apply_http_options($request, $options);
/** @var HandlerStack $handler */
$handler = $options['handler'];

Expand All @@ -339,144 +340,4 @@ private function transfer(RequestInterface $request, array $options): PromiseInt
return Promise\rejection_for($e);
}
}

/**
* Applies the array of request options to a request.
*/
private function applyOptions(RequestInterface $request, array &$options): RequestInterface
{
$modify = [
'set_headers' => [],
];

if (isset($options['headers'])) {
$modify['set_headers'] = $options['headers'];
unset($options['headers']);
}

if (isset($options['form_params'])) {
if (isset($options['multipart'])) {
throw new InvalidArgumentException('You cannot use '
. 'form_params and multipart at the same time. Use the '
. 'form_params option if you want to send application/'
. 'x-www-form-urlencoded requests, and the multipart '
. 'option to send multipart/form-data requests.');
}
$options['body'] = \http_build_query($options['form_params'], '', '&');
unset($options['form_params']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
}

if (isset($options['multipart'])) {
$options['body'] = new Psr7\MultipartStream($options['multipart']);
unset($options['multipart']);
}

if (isset($options['json'])) {
$options['body'] = \GuzzleHttp\json_encode($options['json']);
unset($options['json']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/json';
}

if (!empty($options['decode_content'])
&& $options['decode_content'] !== true
) {
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']);
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
}

if (isset($options['body'])) {
if (\is_array($options['body'])) {
$this->invalidBody();
}
$modify['body'] = Psr7\stream_for($options['body']);
unset($options['body']);
}

if (!empty($options['auth']) && \is_array($options['auth'])) {
$value = $options['auth'];
$type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
switch ($type) {
case 'basic':
// Ensure that we don't have the header in different case and set the new value.
$modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']);
$modify['set_headers']['Authorization'] = 'Basic '
. \base64_encode("$value[0]:$value[1]");
break;
case 'digest':
// @todo: Do not rely on curl
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
case 'ntlm':
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM;
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
}
}

if (isset($options['query'])) {
$value = $options['query'];
if (\is_array($value)) {
$value = \http_build_query($value, null, '&', PHP_QUERY_RFC3986);
}
if (!\is_string($value)) {
throw new InvalidArgumentException('query must be a string or array');
}
$modify['query'] = $value;
unset($options['query']);
}

// Ensure that sink is not an invalid value.
if (isset($options['sink'])) {
// TODO: Add more sink validation?
if (\is_bool($options['sink'])) {
throw new InvalidArgumentException('sink must not be a boolean');
}
}

$request = Psr7\modify_request($request, $modify);
if ($request->getBody() instanceof Psr7\MultipartStream) {
// Use a multipart/form-data POST if a Content-Type is not set.
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
. $request->getBody()->getBoundary();
}

// Merge in conditional headers if they are not present.
if (isset($options['_conditional'])) {
// Build up the changes so it's in a single clone of the message.
$modify = [];
foreach ($options['_conditional'] as $k => $v) {
if (!$request->hasHeader($k)) {
$modify['set_headers'][$k] = $v;
}
}
$request = Psr7\modify_request($request, $modify);
// Don't pass this internal value along to middleware/handlers.
unset($options['_conditional']);
}

return $request;
}

/**
* Throw Exception with pre-set message.
*
* @throws InvalidArgumentException Invalid body.
*/
private function invalidBody(): void
{
throw new InvalidArgumentException('Passing in the "body" request '
. 'option as an array to send a POST request has been deprecated. '
. 'Please use the "form_params" request option to send a '
. 'application/x-www-form-urlencoded request, or the "multipart" '
. 'request option to send a multipart/form-data request.');
}
}
135 changes: 135 additions & 0 deletions src/functions.php
Expand Up @@ -6,6 +6,7 @@
use GuzzleHttp\Handler\CurlMultiHandler;
use GuzzleHttp\Handler\Proxy;
use GuzzleHttp\Handler\StreamHandler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

/**
Expand Down Expand Up @@ -252,6 +253,140 @@ function is_host_in_noproxy(string $host, array $noProxyArray): bool
return false;
}

/**
* Applies the array of request options to a request.
*/
function apply_http_options(RequestInterface $request, array &$options): RequestInterface
{
$modify = [
'set_headers' => [],
];

if (isset($options['version'])) {
$modify['version'] = $options['version'];
}

if (isset($options['headers'])) {
$modify['set_headers'] = $options['headers'];
unset($options['headers']);
}

if (isset($options['form_params'])) {
if (isset($options['multipart'])) {
throw new InvalidArgumentException('You cannot use '
. 'form_params and multipart at the same time. Use the '
. 'form_params option if you want to send application/'
. 'x-www-form-urlencoded requests, and the multipart '
. 'option to send multipart/form-data requests.');
}
$options['body'] = \http_build_query($options['form_params'], '', '&');
unset($options['form_params']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
}

if (isset($options['multipart'])) {
$options['body'] = new Psr7\MultipartStream($options['multipart']);
unset($options['multipart']);
}

if (isset($options['json'])) {
$options['body'] = \GuzzleHttp\json_encode($options['json']);
unset($options['json']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/json';
}

if (!empty($options['decode_content'])
&& $options['decode_content'] !== true
) {
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']);
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
}

if (isset($options['body'])) {
if (\is_array($options['body'])) {
throw new InvalidArgumentException('Passing in the "body" request '
. 'option as an array to send a POST request has been deprecated. '
. 'Please use the "form_params" request option to send a '
. 'application/x-www-form-urlencoded request, or the "multipart" '
. 'request option to send a multipart/form-data request.');
}
$modify['body'] = Psr7\stream_for($options['body']);
unset($options['body']);
}

if (!empty($options['auth']) && \is_array($options['auth'])) {
$value = $options['auth'];
$type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
switch ($type) {
case 'basic':
// Ensure that we don't have the header in different case and set the new value.
$modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']);
$modify['set_headers']['Authorization'] = 'Basic '
. \base64_encode("$value[0]:$value[1]");
break;
case 'digest':
// @todo: Do not rely on curl
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
case 'ntlm':
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM;
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
}
}

if (isset($options['query'])) {
$value = $options['query'];
if (\is_array($value)) {
$value = \http_build_query($value, null, '&', PHP_QUERY_RFC3986);
}
if (!\is_string($value)) {
throw new InvalidArgumentException('query must be a string or array');
}
$modify['query'] = $value;
unset($options['query']);
}

// Ensure that sink is not an invalid value.
if (isset($options['sink'])) {
// TODO: Add more sink validation?
if (\is_bool($options['sink'])) {
throw new InvalidArgumentException('sink must not be a boolean');
}
}

$request = Psr7\modify_request($request, $modify);
if ($request->getBody() instanceof Psr7\MultipartStream) {
// Use a multipart/form-data POST if a Content-Type is not set.
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
. $request->getBody()->getBoundary();
}

// Merge in conditional headers if they are not present.
if (isset($options['_conditional'])) {
// Build up the changes so it's in a single clone of the message.
$modify = [];
foreach ($options['_conditional'] as $k => $v) {
if (!$request->hasHeader($k)) {
$modify['set_headers'][$k] = $v;
}
}
$request = Psr7\modify_request($request, $modify);
// Don't pass this internal value along to middleware/handlers.
unset($options['_conditional']);
}

return $request;
}

/**
* Wrapper for json_decode that throws when an error occurs.
*
Expand Down
18 changes: 0 additions & 18 deletions tests/HttplugIntegrationTest.php

This file was deleted.