diff --git a/CHANGELOG.md b/CHANGELOG.md index 112bfc3..a2faf1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer.json b/composer.json index eba01d8..6b528f1 100644 --- a/composer.json +++ b/composer.json @@ -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", @@ -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", diff --git a/src/Client.php b/src/Client.php index 5b19bd1..7dc4262 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,9 +7,7 @@ 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; @@ -17,7 +15,7 @@ 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 Михаил Красильников @@ -34,7 +32,7 @@ class Client implements HttpClient, HttpAsyncClient * * @var array */ - private $options; + private $curlOptions; /** * PSR-17 response factory. @@ -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( [ @@ -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); } /** @@ -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 @@ -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 @@ -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) { @@ -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 { @@ -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: @@ -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(); @@ -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]; } @@ -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'); diff --git a/tests/Functional/HttpAsyncClientDiactorosTest.php b/tests/Functional/HttpAsyncClientDiactorosTest.php index 273ea0f..2ef7b6c 100644 --- a/tests/Functional/HttpAsyncClientDiactorosTest.php +++ b/tests/Functional/HttpAsyncClientDiactorosTest.php @@ -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 { diff --git a/tests/Functional/HttpAsyncClientGuzzleTest.php b/tests/Functional/HttpAsyncClientGuzzleTest.php index 1b72d83..f1d2808 100644 --- a/tests/Functional/HttpAsyncClientGuzzleTest.php +++ b/tests/Functional/HttpAsyncClientGuzzleTest.php @@ -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 { diff --git a/tests/Functional/HttpAsyncClientTestCase.php b/tests/Functional/HttpAsyncClientTestCase.php index b940dc6..ded8f72 100644 --- a/tests/Functional/HttpAsyncClientTestCase.php +++ b/tests/Functional/HttpAsyncClientTestCase.php @@ -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 ); } } diff --git a/tests/Functional/HttpClientDiactorosTest.php b/tests/Functional/HttpClientDiactorosTest.php index 5117f4b..854bcdc 100644 --- a/tests/Functional/HttpClientDiactorosTest.php +++ b/tests/Functional/HttpClientDiactorosTest.php @@ -16,23 +16,11 @@ */ class HttpClientDiactorosTest extends HttpClientTestCase { - /** - * Create stream from file. - * - * @param string $filename - * - * @return StreamInterface - */ - protected function createFileStream($filename): StreamInterface + protected function createFileStream(string $filename): StreamInterface { return new Stream($filename); } - /** - * Create HTTP client for tests. - * - * @return HttpClient - */ protected function createHttpAdapter(): HttpClient { return new Client(new ResponseFactory(), new StreamFactory()); diff --git a/tests/Functional/HttpClientGuzzleTest.php b/tests/Functional/HttpClientGuzzleTest.php index 02d1689..9cbe9da 100644 --- a/tests/Functional/HttpClientGuzzleTest.php +++ b/tests/Functional/HttpClientGuzzleTest.php @@ -12,26 +12,22 @@ use Psr\Http\Message\StreamInterface; /** - * Tests for Http\Client\Curl\Client. + * @covers \Http\Client\Curl\Client */ class HttpClientGuzzleTest extends HttpClientTestCase { /** - * @return HttpClient + * {@inheritdoc} */ - protected function createHttpAdapter() + protected function createHttpAdapter(): HttpClient { return new Client(new GuzzleMessageFactory(), new GuzzleStreamFactory()); } /** - * Create stream from file. - * - * @param string $filename - * - * @return StreamInterface + * {@inheritdoc} */ - protected function createFileStream($filename) + protected function createFileStream(string $filename): StreamInterface { return new Stream(fopen($filename, 'r')); } diff --git a/tests/Functional/HttpClientTestCase.php b/tests/Functional/HttpClientTestCase.php index e7cec47..6df8a8d 100644 --- a/tests/Functional/HttpClientTestCase.php +++ b/tests/Functional/HttpClientTestCase.php @@ -20,9 +20,6 @@ abstract class HttpClientTestCase extends HttpClientTest */ protected $tmpFiles = []; - /** - * Test sending large files. - */ public function testSendLargeFile(): void { $filename = $this->createTempFile(); @@ -55,63 +52,46 @@ public function testSendLargeFile(): void } /** - * 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 testSendRequest($method, $uri, array $headers, $body): void + public function testSendRequest($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::testSendRequest( - $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 testSendRequestWithOutcome( $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::testSendRequestWithOutcome( $uriAndOutcome, - $protocolVersion, - $headers, - $body + $httpVersion, + $httpHeaders, + $requestBody ); } - /** - * Create stream from file. - * - * @param string $filename - * - * @return StreamInterface - */ - abstract protected function createFileStream($filename): StreamInterface; + abstract protected function createFileStream(string $filename): StreamInterface; /** * Create temporary file. @@ -127,7 +107,7 @@ protected function createTempFile(): string } /** - * Tears down the fixture. + * Delete files created with createTempFile */ protected function tearDown() { diff --git a/tests/Unit/ClientTest.php b/tests/Unit/ClientTest.php index d4274c1..264e977 100644 --- a/tests/Unit/ClientTest.php +++ b/tests/Unit/ClientTest.php @@ -12,8 +12,6 @@ use Zend\Diactoros\Request; /** - * Tests for Http\Client\Curl\Client. - * * @covers \Http\Client\Curl\Client */ class ClientTest extends TestCase diff --git a/tests/Unit/CurlPromiseTest.php b/tests/Unit/CurlPromiseTest.php index 2cf42d8..617b258 100644 --- a/tests/Unit/CurlPromiseTest.php +++ b/tests/Unit/CurlPromiseTest.php @@ -13,15 +13,10 @@ use Psr\Http\Message\ResponseInterface; /** - * Tests for Http\Client\Curl\CurlPromise. - * * @covers \Http\Client\Curl\CurlPromise */ class CurlPromiseTest extends TestCase { - /** - * TODO Summary - */ public function testCoreCallWaitFulfilled(): void { $core = $this->createMock(PromiseCore::class); @@ -38,9 +33,6 @@ public function testCoreCallWaitFulfilled(): void self::assertSame($response, $promise->wait()); } - /** - * TODO Summary - */ public function testCoreCallWaitRejected(): void { $core = $this->createMock(PromiseCore::class); @@ -59,9 +51,6 @@ public function testCoreCallWaitRejected(): void } } - /** - * Test that promise call core methods. - */ public function testCoreCalls(): void { $core = $this->createMock(PromiseCore::class); diff --git a/tests/Unit/PromiseCoreTest.php b/tests/Unit/PromiseCoreTest.php index 489f834..b52df28 100644 --- a/tests/Unit/PromiseCoreTest.php +++ b/tests/Unit/PromiseCoreTest.php @@ -16,8 +16,6 @@ use Psr\Http\Message\StreamInterface; /** - * Tests for Http\Client\Curl\PromiseCore. - * * @covers \Http\Client\Curl\PromiseCore */ class PromiseCoreTest extends TestCase @@ -87,9 +85,6 @@ function (RequestException $exception) { self::assertEquals('Bar', $core->getException()->getMessage()); } - /** - * @expectedException \LogicException - */ public function testNotRejected(): void { $request = $this->createMock(RequestInterface::class); @@ -98,12 +93,10 @@ public function testNotRejected(): void $this->handle = curl_init(); $core = new PromiseCore($request, $this->handle, $responseBuilder); + $this->expectException(\LogicException::class); $core->getException(); } - /** - * Test on fulfill actions. - */ public function testOnFulfill(): void { $request = $this->createMock(RequestInterface::class); @@ -132,9 +125,6 @@ function (ResponseInterface $response) use ($response1, $response2) { self::assertEquals(Promise::FULFILLED, $core->getState()); } - /** - * Test on reject actions. - */ public function testOnReject(): void { $request = $this->createMock(RequestInterface::class); @@ -162,9 +152,6 @@ function (RequestException $exception) { self::assertEquals('Bar', $core->getException()->getMessage()); } - /** - * Tears down the fixture. - */ protected function tearDown() { if (is_resource($this->handle)) {