diff --git a/.gitattributes b/.gitattributes index b63d8d1..5841755 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,9 @@ +spec/ export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore CONTRIBUTING.md export-ignore +phpspec.yml.ci export-ignore +phpspec.yml.dist export-ignore diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..60d211b --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,11 @@ +filter: + paths: [src/*] +checks: + php: + code_rating: true + duplication: true +tools: + external_code_coverage: true + php_code_sniffer: + config: + standard: "PSR2" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..688282a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,38 @@ +language: php + +sudo: false + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +env: + global: + - TEST_COMMAND="composer test" + +matrix: + allow_failures: + - php: 7.0 + fast_finish: true + include: + - php: 5.4 + env: + - COMPOSER_FLAGS="--prefer-stable --prefer-lowest" + - COVERAGE=true + - TEST_COMMAND="composer test-ci" + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction + +script: + - $TEST_COMMAND + +after_success: + - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/README.md b/README.md index a1f36f8..c21c3a7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -# HTTP Adapter +# HTTP Client -[![Latest Version](https://img.shields.io/github/release/php-http/adapter.svg?style=flat-square)](https://github.com/php-http/adapter/releases) +[![Latest Version](https://img.shields.io/github/release/php-http/client.svg?style=flat-square)](https://github.com/php-http/client/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![Total Downloads](https://img.shields.io/packagist/dt/php-http/adapter.svg?style=flat-square)](https://packagist.org/packages/php-http/adapter) +[![Build Status](https://img.shields.io/travis/php-http/client.svg?style=flat-square)](https://travis-ci.org/php-http/client) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/client.svg?style=flat-square)](https://packagist.org/packages/php-http/client) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/php-http/adapter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -**HTTP Adapter interfaces.** +**HTTP Client interfaces.** ## Install @@ -14,15 +17,17 @@ Via Composer ``` bash -$ composer require php-http/adapter +$ composer require php-http/client ``` ## Usage -This is the contract package for HTTP Adapter. It should be used when implementing a custom HTTP Adapter or to rely on a stable version of interfaces. +This is the contract package for HTTP Client interfacess. PSR-7 does not contain Client interfaces which is fine. However there is still need for HTTP Client interoperability. -There is also a virtual package which is versioned together with this contract package: [php-http/adapter-implementation](https://packagist.org/providers/php-http/adapter-implementation). +These interfaces are mostly used to create adapter packages around existing HTTP Client implementations. + +There is also a virtual package which is versioned together with this contract package: [php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation). ## Documentation @@ -30,6 +35,13 @@ There is also a virtual package which is versioned together with this contract p Please see the [official documentation](http://php-http.readthedocs.org/en/latest/). +## Testing + +``` bash +$ composer test +``` + + ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) for details. diff --git a/composer.json b/composer.json index cc7bd7d..49135b2 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "php-http/adapter", - "description": "HTTP Adapter interfaces", + "name": "php-http/client", + "description": "HTTP Client interfaces", "license": "MIT", - "keywords": ["http", "adapter"], + "keywords": ["http", "client"], "homepage": "http://php-http.org", "authors": [ { @@ -18,11 +18,19 @@ "php": ">=5.4", "psr/http-message": "^1.0" }, + "require-dev": { + "phpspec/phpspec": "^2.2", + "henrikbjorn/phpspec-code-coverage" : "^1.0" + }, "autoload": { "psr-4": { - "Http\\Adapter\\": "src/" + "Http\\Client\\": "src/" } }, + "scripts": { + "test": "vendor/bin/phpspec run", + "test-ci": "vendor/bin/phpspec run -c phpspec.yml.ci" + }, "extra": { "branch-alias": { "dev-master": "0.2-dev" diff --git a/phpspec.yml.ci b/phpspec.yml.ci new file mode 100644 index 0000000..36adab5 --- /dev/null +++ b/phpspec.yml.ci @@ -0,0 +1,10 @@ +suites: + client_suite: + namespace: Http\Client + psr4_prefix: Http\Client +formatter.name: pretty +extensions: + - PhpSpec\Extension\CodeCoverageExtension +code_coverage: + format: clover + output: build/coverage.xml diff --git a/phpspec.yml.dist b/phpspec.yml.dist new file mode 100644 index 0000000..472bf13 --- /dev/null +++ b/phpspec.yml.dist @@ -0,0 +1,5 @@ +suites: + client_suite: + namespace: Http\Client + psr4_prefix: Http\Client +formatter.name: pretty diff --git a/spec/BatchResultSpec.php b/spec/BatchResultSpec.php new file mode 100644 index 0000000..429541b --- /dev/null +++ b/spec/BatchResultSpec.php @@ -0,0 +1,44 @@ +shouldHaveType('Http\Client\BatchResult'); + } + + function it_is_immutable(RequestInterface $request, ResponseInterface $response) + { + $new = $this->addResponse($request, $response); + + $this->getResponses()->shouldReturn([]); + $new->shouldHaveType('Http\Client\BatchResult'); + $new->getResponses()->shouldReturn([$response]); + } + + function it_has_a_responses(RequestInterface $request, ResponseInterface $response) + { + $new = $this->addResponse($request, $response); + + $this->hasResponses()->shouldReturn(false); + $this->getResponses()->shouldReturn([]); + $new->hasResponses()->shouldReturn(true); + $new->getResponses()->shouldReturn([$response]); + } + + function it_has_a_response_for_a_request(RequestInterface $request, ResponseInterface $response) + { + $new = $this->addResponse($request, $response); + + $this->shouldThrow('Http\Client\Exception\UnexpectedValueException')->duringGetResponseFor($request); + $this->hasResponseFor($request)->shouldReturn(false); + $new->getResponseFor($request)->shouldReturn($response); + $new->hasResponseFor($request)->shouldReturn(true); + } +} diff --git a/spec/Body/CombinedMultipartSpec.php b/spec/Body/CombinedMultipartSpec.php new file mode 100644 index 0000000..194b497 --- /dev/null +++ b/spec/Body/CombinedMultipartSpec.php @@ -0,0 +1,44 @@ +file = tempnam(sys_get_temp_dir(), 'multipart'); + + $this->beConstructedWith(['data' => 1], ['file' => $this->file], 'boundary'); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Body\CombinedMultipart'); + } + + function it_is_body() + { + $this->shouldImplement('Http\Client\Body'); + } + + function it_is_multipart() + { + $this->shouldHaveType('Http\Client\Body\Multipart'); + } + + function it_has_content_header() + { + $this->getContentHeaders()->shouldReturn(['Content-Type' => 'multipart/form-data; boundary=boundary']); + } + + function it_is_streamable() + { + $body = sprintf("--boundary\r\nContent-Disposition: form-data; name=\"data\"\r\n\r\n1\r\n--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n\r\n\r\n", basename($this->file)); + + $this->toStreamable()->shouldReturn($body); + } +} diff --git a/spec/Body/FilesSpec.php b/spec/Body/FilesSpec.php new file mode 100644 index 0000000..0f5774c --- /dev/null +++ b/spec/Body/FilesSpec.php @@ -0,0 +1,44 @@ +file = tempnam(sys_get_temp_dir(), 'multipart'); + + $this->beConstructedWith(['file' => $this->file], 'boundary'); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Body\Files'); + } + + function it_is_body() + { + $this->shouldImplement('Http\Client\Body'); + } + + function it_is_multipart() + { + $this->shouldHaveType('Http\Client\Body\Multipart'); + } + + function it_has_content_header() + { + $this->getContentHeaders()->shouldReturn(['Content-Type' => 'multipart/form-data; boundary=boundary']); + } + + function it_is_streamable() + { + $body = sprintf("--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n\r\n\r\n", basename($this->file)); + + $this->toStreamable()->shouldReturn($body); + } +} diff --git a/spec/Body/MultipartDataSpec.php b/spec/Body/MultipartDataSpec.php new file mode 100644 index 0000000..94265ab --- /dev/null +++ b/spec/Body/MultipartDataSpec.php @@ -0,0 +1,40 @@ +beConstructedWith(['data' => 1], 'boundary'); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Body\MultipartData'); + } + + function it_is_body() + { + $this->shouldImplement('Http\Client\Body'); + } + + function it_is_multipart() + { + $this->shouldHaveType('Http\Client\Body\Multipart'); + } + + function it_has_content_header() + { + $this->getContentHeaders()->shouldReturn(['Content-Type' => 'multipart/form-data; boundary=boundary']); + } + + function it_is_streamable() + { + $body = "--boundary\r\nContent-Disposition: form-data; name=\"data\"\r\n\r\n1\r\n"; + + $this->toStreamable()->shouldReturn($body); + } +} diff --git a/spec/Body/UrlencodedDataSpec.php b/spec/Body/UrlencodedDataSpec.php new file mode 100644 index 0000000..5e4526b --- /dev/null +++ b/spec/Body/UrlencodedDataSpec.php @@ -0,0 +1,33 @@ +beConstructedWith(['data1' => 1, 'data2' => 2]); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Body\UrlencodedData'); + } + + function it_is_body() + { + $this->shouldImplement('Http\Client\Body'); + } + + function it_has_content_header() + { + $this->getContentHeaders()->shouldReturn(['Content-Type' => 'application/x-www-form-urlencoded']); + } + + function it_is_streamable() + { + $this->toStreamable()->shouldReturn('data1=1&data2=2'); + } +} diff --git a/spec/Exception/BatchExceptionSpec.php b/spec/Exception/BatchExceptionSpec.php new file mode 100644 index 0000000..4e48fb7 --- /dev/null +++ b/spec/Exception/BatchExceptionSpec.php @@ -0,0 +1,69 @@ +shouldHaveType('Http\Client\Exception\BatchException'); + } + + function it_is_a_runtime_exception() + { + $this->shouldHaveType('RuntimeException'); + } + + function it_is_an_exception() + { + $this->shouldImplement('Http\Client\Exception'); + } + + function it_has_a_result() + { + $this->setResult($result = new BatchResult()); + $this->getResult()->shouldReturn($result); + } + + function it_throws_an_exception_if_the_result_is_already_set() + { + $this->getResult()->shouldHaveType('Http\Client\BatchResult'); + $this->shouldThrow('Http\Client\Exception\InvalidArgumentException')->duringSetResult(new BatchResult()); + } + + function it_has_an_exception_for_a_request(RequestInterface $request, Exception $exception) + { + $this->shouldThrow('Http\Client\Exception\UnexpectedValueException')->duringGetExceptionFor($request); + $this->hasExceptionFor($request)->shouldReturn(false); + + $this->addException($request, $exception); + + $this->getExceptionFor($request)->shouldReturn($exception); + $this->hasExceptionFor($request)->shouldReturn(true); + } + + function it_has_exceptions(RequestInterface $request, Exception $exception) + { + $this->getExceptions()->shouldReturn([]); + + $this->addException($request, $exception); + + $this->getExceptions()->shouldReturn([$exception]); + } + + function it_checks_if_a_request_failed(RequestInterface $request, Exception $exception) + { + $this->isSuccessful($request)->shouldReturn(false); + $this->isFailed($request)->shouldReturn(false); + + $this->addException($request, $exception); + + $this->isSuccessful($request)->shouldReturn(false); + $this->isFailed($request)->shouldReturn(true); + } +} diff --git a/spec/Exception/ClientExceptionSpec.php b/spec/Exception/ClientExceptionSpec.php new file mode 100644 index 0000000..a128f6d --- /dev/null +++ b/spec/Exception/ClientExceptionSpec.php @@ -0,0 +1,27 @@ +getStatusCode()->willReturn(400); + + $this->beConstructedWith('message', $request, $response); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Exception\ClientException'); + } + + function it_is_an_http_exception() + { + $this->shouldHaveType('Http\Client\Exception\HttpException'); + } +} diff --git a/spec/Exception/HttpExceptionSpec.php b/spec/Exception/HttpExceptionSpec.php new file mode 100644 index 0000000..0eb7289 --- /dev/null +++ b/spec/Exception/HttpExceptionSpec.php @@ -0,0 +1,71 @@ +getStatusCode()->willReturn(400); + + $this->beConstructedWith('message', $request, $response); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Exception\HttpException'); + } + + function it_is_a_request_exception() + { + $this->shouldHaveType('Http\Client\Exception\RequestException'); + } + + function it_has_a_response(ResponseInterface $response) + { + $this->getResponse()->shouldReturn($response); + } + + function it_creates_a_client_exception(RequestInterface $request, ResponseInterface $response) + { + $request->getRequestTarget()->willReturn('/uri'); + $request->getMethod()->willReturn('GET'); + $response->getStatusCode()->willReturn(404); + $response->getReasonPhrase()->willReturn('Not Found'); + + $e = $this->create($request, $response); + + $e->shouldHaveType('Http\Client\Exception\ClientException'); + $e->getMessage()->shouldReturn('Client error [url] /uri [http method] GET [status code] 404 [reason phrase] Not Found'); + } + + function it_creates_a_server_exception(RequestInterface $request, ResponseInterface $response) + { + $request->getRequestTarget()->willReturn('/uri'); + $request->getMethod()->willReturn('GET'); + $response->getStatusCode()->willReturn(500); + $response->getReasonPhrase()->willReturn('Internal Server Error'); + + $e = $this->create($request, $response); + + $e->shouldHaveType('Http\Client\Exception\ServerException'); + $e->getMessage()->shouldReturn('Server error [url] /uri [http method] GET [status code] 500 [reason phrase] Internal Server Error'); + } + + function it_creates_an_http_exception(RequestInterface $request, ResponseInterface $response) + { + $request->getRequestTarget()->willReturn('/uri'); + $request->getMethod()->willReturn('GET'); + $response->getStatusCode()->willReturn(100); + $response->getReasonPhrase()->willReturn('Continue'); + + $e = $this->create($request, $response); + + $e->shouldHaveType('Http\Client\Exception\HttpException'); + $e->getMessage()->shouldReturn('Unsuccessful response [url] /uri [http method] GET [status code] 100 [reason phrase] Continue'); + } +} diff --git a/spec/Exception/InvalidArgumentExceptionSpec.php b/spec/Exception/InvalidArgumentExceptionSpec.php new file mode 100644 index 0000000..37eabfb --- /dev/null +++ b/spec/Exception/InvalidArgumentExceptionSpec.php @@ -0,0 +1,23 @@ +shouldHaveType('Http\Client\Exception\InvalidArgumentException'); + } + + function it_is_an_invalid_argument_exception() + { + $this->shouldHaveType('InvalidArgumentException'); + } + + function it_is_an_exception() + { + $this->shouldImplement('Http\Client\Exception'); + } +} diff --git a/spec/Exception/NetworkExceptionSpec.php b/spec/Exception/NetworkExceptionSpec.php new file mode 100644 index 0000000..b98708b --- /dev/null +++ b/spec/Exception/NetworkExceptionSpec.php @@ -0,0 +1,24 @@ +beConstructedWith('message', $request); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Exception\NetworkException'); + } + + function it_is_a_request_exception() + { + $this->shouldHaveType('Http\Client\Exception\RequestException'); + } +} diff --git a/spec/Exception/RequestExceptionSpec.php b/spec/Exception/RequestExceptionSpec.php new file mode 100644 index 0000000..fe13b68 --- /dev/null +++ b/spec/Exception/RequestExceptionSpec.php @@ -0,0 +1,44 @@ +beConstructedWith('message', $request); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Exception\RequestException'); + } + + function it_is_a_transfer_exception() + { + $this->shouldHaveType('Http\Client\Exception\TransferException'); + } + + function it_has_a_request(RequestInterface $request) + { + $this->getRequest()->shouldReturn($request); + } + + function it_wraps_an_exception(RequestInterface $request) + { + $e = new \Exception('message'); + + $requestException = $this->wrapException($request, $e); + + $requestException->getMessage()->shouldReturn('message'); + } + + function it_does_not_wrap_if_request_exception(RequestInterface $request, RequestException $requestException) + { + $this->wrapException($request, $requestException)->shouldReturn($requestException); + } +} diff --git a/spec/Exception/RuntimeExceptionSpec.php b/spec/Exception/RuntimeExceptionSpec.php new file mode 100644 index 0000000..0e41726 --- /dev/null +++ b/spec/Exception/RuntimeExceptionSpec.php @@ -0,0 +1,23 @@ +shouldHaveType('Http\Client\Exception\RuntimeException'); + } + + function it_is_a_runtime_exception() + { + $this->shouldHaveType('RuntimeException'); + } + + function it_is_an_exception() + { + $this->shouldImplement('Http\Client\Exception'); + } +} diff --git a/spec/Exception/ServerExceptionSpec.php b/spec/Exception/ServerExceptionSpec.php new file mode 100644 index 0000000..22f8fa0 --- /dev/null +++ b/spec/Exception/ServerExceptionSpec.php @@ -0,0 +1,27 @@ +getStatusCode()->willReturn(500); + + $this->beConstructedWith('message', $request, $response); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Exception\ServerException'); + } + + function it_is_an_http_exception() + { + $this->shouldHaveType('Http\Client\Exception\HttpException'); + } +} diff --git a/spec/Exception/TransferExceptionSpec.php b/spec/Exception/TransferExceptionSpec.php new file mode 100644 index 0000000..a8ba434 --- /dev/null +++ b/spec/Exception/TransferExceptionSpec.php @@ -0,0 +1,24 @@ +shouldHaveType('Http\Client\Exception\TransferException'); + } + + function it_is_a_runtime_exception() + { + $this->shouldHaveType('RuntimeException'); + } + + function it_is_an_exception() + { + $this->shouldImplement('Http\Client\Exception'); + } +} diff --git a/spec/Exception/UnexpectedValueExceptionSpec.php b/spec/Exception/UnexpectedValueExceptionSpec.php new file mode 100644 index 0000000..9eba8c3 --- /dev/null +++ b/spec/Exception/UnexpectedValueExceptionSpec.php @@ -0,0 +1,23 @@ +shouldHaveType('Http\Client\Exception\UnexpectedValueException'); + } + + function it_is_an_unexpected_value_exception() + { + $this->shouldHaveType('UnexpectedValueException'); + } + + function it_is_an_exception() + { + $this->shouldImplement('Http\Client\Exception'); + } +} diff --git a/spec/HttpMethodsSpec.php b/spec/HttpMethodsSpec.php new file mode 100644 index 0000000..cb10cdd --- /dev/null +++ b/spec/HttpMethodsSpec.php @@ -0,0 +1,106 @@ +beAnInstanceOf('spec\Http\Client\HttpMethodsStub'); + } + + function it_sends_a_get_request() + { + $data = HttpMethodsStub::$requestData; + + $this->get($data['uri'], $data['headers'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_head_request() + { + $data = HttpMethodsStub::$requestData; + + $this->head($data['uri'], $data['headers'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_trace_request() + { + $data = HttpMethodsStub::$requestData; + + $this->trace($data['uri'], $data['headers'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_post_request() + { + $data = HttpMethodsStub::$requestData; + + $this->post($data['uri'], $data['headers'], $data['body'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_put_request() + { + $data = HttpMethodsStub::$requestData; + + $this->put($data['uri'], $data['headers'], $data['body'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_patch_request() + { + $data = HttpMethodsStub::$requestData; + + $this->patch($data['uri'], $data['headers'], $data['body'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_delete_request() + { + $data = HttpMethodsStub::$requestData; + + $this->delete($data['uri'], $data['headers'], $data['body'], $data['options'])->shouldReturn(true); + } + + function it_sends_a_options_request() + { + $data = HttpMethodsStub::$requestData; + + $this->options($data['uri'], $data['headers'], $data['body'], $data['options'])->shouldReturn(true); + } +} + +class HttpMethodsStub implements HttpMethodsClient +{ + use HttpMethods; + + public static $requestData = [ + 'uri' => '/uri', + 'headers' => [ + 'Content-Type' => 'text/plain', + ], + 'body' => 'body', + 'options' => [ + 'timeout' => 60, + ], + ]; + + /** + * {@inheritdoc} + */ + public function send($method, $uri, array $headers = [], $body = null, array $options = []) + { + if (in_array($method, ['GET', 'HEAD', 'TRACE'])) { + return $uri === self::$requestData['uri'] && + $headers === self::$requestData['headers'] && + is_null($body) && + $options === self::$requestData['options']; + } + + return in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) && + $uri === self::$requestData['uri'] && + $headers === self::$requestData['headers'] && + $body === self::$requestData['body'] && + $options === self::$requestData['options']; + } +} diff --git a/spec/Util/BatchRequestSpec.php b/spec/Util/BatchRequestSpec.php new file mode 100644 index 0000000..baf23fe --- /dev/null +++ b/spec/Util/BatchRequestSpec.php @@ -0,0 +1,53 @@ +beAnInstanceOf('spec\Http\Client\Util\BatchRequestStub', [$client]); + } + + function it_send_multiple_request_using_send_request(HttpPsrClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response1, ResponseInterface $response2) + { + $client->sendRequest($request1, [])->willReturn($response1); + $client->sendRequest($request2, [])->willReturn($response2); + + $this->sendRequests([$request1, $request2], [])->shouldReturnAnInstanceOf('\Http\Client\BatchResult'); + } + + function it_throw_batch_exception_if_one_or_more_request_failed(HttpPsrClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response) + { + $client->sendRequest($request1, [])->willReturn($response); + $client->sendRequest($request2, [])->willThrow('\Http\Client\Exception\HttpException'); + + $this->shouldThrow('\Http\Client\Exception\BatchException')->duringSendRequests([$request1, $request2], []); + } +} + +class BatchRequestStub implements HttpPsrClient +{ + use BatchRequest; + + protected $client; + + public function __construct(HttpPsrClient $client) + { + $this->client = $client; + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request, array $options = []) + { + return $this->client->sendRequest($request, $options); + } +} diff --git a/src/BatchResult.php b/src/BatchResult.php new file mode 100644 index 0000000..45280c5 --- /dev/null +++ b/src/BatchResult.php @@ -0,0 +1,104 @@ + + */ +final class BatchResult +{ + /** + * @var \SplObjectStorage + */ + private $responses; + + public function __construct() + { + $this->responses = new \SplObjectStorage(); + } + + /** + * Returns all successful responses + * + * @return ResponseInterface[] + */ + public function getResponses() + { + $responses = []; + + foreach ($this->responses as $request) { + $responses[] = $this->responses[$request]; + } + + return $responses; + } + + /** + * Returns a response of a request + * + * @param RequestInterface $request + * + * @return ResponseInterface + * + * @throws UnexpectedValueException + */ + public function getResponseFor(RequestInterface $request) + { + try { + return $this->responses[$request]; + } catch (\UnexpectedValueException $e) { + throw new UnexpectedValueException('Request not found', $e->getCode(), $e); + } + } + + /** + * Checks if there are any successful responses at all + * + * @return boolean + */ + public function hasResponses() + { + return $this->responses->count() > 0; + } + + /** + * Checks if there is a response of a request + * + * @param RequestInterface $request + * + * @return ResponseInterface + */ + public function hasResponseFor(RequestInterface $request) + { + return $this->responses->contains($request); + } + + /** + * Adds a response in an immutable way + * + * @param RequestInterface $request + * @param ResponseInterface $response + * + * @return BatchResult + * + * @internal + */ + public function addResponse(RequestInterface $request, ResponseInterface $response) + { + $new = clone $this; + $new->responses->attach($request, $response); + + return $new; + } + + public function __clone() + { + $this->responses = clone $this->responses; + } +} diff --git a/src/Body.php b/src/Body.php new file mode 100644 index 0000000..2d73119 --- /dev/null +++ b/src/Body.php @@ -0,0 +1,33 @@ + + */ +interface Body +{ + /** + * Returns a set of headers which is needed to correctly send the body + * + * Note: these headers get overwritten by headers manually passed to the client + * + * Content-Length is calculated automatically if possible + * + * @return array + */ + public function getContentHeaders(); + + /** + * Convert data to a format which can be used to create a proper PSR-7 Stream + * + * @return string|StreamInterface + * + * @throws Exception + */ + public function toStreamable(); +} diff --git a/src/Body/CombinedMultipart.php b/src/Body/CombinedMultipart.php new file mode 100644 index 0000000..f032bec --- /dev/null +++ b/src/Body/CombinedMultipart.php @@ -0,0 +1,45 @@ + + */ +class CombinedMultipart extends Multipart +{ + /** + * @var MultipartData + */ + protected $data; + + /** + * @var Files + */ + protected $files; + + /** + * @param array $data + * @param array $files + * @param string|null $boundary + */ + public function __construct(array $data, array $files, $boundary = null) + { + parent::__construct($boundary); + + $this->data = new MultipartData($data, $this->boundary); + $this->files = new Files($files, $this->boundary); + } + + /** + * {@inheritdoc} + */ + public function toStreamable() + { + $body = $this->data->toStreamable(); + $body .= $this->files->toStreamable(); + + return $body; + } +} diff --git a/src/Body/Files.php b/src/Body/Files.php new file mode 100644 index 0000000..ed9be1c --- /dev/null +++ b/src/Body/Files.php @@ -0,0 +1,53 @@ + + */ +class Files extends Multipart +{ + /** + * @var array + */ + protected $files; + + /** + * @param array $files + * @param string|null $boundary + */ + public function __construct(array $files, $boundary = null) + { + $this->files = $files; + + parent::__construct($boundary); + } + + /** + * {@inheritdoc} + */ + public function toStreamable() + { + $body = ''; + + foreach ($this->files as $name => $file) { + if (!is_file($file)) { + throw new RuntimeException(sprintf('File "%s" does not exist', $file)); + } + + $body .= sprintf( + "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n\r\n%s\r\n", + $this->boundary, + $name, + basename($file), + file_get_contents($file) + ); + } + + return $body; + } +} diff --git a/src/Body/Multipart.php b/src/Body/Multipart.php new file mode 100644 index 0000000..62170ae --- /dev/null +++ b/src/Body/Multipart.php @@ -0,0 +1,40 @@ + + */ +abstract class Multipart implements Body +{ + /** + * @var string + */ + protected $boundary; + + /** + * @param string|null $boundary + */ + public function __construct($boundary = null) + { + if (is_null($boundary)) { + $boundary = sha1(microtime()); + } + + $this->boundary = $boundary; + } + + /** + * {@inheritdoc} + */ + public function getContentHeaders() + { + return [ + 'Content-Type' => 'multipart/form-data; boundary='.$this->boundary, + ]; + } +} diff --git a/src/Body/MultipartData.php b/src/Body/MultipartData.php new file mode 100644 index 0000000..b6d4f6a --- /dev/null +++ b/src/Body/MultipartData.php @@ -0,0 +1,67 @@ + + */ +class MultipartData extends Multipart +{ + /** + * @var array + */ + protected $data; + + /** + * @param array $data + * @param string|null $boundary + */ + public function __construct(array $data, $boundary = null) + { + $this->data = $data; + + parent::__construct($boundary); + } + + /** + * {@inheritdoc} + */ + public function toStreamable() + { + return $this->prepareData(null, $this->data); + } + + /** + * @param string|integer|null $name + * @param mixed $data + * + * @return string + */ + protected function prepareData($name, $data) + { + $body = ''; + + if (is_array($data)) { + foreach ($data as $subName => $subData) { + if (!is_null($name)) { + $subName = sprintf('%s[%s]', $name, $subName); + } + + $body .= $this->prepareData($subName, $subData); + } + + return $body; + } + + $body .= sprintf( + "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", + $this->boundary, + $name, + $data + ); + + return $body; + } +} diff --git a/src/Body/UrlencodedData.php b/src/Body/UrlencodedData.php new file mode 100644 index 0000000..ea90e57 --- /dev/null +++ b/src/Body/UrlencodedData.php @@ -0,0 +1,42 @@ + + */ +class UrlencodedData implements Body +{ + /** + * @var array + */ + protected $data; + + /** + * @param array $data + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function getContentHeaders() + { + return [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ]; + } + + /** + * {@inheritdoc} + */ + public function toStreamable() + { + return http_build_query($this->data, null, '&'); + } +} diff --git a/src/Exception.php b/src/Exception.php index 726dd9e..392be92 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,17 +1,10 @@ - * - * For the full copyright and license information, please read the LICENSE - * file that was distributed with this source code. - */ - -namespace Http\Adapter; +namespace Http\Client; /** + * Every HTTP Client related Exception should implement this interface + * * @author Márk Sági-Kazár */ interface Exception diff --git a/src/Exception/BatchException.php b/src/Exception/BatchException.php new file mode 100644 index 0000000..ac8346b --- /dev/null +++ b/src/Exception/BatchException.php @@ -0,0 +1,147 @@ + + */ +final class BatchException extends \RuntimeException implements Exception +{ + /** + * @var BatchResult + */ + private $result; + + /** + * @var \SplObjectStorage + */ + private $exceptions; + + public function __construct() + { + $this->exceptions = new \SplObjectStorage(); + } + + /** + * Returns the BatchResult that contains those responses that where successful. + * + * Note that the BatchResult may contains 0 responses if all requests failed. + * + * @return BatchResult + */ + public function getResult() + { + if (!isset($this->result)) { + $this->result = new BatchResult(); + } + + return $this->result; + } + + /** + * Sets the successful response list + * + * @param BatchResult $result + * + * @throws InvalidArgumentException If the BatchResult is already set + * + * @internal + */ + public function setResult(BatchResult $result) + { + if (isset($this->result)) { + throw new InvalidArgumentException('BatchResult is already set'); + } + + $this->result = $result; + } + + /** + * Checks if a request is successful + * + * @param RequestInterface $request + * + * @return boolean + */ + public function isSuccessful(RequestInterface $request) + { + return $this->getResult()->hasResponseFor($request); + } + + /** + * Checks if a request is failed + * + * @param RequestInterface $request + * + * @return boolean + */ + public function isFailed(RequestInterface $request) + { + return $this->exceptions->contains($request); + } + + /** + * Returns all exceptions + * + * @return Exception[] + */ + public function getExceptions() + { + $exceptions = []; + + foreach ($this->exceptions as $request) { + $exceptions[] = $this->exceptions[$request]; + } + + return $exceptions; + } + + /** + * Returns an exception for a request + * + * @param RequestInterface $request + * + * @return Exception + * + * @throws UnexpectedValueException + */ + public function getExceptionFor(RequestInterface $request) + { + try { + return $this->exceptions[$request]; + } catch (\UnexpectedValueException $e) { + throw new UnexpectedValueException('Request not found', $e->getCode(), $e); + } + } + + /** + * Checks if there is an exception for a request + * + * @param RequestInterface $request + * + * @return boolean + */ + public function hasExceptionFor(RequestInterface $request) + { + return $this->exceptions->contains($request); + } + + /** + * Adds an exception + * + * @param RequestInterface $request + * @param Exception $exception + */ + public function addException(RequestInterface $request, Exception $exception) + { + $this->exceptions->attach($request, $exception); + } +} diff --git a/src/Exception/ClientException.php b/src/Exception/ClientException.php new file mode 100644 index 0000000..454e386 --- /dev/null +++ b/src/Exception/ClientException.php @@ -0,0 +1,12 @@ + + */ +final class ClientException extends HttpException +{ +} diff --git a/src/Exception/HttpAdapterException.php b/src/Exception/HttpAdapterException.php deleted file mode 100644 index 49713b8..0000000 --- a/src/Exception/HttpAdapterException.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please read the LICENSE - * file that was distributed with this source code. - */ - -namespace Http\Adapter\Exception; - -use Http\Adapter\Exception; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; - -/** - * @author GeLo - */ -class HttpAdapterException extends \Exception implements Exception -{ - /** - * @var RequestInterface|null - */ - private $request; - - /** - * @var ResponseInterface|null - */ - private $response; - - /** - * Returns the request - * - * @return RequestInterface|null - */ - public function getRequest() - { - return $this->request; - } - - /** - * Checks if there is a request - * - * @return boolean - */ - public function hasRequest() - { - return isset($this->request); - } - - /** - * Sets the request - * - * @param RequestInterface|null $request - */ - public function setRequest(RequestInterface $request = null) - { - $this->request = $request; - } - - /** - * Returns the response - * - * @return ResponseInterface|null - */ - public function getResponse() - { - return $this->response; - } - - /** - * Checks if there is a response - * - * @return boolean - */ - public function hasResponse() - { - return isset($this->response); - } - - /** - * Sets the response - * - * @param ResponseInterface|null $response - */ - public function setResponse(ResponseInterface $response = null) - { - $this->response = $response; - } - - /** - * @param string $uri - * @param string $adapterName - * @param \Exception|null $previous - */ - public static function cannotFetchUri($uri, $adapterName, \Exception $previous = null) - { - $message = sprintf( - 'An error occurred when fetching the URI "%s" with the adapter "%s" ("%s").', - $uri, - $adapterName, - isset($previous) ? $previous->getMessage() : '' - ); - - $code = isset($previous) ? $previous->getCode() : 0; - - return new self($message, $code, $previous); - } -} diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php new file mode 100644 index 0000000..5e47b07 --- /dev/null +++ b/src/Exception/HttpException.php @@ -0,0 +1,86 @@ + + */ +class HttpException extends RequestException +{ + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @param string $message + * @param RequestInterface $request + * @param ResponseInterface $response + * @param \Exception|null $previous + */ + public function __construct( + $message, + RequestInterface $request, + ResponseInterface $response, + \Exception $previous = null + ) { + $this->response = $response; + $this->code = $response->getStatusCode(); + + + parent::__construct($message, $request, $previous); + } + + /** + * Returns the response + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Factory method to create a new exception with a normalized error message + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param \Exception|null $previous + * + * @return HttpException + */ + public static function create(RequestInterface $request, ResponseInterface $response, \Exception $previous = null) + { + $code = $response->getStatusCode(); + + if ($code >= 400 && $code < 500) { + $message = 'Client error'; + $className = __NAMESPACE__ . '\\ClientException'; + } elseif ($code >= 500 && $code < 600) { + $message = 'Server error'; + $className = __NAMESPACE__ . '\\ServerException'; + } else { + $message = 'Unsuccessful response'; + $className = __CLASS__; + } + + $message = sprintf( + '%s [url] %s [http method] %s [status code] %s [reason phrase] %s', + $message, + $request->getRequestTarget(), + $request->getMethod(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + return new $className($message, $request, $response, $previous); + } +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..2ab338b --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,14 @@ + + */ +final class InvalidArgumentException extends \InvalidArgumentException implements Exception +{ +} diff --git a/src/Exception/MultiHttpAdapterException.php b/src/Exception/MultiHttpAdapterException.php deleted file mode 100644 index 7f900e5..0000000 --- a/src/Exception/MultiHttpAdapterException.php +++ /dev/null @@ -1,235 +0,0 @@ - - * - * For the full copyright and license information, please read the LICENSE - * file that was distributed with this source code. - */ - -namespace Http\Adapter\Exception; - -use Http\Adapter\Exception; -use Psr\Http\Message\ResponseInterface; - -/** - * @author GeLo - */ -class MultiHttpAdapterException extends \Exception implements Exception -{ - /** - * @var HttpAdapterException[] - */ - private $exceptions; - - /** - * @var ResponseInterface[] - */ - private $responses; - - /** - * @param HttpAdapterException[] $exceptions - * @param ResponseInterface[] $responses - */ - public function __construct(array $exceptions = [], array $responses = []) - { - parent::__construct('An error occurred when sending multiple requests.'); - - $this->setExceptions($exceptions); - $this->setResponses($responses); - } - - /** - * Returns all exceptions - * - * @return HttpAdapterException[] - */ - public function getExceptions() - { - return $this->exceptions; - } - - /** - * Checks if a specific exception exists - * - * @param HttpAdapterException $exception - * - * @return boolean TRUE if there is the exception else FALSE. - */ - public function hasException(HttpAdapterException $exception) - { - return array_search($exception, $this->exceptions, true) !== false; - } - - /** - * Checks if any exception exists - * - * @return boolean - */ - public function hasExceptions() - { - return !empty($this->exceptions); - } - - /** - * Sets the exceptions - * - * @param HttpAdapterException[] $exceptions - */ - public function setExceptions(array $exceptions) - { - $this->clearExceptions(); - $this->addExceptions($exceptions); - } - - /** - * Adds an exception - * - * @param HttpAdapterException $exception - */ - public function addException(HttpAdapterException $exception) - { - $this->exceptions[] = $exception; - } - - /** - * Adds some exceptions - * - * @param HttpAdapterException[] $exceptions - */ - public function addExceptions(array $exceptions) - { - foreach ($exceptions as $exception) { - $this->addException($exception); - } - } - - /** - * Removes an exception - * - * @param HttpAdapterException $exception - */ - public function removeException(HttpAdapterException $exception) - { - unset($this->exceptions[array_search($exception, $this->exceptions, true)]); - $this->exceptions = array_values($this->exceptions); - } - - /** - * Removes some exceptions - * - * @param HttpAdapterException[] $exceptions - */ - public function removeExceptions(array $exceptions) - { - foreach ($exceptions as $exception) { - $this->removeException($exception); - } - } - - /** - * Clears all exceptions - */ - public function clearExceptions() - { - $this->exceptions = []; - } - - /** - * Returns all responses - * - * @return ResponseInterface[] - */ - public function getResponses() - { - return $this->responses; - } - - /** - * Checks if a specific response exists - * - * @param ResponseInterface $response - * - * @return boolean - */ - public function hasResponse(ResponseInterface $response) - { - return array_search($response, $this->responses, true) !== false; - } - - /** - * Checks if any response exists - * - * @return boolean - */ - public function hasResponses() - { - return !empty($this->responses); - } - - /** - * Sets the responses - * - * @param ResponseInterface[] $responses - */ - public function setResponses(array $responses) - { - $this->clearResponses(); - $this->addResponses($responses); - } - - /** - * Adds a response - * - * @param ResponseInterface $response - */ - public function addResponse(ResponseInterface $response) - { - $this->responses[] = $response; - } - - /** - * Adds some responses - * - * @param ResponseInterface[] $responses - */ - public function addResponses(array $responses) - { - foreach ($responses as $response) { - $this->addResponse($response); - } - } - - /** - * Removes a response - * - * @param ResponseInterface $response - */ - public function removeResponse(ResponseInterface $response) - { - unset($this->responses[array_search($response, $this->responses, true)]); - $this->responses = array_values($this->responses); - } - - /** - * Removes some responses - * - * @param ResponseInterface[] $responses - */ - public function removeResponses(array $responses) - { - foreach ($responses as $response) { - $this->removeResponse($response); - } - } - - /** - * Clears all responses - */ - public function clearResponses() - { - $this->responses = []; - } -} diff --git a/src/Exception/NetworkException.php b/src/Exception/NetworkException.php new file mode 100644 index 0000000..21ea391 --- /dev/null +++ b/src/Exception/NetworkException.php @@ -0,0 +1,12 @@ + + */ +class NetworkException extends RequestException +{ +} diff --git a/src/Exception/RequestException.php b/src/Exception/RequestException.php new file mode 100644 index 0000000..c4e74a3 --- /dev/null +++ b/src/Exception/RequestException.php @@ -0,0 +1,57 @@ + + */ +class RequestException extends TransferException +{ + /** + * @var RequestInterface + */ + private $request; + + /** + * @param string $message + * @param RequestInterface $request + * @param \Exception|null $previous + */ + public function __construct($message, RequestInterface $request, \Exception $previous = null) + { + $this->request = $request; + + parent::__construct($message, 0, $previous); + } + + /** + * Returns the request + * + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * @param RequestInterface $request + * @param \Exception $e + * + * @return RequestException + */ + public static function wrapException(RequestInterface $request, \Exception $e) + { + if (!$e instanceof RequestException) { + $e = new RequestException($e->getMessage(), $request, $e); + } + + return $e; + } +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 0000000..ea7d50a --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,14 @@ + + */ +final class RuntimeException extends \RuntimeException implements Exception +{ +} diff --git a/src/Exception/ServerException.php b/src/Exception/ServerException.php new file mode 100644 index 0000000..42c7b4b --- /dev/null +++ b/src/Exception/ServerException.php @@ -0,0 +1,12 @@ + + */ +final class ServerException extends HttpException +{ +} diff --git a/src/Exception/TransferException.php b/src/Exception/TransferException.php new file mode 100644 index 0000000..0fc2f2c --- /dev/null +++ b/src/Exception/TransferException.php @@ -0,0 +1,14 @@ + + */ +class TransferException extends \RuntimeException implements Exception +{ +} diff --git a/src/Exception/UnexpectedValueException.php b/src/Exception/UnexpectedValueException.php new file mode 100644 index 0000000..4c436b7 --- /dev/null +++ b/src/Exception/UnexpectedValueException.php @@ -0,0 +1,14 @@ + + */ +final class UnexpectedValueException extends \UnexpectedValueException implements Exception +{ +} diff --git a/src/HttpClient.php b/src/HttpClient.php new file mode 100644 index 0000000..b870451 --- /dev/null +++ b/src/HttpClient.php @@ -0,0 +1,13 @@ + + */ +interface HttpClient extends HttpPsrClient, HttpMethodsClient +{ + +} diff --git a/src/HttpMethods.php b/src/HttpMethods.php new file mode 100644 index 0000000..59991a7 --- /dev/null +++ b/src/HttpMethods.php @@ -0,0 +1,82 @@ + + */ +trait HttpMethods +{ + /** + * {@inheritdoc} + */ + public function get($uri, array $headers = [], array $options = []) + { + return $this->send('GET', $uri, $headers, null, $options); + } + + /** + * {@inheritdoc} + */ + public function head($uri, array $headers = [], array $options = []) + { + return $this->send('HEAD', $uri, $headers, null, $options); + } + + /** + * {@inheritdoc} + */ + public function trace($uri, array $headers = [], array $options = []) + { + return $this->send('TRACE', $uri, $headers, null, $options); + } + + /** + * {@inheritdoc} + */ + public function post($uri, array $headers = [], $body = null, array $options = []) + { + return $this->send('POST', $uri, $headers, $body, $options); + } + + /** + * {@inheritdoc} + */ + public function put($uri, array $headers = [], $body = null, array $options = []) + { + return $this->send('PUT', $uri, $headers, $body, $options); + } + + /** + * {@inheritdoc} + */ + public function patch($uri, array $headers = [], $body = null, array $options = []) + { + return $this->send('PATCH', $uri, $headers, $body, $options); + } + + /** + * {@inheritdoc} + */ + public function delete($uri, array $headers = [], $body = null, array $options = []) + { + return $this->send('DELETE', $uri, $headers, $body, $options); + } + + /** + * {@inheritdoc} + */ + public function options($uri, array $headers = [], $body = null, array $options = []) + { + return $this->send('OPTIONS', $uri, $headers, $body, $options); + } + + /** + * {@inheritdoc} + */ + abstract public function send($method, $uri, array $headers = [], $body = null, array $options = []); +} diff --git a/src/HttpMethodsClient.php b/src/HttpMethodsClient.php new file mode 100644 index 0000000..9b6bec2 --- /dev/null +++ b/src/HttpMethodsClient.php @@ -0,0 +1,141 @@ + + */ +interface HttpMethodsClient +{ + /** + * Sends a GET request + * + * @param string|UriInterface $uri + * @param array $headers + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function get($uri, array $headers = [], array $options = []); + + /** + * Sends an HEAD request + * + * @param string|UriInterface $uri + * @param array $headers + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function head($uri, array $headers = [], array $options = []); + + /** + * Sends a TRACE request + * + * @param string|UriInterface $uri + * @param array $headers + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function trace($uri, array $headers = [], array $options = []); + + /** + * Sends a POST request + * + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function post($uri, array $headers = [], $body = null, array $options = []); + + /** + * Sends a PUT request + * + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function put($uri, array $headers = [], $body = null, array $options = []); + + /** + * Sends a PATCH request + * + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function patch($uri, array $headers = [], $body = null, array $options = []); + + /** + * Sends a DELETE request + * + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function delete($uri, array $headers = [], $body = null, array $options = []); + + /** + * Sends an OPTIONS request + * + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function options($uri, array $headers = [], $body = null, array $options = []); + + /** + * Sends a request + * + * @param string $method + * @param string|UriInterface $uri + * @param array $headers + * @param string|Body|StreamInterface|null $body + * @param array $options + * + * @throws Exception + * + * @return ResponseInterface + */ + public function send($method, $uri, array $headers = [], $body = null, array $options = []); +} diff --git a/src/HttpAdapter.php b/src/HttpPsrClient.php similarity index 51% rename from src/HttpAdapter.php rename to src/HttpPsrClient.php index 0447f9a..5215fa2 100644 --- a/src/HttpAdapter.php +++ b/src/HttpPsrClient.php @@ -1,23 +1,17 @@ - * - * For the full copyright and license information, please read the LICENSE - * file that was distributed with this source code. - */ - -namespace Http\Adapter; +namespace Http\Client; +use Http\Client\Exception\BatchException; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; /** + * Sends one or more PSR-7 Request + * * @author GeLo */ -interface HttpAdapter +interface HttpPsrClient { /** * Sends a PSR request @@ -27,28 +21,23 @@ interface HttpAdapter * * @return ResponseInterface * - * @throws \InvalidArgumentException - * @throws Exception\HttpAdapterException + * @throws Exception */ public function sendRequest(RequestInterface $request, array $options = []); /** * Sends PSR requests * + * If one or more requests led to an exception, the BatchException is thrown. + * The BatchException also gives access to the BatchResult for the successful responses. + * * @param RequestInterface[] $requests * @param array $options * - * @return ResponseInterface[] + * @return BatchResult If all requests where successful. * - * @throws \InvalidArgumentException - * @throws Exception\MultiHttpAdapterException + * @throws Exception + * @throws BatchException */ public function sendRequests(array $requests, array $options = []); - - /** - * Returns the name - * - * @return string - */ - public function getName(); } diff --git a/src/Util/BatchRequest.php b/src/Util/BatchRequest.php new file mode 100644 index 0000000..e8ff1ae --- /dev/null +++ b/src/Util/BatchRequest.php @@ -0,0 +1,49 @@ + + */ +trait BatchRequest +{ + /** + * {@inheritdoc} + */ + abstract public function sendRequest(RequestInterface $request, array $options = []); + + /** + * {@inheritdoc} + */ + public function sendRequests(array $requests, array $options = []) + { + $batchResult = new BatchResult(); + $batchException = new BatchException(); + $batchException->setResult($batchResult); + + foreach ($requests as $request) { + try { + $batchResult->addResponse($request, $this->sendRequest($request, $options)); + } catch (Exception $e) { + $batchException->addException($request, $e); + } + } + + if (count($batchException->getExceptions()) > 0) { + throw $batchException; + } + + return $batchResult; + } +}