Skip to content

cleanup for PSR-17 #53

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
Jan 24, 2019
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Change Log

## Unreleased
## 2.0.0 - Unreleased

- Client expects PSR-17 ResponseFactoryInterface and StreamFactoryInterface rather than Httplug factories.
- Allow cURL options to overwrite our default spec-compliant default configuration

## 1.7.1 - 2018-03-36
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "php-http/curl-client",
"description": "PSR-18 cURL client",
"description": "PSR-18 and HTTPlug Async client with cURL",
"license": "MIT",
"keywords": [
"curl",
Expand All @@ -19,7 +19,7 @@
"require": {
"php": "^7.1",
"ext-curl": "*",
"php-http/discovery": "^1.0",
"php-http/discovery": "^1.6",
"php-http/httplug": "^2.0",
"php-http/message": "^1.2",
"psr/http-client": "^1.0",
Expand Down
98 changes: 30 additions & 68 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
use Http\Client\Exception;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Promise\Promise;
use Http\Discovery\Psr17FactoryDiscovery;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* PSR-7 compatible cURL based HTTP client.
* PSR-18 and HTTPlug Async client based on lib-curl.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <[email protected]>
Expand All @@ -34,7 +32,7 @@ class Client implements HttpClient, HttpAsyncClient
*
* @var array
*/
private $options;
private $curlOptions;

/**
* PSR-17 response factory.
Expand Down Expand Up @@ -65,25 +63,19 @@ class Client implements HttpClient, HttpAsyncClient
private $multiRunner;

/**
* Construct client.
*
* @param ResponseFactoryInterface|null $responseFactory PSR-17 HTTP response factory.
* @param StreamFactoryInterface|null $streamFactory PSR-17 HTTP stream factory.
* @param array $options cURL options {@link http://php.net/curl_setopt}
*
* @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed
*
* @since x.x $messageFactory changed to PSR-17 ResponseFactoryInterface $responseFactory.
* @since x.x $streamFactory type changed to PSR-17 StreamFactoryInterface.
* @since 1.0
*/
public function __construct(
ResponseFactoryInterface $responseFactory = null,
StreamFactoryInterface $streamFactory = null,
array $options = []
) {
$this->responseFactory = $responseFactory; // FIXME ?: MessageFactoryDiscovery::find();
$this->streamFactory = $streamFactory; // FIXME ?: StreamFactoryDiscovery::find();
$this->responseFactory = $responseFactory ?: Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory();
$resolver = new OptionsResolver();
$resolver->setDefaults(
[
Expand All @@ -99,7 +91,7 @@ public function __construct(
// Make sure that we accept everything that is in the options.
$resolver->setDefined(array_keys($options));

$this->options = $resolver->resolve($options);
$this->curlOptions = $resolver->resolve($options);
}

/**
Expand All @@ -113,11 +105,7 @@ public function __destruct()
}

/**
* Sends a PSR-7 request and returns a PSR-7 response.
*
* @param RequestInterface $request
*
* @return ResponseInterface
* {@inheritdoc}
*
* @throws \Http\Client\Exception\NetworkException In case of network problems
* @throws \Http\Client\Exception\RequestException On invalid request
Expand Down Expand Up @@ -164,11 +152,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface
}

/**
* Sends a PSR-7 request in an asynchronous way.
*
* @param RequestInterface $request
*
* @return Promise
* {@inheritdoc}
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
Expand Down Expand Up @@ -198,36 +182,31 @@ public function sendAsyncRequest(RequestInterface $request)
/**
* Update cURL options for this request and hook in the response builder.
*
* @param RequestInterface $request
* @param ResponseBuilder $responseBuilder
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If can not read body
*
* @return array
*/
private function prepareRequestOptions(RequestInterface $request, ResponseBuilder $responseBuilder)
private function prepareRequestOptions(RequestInterface $request, ResponseBuilder $responseBuilder): array
{
$options = $this->options;
$curlOptions = $this->curlOptions;

try {
$options[CURLOPT_HTTP_VERSION]
$curlOptions[CURLOPT_HTTP_VERSION]
= $this->getProtocolVersion($request->getProtocolVersion());
} catch (\UnexpectedValueException $e) {
throw new Exception\RequestException($e->getMessage(), $request);
}
$options[CURLOPT_URL] = (string) $request->getUri();
$curlOptions[CURLOPT_URL] = (string) $request->getUri();

$options = $this->addRequestBodyOptions($request, $options);
$curlOptions = $this->addRequestBodyOptions($request, $curlOptions);

$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);
$curlOptions[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $curlOptions);

if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
$curlOptions[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}

$options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$curlOptions[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$str = trim($data);
if ('' !== $str) {
if (strpos(strtolower($str), 'http/') === 0) {
Expand All @@ -240,21 +219,17 @@ private function prepareRequestOptions(RequestInterface $request, ResponseBuilde
return strlen($data);
};

$options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$curlOptions[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
return $responseBuilder->getResponse()->getBody()->write($data);
};

return $options;
return $curlOptions;
}

/**
* Return cURL constant for specified HTTP version.
*
* @param string $requestVersion
*
* @throws \UnexpectedValueException If unsupported version requested
*
* @return int
*/
private function getProtocolVersion(string $requestVersion): int
{
Expand All @@ -275,13 +250,8 @@ private function getProtocolVersion(string $requestVersion): int

/**
* Add request body related cURL options.
*
* @param RequestInterface $request
* @param array $options
*
* @return array
*/
private function addRequestBodyOptions(RequestInterface $request, array $options): array
private function addRequestBodyOptions(RequestInterface $request, array $curlOptions): array
{
/*
* Some HTTP methods cannot have payload:
Expand All @@ -302,40 +272,37 @@ private function addRequestBodyOptions(RequestInterface $request, array $options
// Message has non empty body.
if (null === $bodySize || $bodySize > 1024 * 1024) {
// Avoid full loading large or unknown size body into memory
$options[CURLOPT_UPLOAD] = true;
$curlOptions[CURLOPT_UPLOAD] = true;
if (null !== $bodySize) {
$options[CURLOPT_INFILESIZE] = $bodySize;
$curlOptions[CURLOPT_INFILESIZE] = $bodySize;
}
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
$curlOptions[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
} else {
// Small body can be loaded into memory
$options[CURLOPT_POSTFIELDS] = (string) $body;
$curlOptions[CURLOPT_POSTFIELDS] = (string) $body;
}
}
}

if ($request->getMethod() === 'HEAD') {
// This will set HTTP method to "HEAD".
$options[CURLOPT_NOBODY] = true;
$curlOptions[CURLOPT_NOBODY] = true;
} elseif ($request->getMethod() !== 'GET') {
// GET is a default method. Other methods should be specified explicitly.
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
$curlOptions[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}

return $options;
return $curlOptions;
}

/**
* Create headers array for CURLOPT_HTTPHEADER.
*
* @param RequestInterface $request
* @param array $options cURL options
*
* @return string[]
*/
private function createHeaders(RequestInterface $request, array $options): array
private function createHeaders(RequestInterface $request, array $curlOptions): array
{
$curlHeaders = [];
$headers = $request->getHeaders();
Expand All @@ -346,10 +313,10 @@ private function createHeaders(RequestInterface $request, array $options): array
continue;
}
if ('content-length' === $header) {
if (array_key_exists(CURLOPT_POSTFIELDS, $options)) {
if (array_key_exists(CURLOPT_POSTFIELDS, $curlOptions)) {
// Small body content length can be calculated here.
$values = [strlen($options[CURLOPT_POSTFIELDS])];
} elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) {
$values = [strlen($curlOptions[CURLOPT_POSTFIELDS])];
} elseif (!array_key_exists(CURLOPT_READFUNCTION, $curlOptions)) {
// Else if there is no body, forcing "Content-length" to 0
$values = [0];
}
Expand All @@ -367,11 +334,6 @@ private function createHeaders(RequestInterface $request, array $options): array
return $curlHeaders;
}

/**
* Create new ResponseBuilder instance.
*
* @return ResponseBuilder
*/
private function createResponseBuilder(): ResponseBuilder
{
$body = $this->streamFactory->createStreamFromFile('php://temp', 'w+b');
Expand Down
6 changes: 2 additions & 4 deletions tests/Functional/HttpAsyncClientDiactorosTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
use Zend\Diactoros\StreamFactory;

/**
* Testing asynchronous requests with Zend Diactoros factories.
* @covers \Http\Client\Curl\Client
*/
class HttpAsyncClientDiactorosTest extends HttpAsyncClientTestCase
{
/**
* Create asynchronous HTTP client for tests.
*
* @return HttpAsyncClient
* {@inheritdoc}
*/
protected function createHttpAsyncClient(): HttpAsyncClient
{
Expand Down
6 changes: 2 additions & 4 deletions tests/Functional/HttpAsyncClientGuzzleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
use Http\Message\StreamFactory\GuzzleStreamFactory;

/**
* Tests for Http\Client\Curl\Client.
* @covers \Http\Client\Curl\Client
*/
class HttpAsyncClientGuzzleTest extends HttpAsyncClientTestCase
{
/**
* Create asynchronious HTTP client for tests.
*
* @return HttpAsyncClient
* {@inheritdoc}
*/
protected function createHttpAsyncClient(): HttpAsyncClient
{
Expand Down
40 changes: 15 additions & 25 deletions tests/Functional/HttpAsyncClientTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,42 @@
abstract class HttpAsyncClientTestCase extends HttpAsyncClientTest
{
/**
* TODO Summary.
*
* @param string $method HTTP method.
* @param string $uri Request URI.
* @param array $headers HTTP headers.
* @param string $body Request body.
* {@inheritdoc}
*
* @dataProvider requestProvider
*/
public function testAsyncSendRequest($method, $uri, array $headers, $body): void
public function testAsyncSendRequest($httpMethod, $uri, array $httpHeaders, $requestBody): void
{
if ($body !== null && in_array($method, ['GET', 'HEAD', 'TRACE'], true)) {
self::markTestSkipped('cURL can not send body using '.$method);
if ($requestBody !== null && in_array($httpMethod, ['GET', 'HEAD', 'TRACE'], true)) {
self::markTestSkipped('cURL can not send body using '.$httpMethod);
}
parent::testAsyncSendRequest(
$method,
$httpMethod,
$uri,
$headers,
$body
$httpHeaders,
$requestBody
);
}

/**
* TODO Summary.
*
* @param array $uriAndOutcome TODO ???
* @param string $protocolVersion HTTP version.
* @param array $headers HTTP headers.
* @param string $body Request body.
* {@inheritdoc}
*
* @dataProvider requestWithOutcomeProvider
*/
public function testSendAsyncRequestWithOutcome(
$uriAndOutcome,
$protocolVersion,
array $headers,
$body
$httpVersion,
array $httpHeaders,
$requestBody
): void {
if ( $body !== null) {
if ( $requestBody !== null) {
self::markTestSkipped('cURL can not send body using GET');
}
parent::testSendAsyncRequestWithOutcome(
$uriAndOutcome,
$protocolVersion,
$headers,
$body
$httpVersion,
$httpHeaders,
$requestBody
);
}
}
Loading