updated mailgun librarie

This commit is contained in:
2019-05-14 10:47:30 +02:00
parent c97e0f8ba1
commit dc39ddbbea
1588 changed files with 40009 additions and 143222 deletions

View File

@@ -0,0 +1,9 @@
<?php
return Symfony\CS\Config\Config::create()
->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
->fixers([])
->finder(
Symfony\CS\Finder\DefaultFinder::create()->in(__DIR__ . '/src')
)
;

View File

@@ -0,0 +1,4 @@
preset: psr2
finder:
path:
- "src"

View File

@@ -0,0 +1,173 @@
# Change Log
## Unreleased
## 1.7.1 - 2018-03-36
### Fixed
- #36: Failure evaluating code: is_resource($handle) (string assertions are deprecated in PHP 7.2)
## 1.7 - 2017-02-09
### Changed
- #30: Make sure we rewind streams
## 1.6.2 - 2017-01-02
### Fixed
- #29: Request not using CURLOPT_POSTFIELDS have content-length set to
### Changed
- Use binary mode to create response body stream.
## 1.6.1 - 2016-11-11
### Fixed
- #27: ErrorPlugin and sendAsyncRequest() incompatibility
## 1.6 - 2016-09-12
### Changed
- `Client::sendRequest` now throws `Http\Client\Exception\NetworkException` on network errors.
- `\UnexpectedValueException` replaced with `Http\Client\Exception\RequestException` in
`Client::sendRequest` and `Client::sendAsyncRequest`
## 1.5.1 - 2016-08-29
### Fixed
- #26: Combining CurlClient with StopwatchPlugin causes Promise onRejected handler to never be
invoked.
## 1.5 - 2016-08-03
### Changed
- Request body can be send with any method except GET, HEAD and TRACE.
- #25: Make discovery a hard dependency.
## 1.4.2 - 2016-06-14
### Added
- #23: "php-http/async-client-implementation" added to "provide" section.
## 1.4.1 - 2016-05-30
### Fixed
- #22: Cannot create the client using `HttpClientDiscovery`.
## 1.4 - 2016-03-30
### Changed
- #20: Minimize memory usage when reading large response body.
## 1.3 - 2016-03-14
### Fixed
- #18: Invalid "Expect" header.
### Removed
- #13: Remove HeaderParser.
## 1.2 - 2016-03-09
### Added
- #16: Make sure discovery can find the curl client
### Fixed
- #15: "Out of memory" sending large files.
## 1.1.0 - 2016-01-29
### Changed
- Switch to php-http/message 1.0.
## 1.0.0 - 2016-01-28
First stable release.
## 0.7.0 - 2016-01-26
### Changed
- Migrate from `php-http/discovery` and `php-http/utils` to `php-http/message`.
## 0.6.0 - 2016-01-12
### Changed
- Root namespace changed from `Http\Curl` to `Http\Client\Curl`.
- Main client class name renamed from `CurlHttpClient` to `Client`.
- Minimum required [php-http/discovery](https://packagist.org/packages/php-http/discovery)
version changed to 0.5.
## 0.5.0 - 2015-12-18
### Changed
- Compatibility with php-http/httplug 1.0 beta
- Switch to php-http/discovery 0.4
## 0.4.0 - 2015-12-16
### Changed
- Switch to php-http/message-factory 1.0
## 0.3.1 - 2015-12-14
### Changed
- Requirements fixed.
## 0.3.0 - 2015-11-24
### Changed
- Use cURL constants as options keys.
## 0.2.0 - 2015-11-17
### Added
- HttpAsyncClient support.
## 0.1.0 - 2015-11-11
### Added
- Initial release

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,44 @@
# Curl client for PHP HTTP
[![Latest Version](https://img.shields.io/github/release/php-http/curl-client.svg?style=flat-square)](https://github.com/php-http/curl-client/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/curl-client.svg?style=flat-square)](https://travis-ci.org/php-http/curl-client)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/curl-client.svg?style=flat-square)](https://packagist.org/packages/php-http/curl-client)
The cURL client use the cURL PHP extension which must be activated in your `php.ini`.
## Install
Via Composer
``` bash
$ composer require php-http/curl-client
```
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/clients/curl-client.html).
## Testing
``` bash
$ composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
## Security
If you discover any security related issues, please contact us at
[security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,50 @@
{
"name": "php-http/curl-client",
"description": "cURL client for PHP-HTTP",
"license": "MIT",
"keywords": ["http", "curl"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Михаил Красильников",
"email": "m.krasilnikov@yandex.ru"
}
],
"prefer-stable": true,
"minimum-stability": "beta",
"config": {
"bin-dir": "vendor/bin"
},
"require": {
"php": "^5.5 || ^7.0",
"ext-curl": "*",
"php-http/httplug": "^1.0",
"php-http/message-factory": "^1.0.2",
"php-http/message": "^1.2",
"php-http/discovery": "^1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1.0",
"php-http/client-integration-tests": "^0.6",
"phpunit/phpunit": "^4.8.27",
"zendframework/zend-diactoros": "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\Curl\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Http\\Client\\Curl\\Tests\\": "tests/"
}
},
"provide": {
"php-http/client-implementation": "1.0",
"php-http/async-client-implementation": "1.0"
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml"
}
}

View File

@@ -0,0 +1,242 @@
{
"version": "1.0",
"name": "php-http/curl-client",
"bindings": {
"98239b8b-103b-4f47-94c7-4cba49a05a1f": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpAsyncClient"
},
"a6a79968-2aa5-427c-bbe1-a581d9a48321": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpClient"
}
},
"config": {
"bootstrap-file": "vendor/autoload.php"
},
"packages": {
"clue/stream-filter": {
"install-path": "vendor/clue/stream-filter",
"installer": "composer",
"env": "dev"
},
"doctrine/instantiator": {
"install-path": "vendor/doctrine/instantiator",
"installer": "composer",
"env": "dev"
},
"guzzlehttp/psr7": {
"install-path": "vendor/guzzlehttp/psr7",
"installer": "composer",
"env": "dev"
},
"justinrainbow/json-schema": {
"install-path": "vendor/justinrainbow/json-schema",
"installer": "composer",
"env": "dev"
},
"paragonie/random_compat": {
"install-path": "vendor/paragonie/random_compat",
"installer": "composer",
"env": "dev"
},
"php-http/adapter-integration-tests": {
"install-path": "vendor/php-http/adapter-integration-tests",
"installer": "composer",
"env": "dev"
},
"php-http/discovery": {
"install-path": "vendor/php-http/discovery",
"installer": "composer",
"env": "dev"
},
"php-http/httplug": {
"install-path": "vendor/php-http/httplug",
"installer": "composer"
},
"php-http/message": {
"install-path": "vendor/php-http/message",
"installer": "composer",
"env": "dev"
},
"php-http/message-factory": {
"install-path": "vendor/php-http/message-factory",
"installer": "composer"
},
"php-http/promise": {
"install-path": "vendor/php-http/promise",
"installer": "composer"
},
"phpdocumentor/reflection-docblock": {
"install-path": "vendor/phpdocumentor/reflection-docblock",
"installer": "composer",
"env": "dev"
},
"phpspec/prophecy": {
"install-path": "vendor/phpspec/prophecy",
"installer": "composer",
"env": "dev"
},
"phpunit/php-code-coverage": {
"install-path": "vendor/phpunit/php-code-coverage",
"installer": "composer",
"env": "dev"
},
"phpunit/php-file-iterator": {
"install-path": "vendor/phpunit/php-file-iterator",
"installer": "composer",
"env": "dev"
},
"phpunit/php-text-template": {
"install-path": "vendor/phpunit/php-text-template",
"installer": "composer",
"env": "dev"
},
"phpunit/php-timer": {
"install-path": "vendor/phpunit/php-timer",
"installer": "composer",
"env": "dev"
},
"phpunit/php-token-stream": {
"install-path": "vendor/phpunit/php-token-stream",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit": {
"install-path": "vendor/phpunit/phpunit",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit-mock-objects": {
"install-path": "vendor/phpunit/phpunit-mock-objects",
"installer": "composer",
"env": "dev"
},
"psr/http-message": {
"install-path": "vendor/psr/http-message",
"installer": "composer"
},
"psr/log": {
"install-path": "vendor/psr/log",
"installer": "composer",
"env": "dev"
},
"puli/composer-plugin": {
"install-path": "vendor/puli/composer-plugin",
"installer": "composer",
"env": "dev"
},
"puli/discovery": {
"install-path": "vendor/puli/discovery",
"installer": "composer",
"env": "dev"
},
"puli/repository": {
"install-path": "vendor/puli/repository",
"installer": "composer",
"env": "dev"
},
"puli/url-generator": {
"install-path": "vendor/puli/url-generator",
"installer": "composer",
"env": "dev"
},
"ramsey/uuid": {
"install-path": "vendor/ramsey/uuid",
"installer": "composer",
"env": "dev"
},
"sebastian/comparator": {
"install-path": "vendor/sebastian/comparator",
"installer": "composer",
"env": "dev"
},
"sebastian/diff": {
"install-path": "vendor/sebastian/diff",
"installer": "composer",
"env": "dev"
},
"sebastian/environment": {
"install-path": "vendor/sebastian/environment",
"installer": "composer",
"env": "dev"
},
"sebastian/exporter": {
"install-path": "vendor/sebastian/exporter",
"installer": "composer",
"env": "dev"
},
"sebastian/global-state": {
"install-path": "vendor/sebastian/global-state",
"installer": "composer",
"env": "dev"
},
"sebastian/recursion-context": {
"install-path": "vendor/sebastian/recursion-context",
"installer": "composer",
"env": "dev"
},
"sebastian/version": {
"install-path": "vendor/sebastian/version",
"installer": "composer",
"env": "dev"
},
"seld/jsonlint": {
"install-path": "vendor/seld/jsonlint",
"installer": "composer",
"env": "dev"
},
"symfony/filesystem": {
"install-path": "vendor/symfony/filesystem",
"installer": "composer",
"env": "dev"
},
"symfony/process": {
"install-path": "vendor/symfony/process",
"installer": "composer",
"env": "dev"
},
"symfony/yaml": {
"install-path": "vendor/symfony/yaml",
"installer": "composer",
"env": "dev"
},
"th3n3rd/cartesian-product": {
"install-path": "vendor/th3n3rd/cartesian-product",
"installer": "composer",
"env": "dev"
},
"webmozart/assert": {
"install-path": "vendor/webmozart/assert",
"installer": "composer",
"env": "dev"
},
"webmozart/expression": {
"install-path": "vendor/webmozart/expression",
"installer": "composer",
"env": "dev"
},
"webmozart/glob": {
"install-path": "vendor/webmozart/glob",
"installer": "composer",
"env": "dev"
},
"webmozart/json": {
"install-path": "vendor/webmozart/json",
"installer": "composer",
"env": "dev"
},
"webmozart/path-util": {
"install-path": "vendor/webmozart/path-util",
"installer": "composer",
"env": "dev"
},
"zendframework/zend-diactoros": {
"install-path": "vendor/zendframework/zend-diactoros",
"installer": "composer",
"env": "dev"
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* PSR-7 compatible cURL based HTTP client.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
* @author Blake Williams <github@shabbyrobe.org>
*
* @api
*
* @since 1.0
*/
class Client implements HttpClient, HttpAsyncClient
{
/**
* cURL options.
*
* @var array
*/
private $options;
/**
* PSR-7 message factory.
*
* @var MessageFactory
*/
private $messageFactory;
/**
* PSR-7 stream factory.
*
* @var StreamFactory
*/
private $streamFactory;
/**
* cURL synchronous requests handle.
*
* @var resource|null
*/
private $handle = null;
/**
* Simultaneous requests runner.
*
* @var MultiRunner|null
*/
private $multiRunner = null;
/**
* Create new client.
*
* @param MessageFactory|null $messageFactory HTTP Message factory
* @param StreamFactory|null $streamFactory HTTP Stream factory
* @param array $options cURL options (see http://php.net/curl_setopt)
*
* @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed
*
* @since 1.0
*/
public function __construct(
MessageFactory $messageFactory = null,
StreamFactory $streamFactory = null,
array $options = []
) {
$this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find();
$this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
$this->options = $options;
}
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->handle)) {
curl_close($this->handle);
}
}
/**
* Sends a PSR-7 request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \Http\Client\Exception\NetworkException In case of network problems
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.6 Throw NetworkException on network errors
* @since 1.0
*/
public function sendRequest(RequestInterface $request)
{
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
if (is_resource($this->handle)) {
curl_reset($this->handle);
} else {
$this->handle = curl_init();
}
curl_setopt_array($this->handle, $options);
curl_exec($this->handle);
$errno = curl_errno($this->handle);
switch ($errno) {
case CURLE_OK:
// All OK, no actions needed.
break;
case CURLE_COULDNT_RESOLVE_PROXY:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_COULDNT_CONNECT:
case CURLE_OPERATION_TIMEOUTED:
case CURLE_SSL_CONNECT_ERROR:
throw new Exception\NetworkException(curl_error($this->handle), $request);
default:
throw new Exception\RequestException(curl_error($this->handle), $request);
}
$response = $responseBuilder->getResponse();
$response->getBody()->seek(0);
return $response;
}
/**
* Sends a PSR-7 request in an asynchronous way.
*
* @param RequestInterface $request
*
* @return Promise
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.0
*/
public function sendAsyncRequest(RequestInterface $request)
{
if (!$this->multiRunner instanceof MultiRunner) {
$this->multiRunner = new MultiRunner();
}
$handle = curl_init();
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
curl_setopt_array($handle, $options);
$core = new PromiseCore($request, $handle, $responseBuilder);
$promise = new CurlPromise($core, $this->multiRunner);
$this->multiRunner->add($core);
return $promise;
}
/**
* Generates cURL options.
*
* @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 createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder)
{
$options = $this->options;
$options[CURLOPT_HEADER] = false;
$options[CURLOPT_RETURNTRANSFER] = false;
$options[CURLOPT_FOLLOWLOCATION] = false;
try {
$options[CURLOPT_HTTP_VERSION]
= $this->getProtocolVersion($request->getProtocolVersion());
} catch (\UnexpectedValueException $e) {
throw new Exception\RequestException($e->getMessage(), $request);
}
$options[CURLOPT_URL] = (string) $request->getUri();
$options = $this->addRequestBodyOptions($request, $options);
$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);
if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}
$options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$str = trim($data);
if ('' !== $str) {
if (strpos(strtolower($str), 'http/') === 0) {
$responseBuilder->setStatus($str)->getResponse();
} else {
$responseBuilder->addHeader($str);
}
}
return strlen($data);
};
$options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
return $responseBuilder->getResponse()->getBody()->write($data);
};
return $options;
}
/**
* Return cURL constant for specified HTTP version.
*
* @param string $requestVersion
*
* @throws \UnexpectedValueException if unsupported version requested
*
* @return int
*/
private function getProtocolVersion($requestVersion)
{
switch ($requestVersion) {
case '1.0':
return CURL_HTTP_VERSION_1_0;
case '1.1':
return CURL_HTTP_VERSION_1_1;
case '2.0':
if (defined('CURL_HTTP_VERSION_2_0')) {
return CURL_HTTP_VERSION_2_0;
}
throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
}
return CURL_HTTP_VERSION_NONE;
}
/**
* Add request body related cURL options.
*
* @param RequestInterface $request
* @param array $options
*
* @return array
*/
private function addRequestBodyOptions(RequestInterface $request, array $options)
{
/*
* Some HTTP methods cannot have payload:
*
* - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or
* CURLOPT_POSTFIELDS.
* - HEAD — cURL treats HEAD as GET request with a same restrictions.
* - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
*/
if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) {
$body = $request->getBody();
$bodySize = $body->getSize();
if ($bodySize !== 0) {
if ($body->isSeekable()) {
$body->rewind();
}
// 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;
if (null !== $bodySize) {
$options[CURLOPT_INFILESIZE] = $bodySize;
}
$options[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;
}
}
}
if ($request->getMethod() === 'HEAD') {
// This will set HTTP method to "HEAD".
$options[CURLOPT_NOBODY] = true;
} elseif ($request->getMethod() !== 'GET') {
// GET is a default method. Other methods should be specified explicitly.
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}
return $options;
}
/**
* Create headers array for CURLOPT_HTTPHEADER.
*
* @param RequestInterface $request
* @param array $options cURL options
*
* @return string[]
*/
private function createHeaders(RequestInterface $request, array $options)
{
$curlHeaders = [];
$headers = $request->getHeaders();
foreach ($headers as $name => $values) {
$header = strtolower($name);
if ('expect' === $header) {
// curl-client does not support "Expect-Continue", so dropping "expect" headers
continue;
}
if ('content-length' === $header) {
if (array_key_exists(CURLOPT_POSTFIELDS, $options)) {
// Small body content length can be calculated here.
$values = [strlen($options[CURLOPT_POSTFIELDS])];
} elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) {
// Else if there is no body, forcing "Content-length" to 0
$values = [0];
}
}
foreach ($values as $value) {
$curlHeaders[] = $name.': '.$value;
}
}
/*
* curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default.
* We can not suppress it, but we can set it to empty.
*/
$curlHeaders[] = 'Expect:';
return $curlHeaders;
}
/**
* Create new ResponseBuilder instance.
*
* @return ResponseBuilder
*
* @throws \RuntimeException If creating the stream from $body fails
*/
private function createResponseBuilder()
{
try {
$body = $this->streamFactory->createStream(fopen('php://temp', 'w+b'));
} catch (\InvalidArgumentException $e) {
throw new \RuntimeException('Can not create "php://temp" stream.');
}
$response = $this->messageFactory->createResponse(200, null, [], $body);
return new ResponseBuilder($response);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Http\Client\Curl;
use Http\Promise\Promise;
/**
* Promise represents a response that may not be available yet, but will be resolved at some point
* in future. It acts like a proxy to the actual response.
*
* This interface is an extension of the promises/a+ specification https://promisesaplus.com/
* Value is replaced by an object where its class implement a Psr\Http\Message\RequestInterface.
* Reason is replaced by an object where its class implement a Http\Client\Exception.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class CurlPromise implements Promise
{
/**
* Shared promise core.
*
* @var PromiseCore
*/
private $core;
/**
* Requests runner.
*
* @var MultiRunner
*/
private $runner;
/**
* Create new promise.
*
* @param PromiseCore $core Shared promise core
* @param MultiRunner $runner Simultaneous requests runner
*/
public function __construct(PromiseCore $core, MultiRunner $runner)
{
$this->core = $core;
$this->runner = $runner;
}
/**
* Add behavior for when the promise is resolved or rejected.
*
* If you do not care about one of the cases, you can set the corresponding callable to null
* The callback will be called when the response or exception arrived and never more than once.
*
* @param callable $onFulfilled Called when a response will be available
* @param callable $onRejected Called when an error happens.
*
* You must always return the Response in the interface or throw an Exception
*
* @return Promise Always returns a new promise which is resolved with value of the executed
* callback (onFulfilled / onRejected)
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
if ($onFulfilled) {
$this->core->addOnFulfilled($onFulfilled);
}
if ($onRejected) {
$this->core->addOnRejected($onRejected);
}
return new self($this->core, $this->runner);
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->core->getState();
}
/**
* Wait for the promise to be fulfilled or rejected.
*
* When this method returns, the request has been resolved and the appropriate callable has terminated.
*
* When called with the unwrap option
*
* @param bool $unwrap Whether to return resolved value / throw reason or not
*
* @return \Psr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false
*
* @throws \Http\Client\Exception The rejection reason
*/
public function wait($unwrap = true)
{
$this->runner->wait($this->core);
if ($unwrap) {
if ($this->core->getState() === self::REJECTED) {
throw $this->core->getException();
}
return $this->core->getResponse();
}
return null;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception\RequestException;
/**
* Simultaneous requests runner.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class MultiRunner
{
/**
* cURL multi handle.
*
* @var resource|null
*/
private $multiHandle = null;
/**
* Awaiting cores.
*
* @var PromiseCore[]
*/
private $cores = [];
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
}
}
/**
* Add promise to runner.
*
* @param PromiseCore $core
*/
public function add(PromiseCore $core)
{
foreach ($this->cores as $existed) {
if ($existed === $core) {
return;
}
}
$this->cores[] = $core;
if (null === $this->multiHandle) {
$this->multiHandle = curl_multi_init();
}
curl_multi_add_handle($this->multiHandle, $core->getHandle());
}
/**
* Remove promise from runner.
*
* @param PromiseCore $core
*/
public function remove(PromiseCore $core)
{
foreach ($this->cores as $index => $existed) {
if ($existed === $core) {
curl_multi_remove_handle($this->multiHandle, $core->getHandle());
unset($this->cores[$index]);
return;
}
}
}
/**
* Wait for request(s) to be completed.
*
* @param PromiseCore|null $targetCore
*/
public function wait(PromiseCore $targetCore = null)
{
do {
$status = curl_multi_exec($this->multiHandle, $active);
$info = curl_multi_info_read($this->multiHandle);
if (false !== $info) {
$core = $this->findCoreByHandle($info['handle']);
if (null === $core) {
// We have no promise for this handle. Drop it.
curl_multi_remove_handle($this->multiHandle, $info['handle']);
continue;
}
if (CURLE_OK === $info['result']) {
$core->fulfill();
} else {
$error = curl_error($core->getHandle());
$core->reject(new RequestException($error, $core->getRequest()));
}
$this->remove($core);
// This is a promise we are waited for. So exiting wait().
if ($core === $targetCore) {
return;
}
}
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
}
/**
* Find core by handle.
*
* @param resource $handle
*
* @return PromiseCore|null
*/
private function findCoreByHandle($handle)
{
foreach ($this->cores as $core) {
if ($core->getHandle() === $handle) {
return $core;
}
}
return null;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Shared promises core.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class PromiseCore
{
/**
* HTTP request.
*
* @var RequestInterface
*/
private $request;
/**
* cURL handle.
*
* @var resource
*/
private $handle;
/**
* Response builder.
*
* @var ResponseBuilder
*/
private $responseBuilder;
/**
* Promise state.
*
* @var string
*/
private $state;
/**
* Exception.
*
* @var Exception|null
*/
private $exception = null;
/**
* Functions to call when a response will be available.
*
* @var callable[]
*/
private $onFulfilled = [];
/**
* Functions to call when an error happens.
*
* @var callable[]
*/
private $onRejected = [];
/**
* Create shared core.
*
* @param RequestInterface $request HTTP request.
* @param resource $handle cURL handle.
* @param ResponseBuilder $responseBuilder Response builder.
*
* @throws \InvalidArgumentException If $handle is not a cURL resource.
*/
public function __construct(
RequestInterface $request,
$handle,
ResponseBuilder $responseBuilder
) {
if (!is_resource($handle)) {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s given',
gettype($handle)
)
);
}
if (get_resource_type($handle) !== 'curl') {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s resource given',
get_resource_type($handle)
)
);
}
$this->request = $request;
$this->handle = $handle;
$this->responseBuilder = $responseBuilder;
$this->state = Promise::PENDING;
}
/**
* Add on fulfilled callback.
*
* @param callable $callback
*/
public function addOnFulfilled(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onFulfilled[] = $callback;
} elseif ($this->getState() === Promise::FULFILLED) {
$response = call_user_func($callback, $this->responseBuilder->getResponse());
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
}
/**
* Add on rejected callback.
*
* @param callable $callback
*/
public function addOnRejected(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onRejected[] = $callback;
} elseif ($this->getState() === Promise::REJECTED) {
$this->exception = call_user_func($callback, $this->exception);
}
}
/**
* Return cURL handle.
*
* @return resource
*/
public function getHandle()
{
return $this->handle;
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* Return request.
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
/**
* Return the value of the promise (fulfilled).
*
* @return ResponseInterface Response Object only when the Promise is fulfilled
*/
public function getResponse()
{
return $this->responseBuilder->getResponse();
}
/**
* Get the reason why the promise was rejected.
*
* If the exception is an instance of Http\Client\Exception\HttpException it will contain
* the response object with the status code and the http reason.
*
* @return Exception Exception Object only when the Promise is rejected
*
* @throws \LogicException When the promise is not rejected
*/
public function getException()
{
if (null === $this->exception) {
throw new \LogicException('Promise is not rejected');
}
return $this->exception;
}
/**
* Fulfill promise.
*/
public function fulfill()
{
$this->state = Promise::FULFILLED;
$response = $this->responseBuilder->getResponse();
try {
$response->getBody()->seek(0);
} catch (\RuntimeException $e) {
$exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e);
$this->reject($exception);
return;
}
while (count($this->onFulfilled) > 0) {
$callback = array_shift($this->onFulfilled);
$response = call_user_func($callback, $response);
}
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
/**
* Reject promise.
*
* @param Exception $exception Reject reason
*/
public function reject(Exception $exception)
{
$this->exception = $exception;
$this->state = Promise::REJECTED;
while (count($this->onRejected) > 0) {
$callback = array_shift($this->onRejected);
try {
$exception = call_user_func($callback, $this->exception);
$this->exception = $exception;
} catch (Exception $exception) {
$this->exception = $exception;
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Http\Client\Curl;
use Http\Message\Builder\ResponseBuilder as OriginalResponseBuilder;
use Psr\Http\Message\ResponseInterface;
/**
* Extended response builder.
*/
class ResponseBuilder extends OriginalResponseBuilder
{
/**
* Replace response with a new instance.
*
* @param ResponseInterface $response
*/
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
}