Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,74 @@
<?php
namespace GuzzleHttp\Ring\Client;
/**
* Client specific utility functions.
*/
class ClientUtils
{
/**
* Returns the default cacert bundle for the current system.
*
* First, the openssl.cafile and curl.cainfo php.ini settings are checked.
* If those settings are not configured, then the common locations for
* bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X
* and Windows are checked. If any of these file locations are found on
* disk, they will be utilized.
*
* Note: the result of this function is cached for subsequent calls.
*
* @return string
* @throws \RuntimeException if no bundle can be found.
*/
public static function getDefaultCaBundle()
{
static $cached = null;
static $cafiles = [
// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
'/etc/pki/tls/certs/ca-bundle.crt',
// Ubuntu, Debian (provided by the ca-certificates package)
'/etc/ssl/certs/ca-certificates.crt',
// FreeBSD (provided by the ca_root_nss package)
'/usr/local/share/certs/ca-root-nss.crt',
// OS X provided by homebrew (using the default path)
'/usr/local/etc/openssl/cert.pem',
// Windows?
'C:\\windows\\system32\\curl-ca-bundle.crt',
'C:\\windows\\curl-ca-bundle.crt',
];
if ($cached) {
return $cached;
}
if ($ca = ini_get('openssl.cafile')) {
return $cached = $ca;
}
if ($ca = ini_get('curl.cainfo')) {
return $cached = $ca;
}
foreach ($cafiles as $filename) {
if (file_exists($filename)) {
return $cached = $filename;
}
}
throw new \RuntimeException(self::CA_ERR);
}
const CA_ERR = "
No system CA bundle could be found in any of the the common system locations.
PHP versions earlier than 5.6 are not properly configured to use the system's
CA bundle by default. In order to verify peer certificates, you will need to
supply the path on disk to a certificate bundle to the 'verify' request
option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not
need a specific certificate bundle, then Mozilla provides a commonly used CA
bundle which can be downloaded here (provided by the maintainer of cURL):
https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt. Once
you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP
ini setting to point to the path to the file, allowing you to omit the 'verify'
request option. See http://curl.haxx.se/docs/sslcerts.html for more
information.";
}

View file

@ -0,0 +1,546 @@
<?php
namespace GuzzleHttp\Ring\Client;
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Exception\ConnectException;
use GuzzleHttp\Ring\Exception\RingException;
use GuzzleHttp\Stream\LazyOpenStream;
use GuzzleHttp\Stream\StreamInterface;
/**
* Creates curl resources from a request
*/
class CurlFactory
{
/**
* Creates a cURL handle, header resource, and body resource based on a
* transaction.
*
* @param array $request Request hash
* @param null|resource $handle Optionally provide a curl handle to modify
*
* @return array Returns an array of the curl handle, headers array, and
* response body handle.
* @throws \RuntimeException when an option cannot be applied
*/
public function __invoke(array $request, $handle = null)
{
$headers = [];
$options = $this->getDefaultOptions($request, $headers);
$this->applyMethod($request, $options);
if (isset($request['client'])) {
$this->applyHandlerOptions($request, $options);
}
$this->applyHeaders($request, $options);
unset($options['_headers']);
// Add handler options from the request's configuration options
if (isset($request['client']['curl'])) {
$options = $this->applyCustomCurlOptions(
$request['client']['curl'],
$options
);
}
if (!$handle) {
$handle = curl_init();
}
$body = $this->getOutputBody($request, $options);
curl_setopt_array($handle, $options);
return [$handle, &$headers, $body];
}
/**
* Creates a response hash from a cURL result.
*
* @param callable $handler Handler that was used.
* @param array $request Request that sent.
* @param array $response Response hash to update.
* @param array $headers Headers received during transfer.
* @param resource $body Body fopen response.
*
* @return array
*/
public static function createResponse(
callable $handler,
array $request,
array $response,
array $headers,
$body
) {
if (isset($response['transfer_stats']['url'])) {
$response['effective_url'] = $response['transfer_stats']['url'];
}
if (!empty($headers)) {
$startLine = explode(' ', array_shift($headers), 3);
$headerList = Core::headersFromLines($headers);
$response['headers'] = $headerList;
$response['status'] = isset($startLine[1]) ? (int) $startLine[1] : null;
$response['reason'] = isset($startLine[2]) ? $startLine[2] : null;
$response['body'] = $body;
Core::rewindBody($response);
}
return !empty($response['curl']['errno']) || !isset($response['status'])
? self::createErrorResponse($handler, $request, $response)
: $response;
}
private static function createErrorResponse(
callable $handler,
array $request,
array $response
) {
static $connectionErrors = [
CURLE_OPERATION_TIMEOUTED => true,
CURLE_COULDNT_RESOLVE_HOST => true,
CURLE_COULDNT_CONNECT => true,
CURLE_SSL_CONNECT_ERROR => true,
CURLE_GOT_NOTHING => true,
];
// Retry when nothing is present or when curl failed to rewind.
if (!isset($response['err_message'])
&& (empty($response['curl']['errno'])
|| $response['curl']['errno'] == 65)
) {
return self::retryFailedRewind($handler, $request, $response);
}
$message = isset($response['err_message'])
? $response['err_message']
: sprintf('cURL error %s: %s',
$response['curl']['errno'],
isset($response['curl']['error'])
? $response['curl']['error']
: 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html');
$error = isset($response['curl']['errno'])
&& isset($connectionErrors[$response['curl']['errno']])
? new ConnectException($message)
: new RingException($message);
return $response + [
'status' => null,
'reason' => null,
'body' => null,
'headers' => [],
'error' => $error,
];
}
private function getOutputBody(array $request, array &$options)
{
// Determine where the body of the response (if any) will be streamed.
if (isset($options[CURLOPT_WRITEFUNCTION])) {
return $request['client']['save_to'];
}
if (isset($options[CURLOPT_FILE])) {
return $options[CURLOPT_FILE];
}
if ($request['http_method'] != 'HEAD') {
// Create a default body if one was not provided
return $options[CURLOPT_FILE] = fopen('php://temp', 'w+');
}
return null;
}
private function getDefaultOptions(array $request, array &$headers)
{
$url = Core::url($request);
$startingResponse = false;
$options = [
'_headers' => $request['headers'],
CURLOPT_CUSTOMREQUEST => $request['http_method'],
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 150,
CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$headers, &$startingResponse) {
$value = trim($h);
if ($value === '') {
$startingResponse = true;
} elseif ($startingResponse) {
$startingResponse = false;
$headers = [$value];
} else {
$headers[] = $value;
}
return strlen($h);
},
];
if (isset($request['version'])) {
$options[CURLOPT_HTTP_VERSION] = $request['version'] == 1.1 ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0;
}
if (defined('CURLOPT_PROTOCOLS')) {
$options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
}
return $options;
}
private function applyMethod(array $request, array &$options)
{
if (isset($request['body'])) {
$this->applyBody($request, $options);
return;
}
switch ($request['http_method']) {
case 'PUT':
case 'POST':
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
if (!Core::hasHeader($request, 'Content-Length')) {
$options[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
}
break;
case 'HEAD':
$options[CURLOPT_NOBODY] = true;
unset(
$options[CURLOPT_WRITEFUNCTION],
$options[CURLOPT_READFUNCTION],
$options[CURLOPT_FILE],
$options[CURLOPT_INFILE]
);
}
}
private function applyBody(array $request, array &$options)
{
$contentLength = Core::firstHeader($request, 'Content-Length');
$size = $contentLength !== null ? (int) $contentLength : null;
// Send the body as a string if the size is less than 1MB OR if the
// [client][curl][body_as_string] request value is set.
if (($size !== null && $size < 1000000) ||
isset($request['client']['curl']['body_as_string']) ||
is_string($request['body'])
) {
$options[CURLOPT_POSTFIELDS] = Core::body($request);
// Don't duplicate the Content-Length header
$this->removeHeader('Content-Length', $options);
$this->removeHeader('Transfer-Encoding', $options);
} else {
$options[CURLOPT_UPLOAD] = true;
if ($size !== null) {
// Let cURL handle setting the Content-Length header
$options[CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $options);
}
$this->addStreamingBody($request, $options);
}
// If the Expect header is not present, prevent curl from adding it
if (!Core::hasHeader($request, 'Expect')) {
$options[CURLOPT_HTTPHEADER][] = 'Expect:';
}
// cURL sometimes adds a content-type by default. Prevent this.
if (!Core::hasHeader($request, 'Content-Type')) {
$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
}
}
private function addStreamingBody(array $request, array &$options)
{
$body = $request['body'];
if ($body instanceof StreamInterface) {
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return (string) $body->read($length);
};
if (!isset($options[CURLOPT_INFILESIZE])) {
if ($size = $body->getSize()) {
$options[CURLOPT_INFILESIZE] = $size;
}
}
} elseif (is_resource($body)) {
$options[CURLOPT_INFILE] = $body;
} elseif ($body instanceof \Iterator) {
$buf = '';
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, &$buf) {
if ($body->valid()) {
$buf .= $body->current();
$body->next();
}
$result = (string) substr($buf, 0, $length);
$buf = substr($buf, $length);
return $result;
};
} else {
throw new \InvalidArgumentException('Invalid request body provided');
}
}
private function applyHeaders(array $request, array &$options)
{
foreach ($options['_headers'] as $name => $values) {
foreach ($values as $value) {
$options[CURLOPT_HTTPHEADER][] = "$name: $value";
}
}
// Remove the Accept header if one was not set
if (!Core::hasHeader($request, 'Accept')) {
$options[CURLOPT_HTTPHEADER][] = 'Accept:';
}
}
/**
* Takes an array of curl options specified in the 'curl' option of a
* request's configuration array and maps them to CURLOPT_* options.
*
* This method is only called when a request has a 'curl' config setting.
*
* @param array $config Configuration array of custom curl option
* @param array $options Array of existing curl options
*
* @return array Returns a new array of curl options
*/
private function applyCustomCurlOptions(array $config, array $options)
{
$curlOptions = [];
foreach ($config as $key => $value) {
if (is_int($key)) {
$curlOptions[$key] = $value;
}
}
return $curlOptions + $options;
}
/**
* Remove a header from the options array.
*
* @param string $name Case-insensitive header to remove
* @param array $options Array of options to modify
*/
private function removeHeader($name, array &$options)
{
foreach (array_keys($options['_headers']) as $key) {
if (!strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
return;
}
}
}
/**
* Applies an array of request client options to a the options array.
*
* This method uses a large switch rather than double-dispatch to save on
* high overhead of calling functions in PHP.
*/
private function applyHandlerOptions(array $request, array &$options)
{
foreach ($request['client'] as $key => $value) {
switch ($key) {
// Violating PSR-4 to provide more room.
case 'verify':
if ($value === false) {
unset($options[CURLOPT_CAINFO]);
$options[CURLOPT_SSL_VERIFYHOST] = 0;
$options[CURLOPT_SSL_VERIFYPEER] = false;
continue;
}
$options[CURLOPT_SSL_VERIFYHOST] = 2;
$options[CURLOPT_SSL_VERIFYPEER] = true;
if (is_string($value)) {
$options[CURLOPT_CAINFO] = $value;
if (!file_exists($value)) {
throw new \InvalidArgumentException(
"SSL CA bundle not found: $value"
);
}
}
break;
case 'decode_content':
if ($value === false) {
continue;
}
$accept = Core::firstHeader($request, 'Accept-Encoding');
if ($accept) {
$options[CURLOPT_ENCODING] = $accept;
} else {
$options[CURLOPT_ENCODING] = '';
// Don't let curl send the header over the wire
$options[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
}
break;
case 'save_to':
if (is_string($value)) {
$value = new LazyOpenStream($value, 'w+');
}
if ($value instanceof StreamInterface) {
$options[CURLOPT_WRITEFUNCTION] =
function ($ch, $write) use ($value) {
return $value->write($write);
};
} elseif (is_resource($value)) {
$options[CURLOPT_FILE] = $value;
} else {
throw new \InvalidArgumentException('save_to must be a '
. 'GuzzleHttp\Stream\StreamInterface or resource');
}
break;
case 'timeout':
if (defined('CURLOPT_TIMEOUT_MS')) {
$options[CURLOPT_TIMEOUT_MS] = $value * 1000;
} else {
$options[CURLOPT_TIMEOUT] = $value;
}
break;
case 'connect_timeout':
if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
$options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000;
} else {
$options[CURLOPT_CONNECTTIMEOUT] = $value;
}
break;
case 'proxy':
if (!is_array($value)) {
$options[CURLOPT_PROXY] = $value;
} elseif (isset($request['scheme'])) {
$scheme = $request['scheme'];
if (isset($value[$scheme])) {
$options[CURLOPT_PROXY] = $value[$scheme];
}
}
break;
case 'cert':
if (is_array($value)) {
$options[CURLOPT_SSLCERTPASSWD] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$value}"
);
}
$options[CURLOPT_SSLCERT] = $value;
break;
case 'ssl_key':
if (is_array($value)) {
$options[CURLOPT_SSLKEYPASSWD] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new \InvalidArgumentException(
"SSL private key not found: {$value}"
);
}
$options[CURLOPT_SSLKEY] = $value;
break;
case 'progress':
if (!is_callable($value)) {
throw new \InvalidArgumentException(
'progress client option must be callable'
);
}
$options[CURLOPT_NOPROGRESS] = false;
$options[CURLOPT_PROGRESSFUNCTION] =
function () use ($value) {
$args = func_get_args();
// PHP 5.5 pushed the handle onto the start of the args
if (is_resource($args[0])) {
array_shift($args);
}
call_user_func_array($value, $args);
};
break;
case 'debug':
if ($value) {
$options[CURLOPT_STDERR] = Core::getDebugResource($value);
$options[CURLOPT_VERBOSE] = true;
}
break;
}
}
}
/**
* This function ensures that a response was set on a transaction. If one
* was not set, then the request is retried if possible. This error
* typically means you are sending a payload, curl encountered a
* "Connection died, retrying a fresh connect" error, tried to rewind the
* stream, and then encountered a "necessary data rewind wasn't possible"
* error, causing the request to be sent through curl_multi_info_read()
* without an error status.
*/
private static function retryFailedRewind(
callable $handler,
array $request,
array $response
) {
// If there is no body, then there is some other kind of issue. This
// is weird and should probably never happen.
if (!isset($request['body'])) {
$response['err_message'] = 'No response was received for a request '
. 'with no body. This could mean that you are saturating your '
. 'network.';
return self::createErrorResponse($handler, $request, $response);
}
if (!Core::rewindBody($request)) {
$response['err_message'] = 'The connection unexpectedly failed '
. 'without providing an error. The request would have been '
. 'retried, but attempting to rewind the request body failed.';
return self::createErrorResponse($handler, $request, $response);
}
// Retry no more than 3 times before giving up.
if (!isset($request['curl']['retries'])) {
$request['curl']['retries'] = 1;
} elseif ($request['curl']['retries'] == 2) {
$response['err_message'] = 'The cURL request was retried 3 times '
. 'and did no succeed. cURL was unable to rewind the body of '
. 'the request and subsequent retries resulted in the same '
. 'error. Turn on the debug option to see what went wrong. '
. 'See https://bugs.php.net/bug.php?id=47204 for more information.';
return self::createErrorResponse($handler, $request, $response);
} else {
$request['curl']['retries']++;
}
return $handler($request);
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace GuzzleHttp\Ring\Client;
use GuzzleHttp\Ring\Future\CompletedFutureArray;
use GuzzleHttp\Ring\Core;
/**
* HTTP handler that uses cURL easy handles as a transport layer.
*
* Requires PHP 5.5+
*
* When using the CurlHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*/
class CurlHandler
{
/** @var callable */
private $factory;
/** @var array Array of curl easy handles */
private $handles = [];
/** @var array Array of owned curl easy handles */
private $ownedHandles = [];
/** @var int Total number of idle handles to keep in cache */
private $maxHandles;
/**
* Accepts an associative array of options:
*
* - factory: Optional callable factory used to create cURL handles.
* The callable is passed a request hash when invoked, and returns an
* array of the curl handle, headers resource, and body resource.
* - max_handles: Maximum number of idle handles (defaults to 5).
*
* @param array $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
$this->handles = $this->ownedHandles = [];
$this->factory = isset($options['handle_factory'])
? $options['handle_factory']
: new CurlFactory();
$this->maxHandles = isset($options['max_handles'])
? $options['max_handles']
: 5;
}
public function __destruct()
{
foreach ($this->handles as $handle) {
if (is_resource($handle)) {
curl_close($handle);
}
}
}
public function __invoke(array $request)
{
$factory = $this->factory;
// Ensure headers are by reference. They're updated elsewhere.
$result = $factory($request, $this->checkoutEasyHandle());
$h = $result[0];
$hd =& $result[1];
$bd = $result[2];
Core::doSleep($request);
curl_exec($h);
$response = ['transfer_stats' => curl_getinfo($h)];
$response['curl']['error'] = curl_error($h);
$response['curl']['errno'] = curl_errno($h);
$response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']);
$this->releaseEasyHandle($h);
return new CompletedFutureArray(
CurlFactory::createResponse($this, $request, $response, $hd, $bd)
);
}
private function checkoutEasyHandle()
{
// Find an unused handle in the cache
if (false !== ($key = array_search(false, $this->ownedHandles, true))) {
$this->ownedHandles[$key] = true;
return $this->handles[$key];
}
// Add a new handle
$handle = curl_init();
$id = (int) $handle;
$this->handles[$id] = $handle;
$this->ownedHandles[$id] = true;
return $handle;
}
private function releaseEasyHandle($handle)
{
$id = (int) $handle;
if (count($this->ownedHandles) > $this->maxHandles) {
curl_close($this->handles[$id]);
unset($this->handles[$id], $this->ownedHandles[$id]);
} else {
// curl_reset doesn't clear these out for some reason
static $unsetValues = [
CURLOPT_HEADERFUNCTION => null,
CURLOPT_WRITEFUNCTION => null,
CURLOPT_READFUNCTION => null,
CURLOPT_PROGRESSFUNCTION => null,
];
curl_setopt_array($handle, $unsetValues);
curl_reset($handle);
$this->ownedHandles[$id] = false;
}
}
}

View file

@ -0,0 +1,250 @@
<?php
namespace GuzzleHttp\Ring\Client;
use GuzzleHttp\Ring\Future\FutureArray;
use React\Promise\Deferred;
/**
* Returns an asynchronous response using curl_multi_* functions.
*
* This handler supports future responses and the "delay" request client
* option that can be used to delay before sending a request.
*
* When using the CurlMultiHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*
* @property resource $_mh Internal use only. Lazy loaded multi-handle.
*/
class CurlMultiHandler
{
/** @var callable */
private $factory;
private $selectTimeout;
private $active;
private $handles = [];
private $delays = [];
private $maxHandles;
/**
* This handler accepts the following options:
*
* - mh: An optional curl_multi resource
* - handle_factory: An optional callable used to generate curl handle
* resources. the callable accepts a request hash and returns an array
* of the handle, headers file resource, and the body resource.
* - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second.
* - max_handles: Optional integer representing the maximum number of
* open requests. When this number is reached, the queued futures are
* flushed.
*
* @param array $options
*/
public function __construct(array $options = [])
{
if (isset($options['mh'])) {
$this->_mh = $options['mh'];
}
$this->factory = isset($options['handle_factory'])
? $options['handle_factory'] : new CurlFactory();
$this->selectTimeout = isset($options['select_timeout'])
? $options['select_timeout'] : 1;
$this->maxHandles = isset($options['max_handles'])
? $options['max_handles'] : 100;
}
public function __get($name)
{
if ($name === '_mh') {
return $this->_mh = curl_multi_init();
}
throw new \BadMethodCallException();
}
public function __destruct()
{
// Finish any open connections before terminating the script.
if ($this->handles) {
$this->execute();
}
if (isset($this->_mh)) {
curl_multi_close($this->_mh);
unset($this->_mh);
}
}
public function __invoke(array $request)
{
$factory = $this->factory;
$result = $factory($request);
$entry = [
'request' => $request,
'response' => [],
'handle' => $result[0],
'headers' => &$result[1],
'body' => $result[2],
'deferred' => new Deferred(),
];
$id = (int) $result[0];
$future = new FutureArray(
$entry['deferred']->promise(),
[$this, 'execute'],
function () use ($id) {
return $this->cancel($id);
}
);
$this->addRequest($entry);
// Transfer outstanding requests if there are too many open handles.
if (count($this->handles) >= $this->maxHandles) {
$this->execute();
}
return $future;
}
/**
* Runs until all outstanding connections have completed.
*/
public function execute()
{
do {
if ($this->active &&
curl_multi_select($this->_mh, $this->selectTimeout) === -1
) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
// Add any delayed futures if needed.
if ($this->delays) {
$this->addDelays();
}
do {
$mrc = curl_multi_exec($this->_mh, $this->active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
// If there are delays but no transfers, then sleep for a bit.
if (!$this->active && $this->delays) {
usleep(500);
}
} while ($this->active || $this->handles);
}
private function addRequest(array &$entry)
{
$id = (int) $entry['handle'];
$this->handles[$id] = $entry;
// If the request is a delay, then add the reques to the curl multi
// pool only after the specified delay.
if (isset($entry['request']['client']['delay'])) {
$this->delays[$id] = microtime(true) + ($entry['request']['client']['delay'] / 1000);
} elseif (empty($entry['request']['future'])) {
curl_multi_add_handle($this->_mh, $entry['handle']);
} else {
curl_multi_add_handle($this->_mh, $entry['handle']);
// "lazy" futures are only sent once the pool has many requests.
if ($entry['request']['future'] !== 'lazy') {
do {
$mrc = curl_multi_exec($this->_mh, $this->active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
}
}
private function removeProcessed($id)
{
if (isset($this->handles[$id])) {
curl_multi_remove_handle(
$this->_mh,
$this->handles[$id]['handle']
);
curl_close($this->handles[$id]['handle']);
unset($this->handles[$id], $this->delays[$id]);
}
}
/**
* Cancels a handle from sending and removes references to it.
*
* @param int $id Handle ID to cancel and remove.
*
* @return bool True on success, false on failure.
*/
private function cancel($id)
{
// Cannot cancel if it has been processed.
if (!isset($this->handles[$id])) {
return false;
}
$handle = $this->handles[$id]['handle'];
unset($this->delays[$id], $this->handles[$id]);
curl_multi_remove_handle($this->_mh, $handle);
curl_close($handle);
return true;
}
private function addDelays()
{
$currentTime = microtime(true);
foreach ($this->delays as $id => $delay) {
if ($currentTime >= $delay) {
unset($this->delays[$id]);
curl_multi_add_handle(
$this->_mh,
$this->handles[$id]['handle']
);
}
}
}
private function processMessages()
{
while ($done = curl_multi_info_read($this->_mh)) {
$id = (int) $done['handle'];
if (!isset($this->handles[$id])) {
// Probably was cancelled.
continue;
}
$entry = $this->handles[$id];
$entry['response']['transfer_stats'] = curl_getinfo($done['handle']);
if ($done['result'] !== CURLM_OK) {
$entry['response']['curl']['errno'] = $done['result'];
if (function_exists('curl_strerror')) {
$entry['response']['curl']['error'] = curl_strerror($done['result']);
}
}
$result = CurlFactory::createResponse(
$this,
$entry['request'],
$entry['response'],
$entry['headers'],
$entry['body']
);
$this->removeProcessed($id);
$entry['deferred']->resolve($result);
}
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace GuzzleHttp\Ring\Client;
/**
* Provides basic middleware wrappers.
*
* If a middleware is more complex than a few lines of code, then it should
* be implemented in a class rather than a static method.
*/
class Middleware
{
/**
* Sends future requests to a future compatible handler while sending all
* other requests to a default handler.
*
* When the "future" option is not provided on a request, any future responses
* are automatically converted to synchronous responses and block.
*
* @param callable $default Handler used for non-streaming responses
* @param callable $future Handler used for future responses
*
* @return callable Returns the composed handler.
*/
public static function wrapFuture(
callable $default,
callable $future
) {
return function (array $request) use ($default, $future) {
return empty($request['client']['future'])
? $default($request)
: $future($request);
};
}
/**
* Sends streaming requests to a streaming compatible handler while sendin
* all other requests to a default handler.
*
* This, for example, could be useful for taking advantage of the
* performance benefits of curl while still supporting true streaming
* through the StreamHandler.
*
* @param callable $default Handler used for non-streaming responses
* @param callable $streaming Handler used for streaming responses
*
* @return callable Returns the composed handler.
*/
public static function wrapStreaming(
callable $default,
callable $streaming
) {
return function (array $request) use ($default, $streaming) {
return empty($request['client']['stream'])
? $default($request)
: $streaming($request);
};
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace GuzzleHttp\Ring\Client;
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Future\CompletedFutureArray;
use GuzzleHttp\Ring\Future\FutureArrayInterface;
/**
* Ring handler that returns a canned response or evaluated function result.
*/
class MockHandler
{
/** @var callable|array|FutureArrayInterface */
private $result;
/**
* Provide an array or future to always return the same value. Provide a
* callable that accepts a request object and returns an array or future
* to dynamically create a response.
*
* @param array|FutureArrayInterface|callable $result Mock return value.
*/
public function __construct($result)
{
$this->result = $result;
}
public function __invoke(array $request)
{
Core::doSleep($request);
$response = is_callable($this->result)
? call_user_func($this->result, $request)
: $this->result;
if (is_array($response)) {
$response = new CompletedFutureArray($response + [
'status' => null,
'body' => null,
'headers' => [],
'reason' => null,
'effective_url' => null,
]);
} elseif (!$response instanceof FutureArrayInterface) {
throw new \InvalidArgumentException(
'Response must be an array or FutureArrayInterface. Found '
. Core::describeType($request)
);
}
return $response;
}
}

View file

@ -0,0 +1,412 @@
<?php
namespace GuzzleHttp\Ring\Client;
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Exception\ConnectException;
use GuzzleHttp\Ring\Exception\RingException;
use GuzzleHttp\Ring\Future\CompletedFutureArray;
use GuzzleHttp\Stream\InflateStream;
use GuzzleHttp\Stream\StreamInterface;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\Utils;
/**
* RingPHP client handler that uses PHP's HTTP stream wrapper.
*/
class StreamHandler
{
private $options;
private $lastHeaders;
public function __construct(array $options = [])
{
$this->options = $options;
}
public function __invoke(array $request)
{
$url = Core::url($request);
Core::doSleep($request);
try {
// Does not support the expect header.
$request = Core::removeHeader($request, 'Expect');
$stream = $this->createStream($url, $request);
return $this->createResponse($request, $url, $stream);
} catch (RingException $e) {
return $this->createErrorResponse($url, $e);
}
}
private function createResponse(array $request, $url, $stream)
{
$hdrs = $this->lastHeaders;
$this->lastHeaders = null;
$parts = explode(' ', array_shift($hdrs), 3);
$response = [
'status' => $parts[1],
'reason' => isset($parts[2]) ? $parts[2] : null,
'headers' => Core::headersFromLines($hdrs),
'effective_url' => $url,
];
$stream = $this->checkDecode($request, $response, $stream);
// If not streaming, then drain the response into a stream.
if (empty($request['client']['stream'])) {
$dest = isset($request['client']['save_to'])
? $request['client']['save_to']
: fopen('php://temp', 'r+');
$stream = $this->drain($stream, $dest);
}
$response['body'] = $stream;
return new CompletedFutureArray($response);
}
private function checkDecode(array $request, array $response, $stream)
{
// Automatically decode responses when instructed.
if (!empty($request['client']['decode_content'])) {
switch (Core::firstHeader($response, 'Content-Encoding', true)) {
case 'gzip':
case 'deflate':
$stream = new InflateStream(Stream::factory($stream));
break;
}
}
return $stream;
}
/**
* Drains the stream into the "save_to" client option.
*
* @param resource $stream
* @param string|resource|StreamInterface $dest
*
* @return Stream
* @throws \RuntimeException when the save_to option is invalid.
*/
private function drain($stream, $dest)
{
if (is_resource($stream)) {
if (!is_resource($dest)) {
$stream = Stream::factory($stream);
} else {
stream_copy_to_stream($stream, $dest);
fclose($stream);
rewind($dest);
return $dest;
}
}
// Stream the response into the destination stream
$dest = is_string($dest)
? new Stream(Utils::open($dest, 'r+'))
: Stream::factory($dest);
Utils::copyToStream($stream, $dest);
$dest->seek(0);
$stream->close();
return $dest;
}
/**
* Creates an error response for the given stream.
*
* @param string $url
* @param RingException $e
*
* @return array
*/
private function createErrorResponse($url, RingException $e)
{
// Determine if the error was a networking error.
$message = $e->getMessage();
// This list can probably get more comprehensive.
if (strpos($message, 'getaddrinfo') // DNS lookup failed
|| strpos($message, 'Connection refused')
) {
$e = new ConnectException($e->getMessage(), 0, $e);
}
return new CompletedFutureArray([
'status' => null,
'body' => null,
'headers' => [],
'effective_url' => $url,
'error' => $e
]);
}
/**
* Create a resource and check to ensure it was created successfully
*
* @param callable $callback Callable that returns stream resource
*
* @return resource
* @throws \RuntimeException on error
*/
private function createResource(callable $callback)
{
$errors = null;
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
$errors[] = [
'message' => $msg,
'file' => $file,
'line' => $line
];
return true;
});
$resource = $callback();
restore_error_handler();
if (!$resource) {
$message = 'Error creating resource: ';
foreach ($errors as $err) {
foreach ($err as $key => $value) {
$message .= "[$key] $value" . PHP_EOL;
}
}
throw new RingException(trim($message));
}
return $resource;
}
private function createStream($url, array $request)
{
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
// HTTP/1.1 streams using the PHP stream wrapper require a
// Connection: close header
if ((!isset($request['version']) || $request['version'] == '1.1')
&& !Core::hasHeader($request, 'Connection')
) {
$request['headers']['Connection'] = ['close'];
}
// Ensure SSL is verified by default
if (!isset($request['client']['verify'])) {
$request['client']['verify'] = true;
}
$params = [];
$options = $this->getDefaultOptions($request);
if (isset($request['client'])) {
foreach ($request['client'] as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $options, $value, $params);
}
}
}
return $this->createStreamResource(
$url,
$request,
$options,
$this->createContext($request, $options, $params)
);
}
private function getDefaultOptions(array $request)
{
$headers = "";
foreach ($request['headers'] as $name => $value) {
foreach ((array) $value as $val) {
$headers .= "$name: $val\r\n";
}
}
$context = [
'http' => [
'method' => $request['http_method'],
'header' => $headers,
'protocol_version' => isset($request['version']) ? $request['version'] : 1.1,
'ignore_errors' => true,
'follow_location' => 0,
],
];
$body = Core::body($request);
if (isset($body)) {
$context['http']['content'] = $body;
// Prevent the HTTP handler from adding a Content-Type header.
if (!Core::hasHeader($request, 'Content-Type')) {
$context['http']['header'] .= "Content-Type:\r\n";
}
}
$context['http']['header'] = rtrim($context['http']['header']);
return $context;
}
private function add_proxy(array $request, &$options, $value, &$params)
{
if (!is_array($value)) {
$options['http']['proxy'] = $value;
} else {
$scheme = isset($request['scheme']) ? $request['scheme'] : 'http';
if (isset($value[$scheme])) {
$options['http']['proxy'] = $value[$scheme];
}
}
}
private function add_timeout(array $request, &$options, $value, &$params)
{
$options['http']['timeout'] = $value;
}
private function add_verify(array $request, &$options, $value, &$params)
{
if ($value === true) {
// PHP 5.6 or greater will find the system cert by default. When
// < 5.6, use the Guzzle bundled cacert.
if (PHP_VERSION_ID < 50600) {
$options['ssl']['cafile'] = ClientUtils::getDefaultCaBundle();
}
} elseif (is_string($value)) {
$options['ssl']['cafile'] = $value;
if (!file_exists($value)) {
throw new RingException("SSL CA bundle not found: $value");
}
} elseif ($value === false) {
$options['ssl']['verify_peer'] = false;
return;
} else {
throw new RingException('Invalid verify request option');
}
$options['ssl']['verify_peer'] = true;
$options['ssl']['allow_self_signed'] = true;
}
private function add_cert(array $request, &$options, $value, &$params)
{
if (is_array($value)) {
$options['ssl']['passphrase'] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new RingException("SSL certificate not found: {$value}");
}
$options['ssl']['local_cert'] = $value;
}
private function add_progress(array $request, &$options, $value, &$params)
{
$fn = function ($code, $_1, $_2, $_3, $transferred, $total) use ($value) {
if ($code == STREAM_NOTIFY_PROGRESS) {
$value($total, $transferred, null, null);
}
};
// Wrap the existing function if needed.
$params['notification'] = isset($params['notification'])
? Core::callArray([$params['notification'], $fn])
: $fn;
}
private function add_debug(array $request, &$options, $value, &$params)
{
if ($value === false) {
return;
}
static $map = [
STREAM_NOTIFY_CONNECT => 'CONNECT',
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
STREAM_NOTIFY_PROGRESS => 'PROGRESS',
STREAM_NOTIFY_FAILURE => 'FAILURE',
STREAM_NOTIFY_COMPLETED => 'COMPLETED',
STREAM_NOTIFY_RESOLVE => 'RESOLVE',
];
static $args = ['severity', 'message', 'message_code',
'bytes_transferred', 'bytes_max'];
$value = Core::getDebugResource($value);
$ident = $request['http_method'] . ' ' . Core::url($request);
$fn = function () use ($ident, $value, $map, $args) {
$passed = func_get_args();
$code = array_shift($passed);
fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
foreach (array_filter($passed) as $i => $v) {
fwrite($value, $args[$i] . ': "' . $v . '" ');
}
fwrite($value, "\n");
};
// Wrap the existing function if needed.
$params['notification'] = isset($params['notification'])
? Core::callArray([$params['notification'], $fn])
: $fn;
}
private function applyCustomOptions(array $request, array &$options)
{
if (!isset($request['client']['stream_context'])) {
return;
}
if (!is_array($request['client']['stream_context'])) {
throw new RingException('stream_context must be an array');
}
$options = array_replace_recursive(
$options,
$request['client']['stream_context']
);
}
private function createContext(array $request, array $options, array $params)
{
$this->applyCustomOptions($request, $options);
return $this->createResource(
function () use ($request, $options, $params) {
return stream_context_create($options, $params);
},
$request,
$options
);
}
private function createStreamResource(
$url,
array $request,
array $options,
$context
) {
return $this->createResource(
function () use ($url, $context) {
if (false === strpos($url, 'http')) {
trigger_error("URL is invalid: {$url}", E_USER_WARNING);
return null;
}
$resource = fopen($url, 'r', null, $context);
$this->lastHeaders = $http_response_header;
return $resource;
},
$request,
$options
);
}
}

View file

@ -0,0 +1,364 @@
<?php
namespace GuzzleHttp\Ring;
use GuzzleHttp\Stream\StreamInterface;
use GuzzleHttp\Ring\Future\FutureArrayInterface;
use GuzzleHttp\Ring\Future\FutureArray;
/**
* Provides core functionality of Ring handlers and middleware.
*/
class Core
{
/**
* Returns a function that calls all of the provided functions, in order,
* passing the arguments provided to the composed function to each function.
*
* @param callable[] $functions Array of functions to proxy to.
*
* @return callable
*/
public static function callArray(array $functions)
{
return function () use ($functions) {
$args = func_get_args();
foreach ($functions as $fn) {
call_user_func_array($fn, $args);
}
};
}
/**
* Gets an array of header line values from a message for a specific header
*
* This method searches through the "headers" key of a message for a header
* using a case-insensitive search.
*
* @param array $message Request or response hash.
* @param string $header Header to retrieve
*
* @return array
*/
public static function headerLines($message, $header)
{
$result = [];
if (!empty($message['headers'])) {
foreach ($message['headers'] as $name => $value) {
if (!strcasecmp($name, $header)) {
$result = array_merge($result, $value);
}
}
}
return $result;
}
/**
* Gets a header value from a message as a string or null
*
* This method searches through the "headers" key of a message for a header
* using a case-insensitive search. The lines of the header are imploded
* using commas into a single string return value.
*
* @param array $message Request or response hash.
* @param string $header Header to retrieve
*
* @return string|null Returns the header string if found, or null if not.
*/
public static function header($message, $header)
{
$match = self::headerLines($message, $header);
return $match ? implode(', ', $match) : null;
}
/**
* Returns the first header value from a message as a string or null. If
* a header line contains multiple values separated by a comma, then this
* function will return the first value in the list.
*
* @param array $message Request or response hash.
* @param string $header Header to retrieve
*
* @return string|null Returns the value as a string if found.
*/
public static function firstHeader($message, $header)
{
if (!empty($message['headers'])) {
foreach ($message['headers'] as $name => $value) {
if (!strcasecmp($name, $header)) {
// Return the match itself if it is a single value.
$pos = strpos($value[0], ',');
return $pos ? substr($value[0], 0, $pos) : $value[0];
}
}
}
return null;
}
/**
* Returns true if a message has the provided case-insensitive header.
*
* @param array $message Request or response hash.
* @param string $header Header to check
*
* @return bool
*/
public static function hasHeader($message, $header)
{
if (!empty($message['headers'])) {
foreach ($message['headers'] as $name => $value) {
if (!strcasecmp($name, $header)) {
return true;
}
}
}
return false;
}
/**
* Parses an array of header lines into an associative array of headers.
*
* @param array $lines Header lines array of strings in the following
* format: "Name: Value"
* @return array
*/
public static function headersFromLines($lines)
{
$headers = [];
foreach ($lines as $line) {
$parts = explode(':', $line, 2);
$headers[trim($parts[0])][] = isset($parts[1])
? trim($parts[1])
: null;
}
return $headers;
}
/**
* Removes a header from a message using a case-insensitive comparison.
*
* @param array $message Message that contains 'headers'
* @param string $header Header to remove
*
* @return array
*/
public static function removeHeader(array $message, $header)
{
if (isset($message['headers'])) {
foreach (array_keys($message['headers']) as $key) {
if (!strcasecmp($header, $key)) {
unset($message['headers'][$key]);
}
}
}
return $message;
}
/**
* Replaces any existing case insensitive headers with the given value.
*
* @param array $message Message that contains 'headers'
* @param string $header Header to set.
* @param array $value Value to set.
*
* @return array
*/
public static function setHeader(array $message, $header, array $value)
{
$message = self::removeHeader($message, $header);
$message['headers'][$header] = $value;
return $message;
}
/**
* Creates a URL string from a request.
*
* If the "url" key is present on the request, it is returned, otherwise
* the url is built up based on the scheme, host, uri, and query_string
* request values.
*
* @param array $request Request to get the URL from
*
* @return string Returns the request URL as a string.
* @throws \InvalidArgumentException if no Host header is present.
*/
public static function url(array $request)
{
if (isset($request['url'])) {
return $request['url'];
}
$uri = (isset($request['scheme'])
? $request['scheme'] : 'http') . '://';
if ($host = self::header($request, 'host')) {
$uri .= $host;
} else {
throw new \InvalidArgumentException('No Host header was provided');
}
if (isset($request['uri'])) {
$uri .= $request['uri'];
}
if (isset($request['query_string'])) {
$uri .= '?' . $request['query_string'];
}
return $uri;
}
/**
* Reads the body of a message into a string.
*
* @param array|FutureArrayInterface $message Array containing a "body" key
*
* @return null|string Returns the body as a string or null if not set.
* @throws \InvalidArgumentException if a request body is invalid.
*/
public static function body($message)
{
if (!isset($message['body'])) {
return null;
}
if ($message['body'] instanceof StreamInterface) {
return (string) $message['body'];
}
switch (gettype($message['body'])) {
case 'string':
return $message['body'];
case 'resource':
return stream_get_contents($message['body']);
case 'object':
if ($message['body'] instanceof \Iterator) {
return implode('', iterator_to_array($message['body']));
} elseif (method_exists($message['body'], '__toString')) {
return (string) $message['body'];
}
default:
throw new \InvalidArgumentException('Invalid request body: '
. self::describeType($message['body']));
}
}
/**
* Rewind the body of the provided message if possible.
*
* @param array $message Message that contains a 'body' field.
*
* @return bool Returns true on success, false on failure
*/
public static function rewindBody($message)
{
if ($message['body'] instanceof StreamInterface) {
return $message['body']->seek(0);
}
if ($message['body'] instanceof \Generator) {
return false;
}
if ($message['body'] instanceof \Iterator) {
$message['body']->rewind();
return true;
}
if (is_resource($message['body'])) {
return rewind($message['body']);
}
return is_string($message['body'])
|| (is_object($message['body'])
&& method_exists($message['body'], '__toString'));
}
/**
* Debug function used to describe the provided value type and class.
*
* @param mixed $input
*
* @return string Returns a string containing the type of the variable and
* if a class is provided, the class name.
*/
public static function describeType($input)
{
switch (gettype($input)) {
case 'object':
return 'object(' . get_class($input) . ')';
case 'array':
return 'array(' . count($input) . ')';
default:
ob_start();
var_dump($input);
// normalize float vs double
return str_replace('double(', 'float(', rtrim(ob_get_clean()));
}
}
/**
* Sleep for the specified amount of time specified in the request's
* ['client']['delay'] option if present.
*
* This function should only be used when a non-blocking sleep is not
* possible.
*
* @param array $request Request to sleep
*/
public static function doSleep(array $request)
{
if (isset($request['client']['delay'])) {
usleep($request['client']['delay'] * 1000);
}
}
/**
* Returns a proxied future that modifies the dereferenced value of another
* future using a promise.
*
* @param FutureArrayInterface $future Future to wrap with a new future
* @param callable $onFulfilled Invoked when the future fulfilled
* @param callable $onRejected Invoked when the future rejected
* @param callable $onProgress Invoked when the future progresses
*
* @return FutureArray
*/
public static function proxy(
FutureArrayInterface $future,
callable $onFulfilled = null,
callable $onRejected = null,
callable $onProgress = null
) {
return new FutureArray(
$future->then($onFulfilled, $onRejected, $onProgress),
[$future, 'wait'],
[$future, 'cancel']
);
}
/**
* Returns a debug stream based on the provided variable.
*
* @param mixed $value Optional value
*
* @return resource
*/
public static function getDebugResource($value = null)
{
if (is_resource($value)) {
return $value;
} elseif (defined('STDOUT')) {
return STDOUT;
} else {
return fopen('php://output', 'w');
}
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Ring\Exception;
/**
* Marker interface for cancelled exceptions.
*/
interface CancelledException {}

View file

@ -0,0 +1,4 @@
<?php
namespace GuzzleHttp\Ring\Exception;
class CancelledFutureAccessException extends RingException implements CancelledException {}

View file

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Ring\Exception;
/**
* Occurs when the connection failed.
*/
class ConnectException extends RingException {}

View file

@ -0,0 +1,4 @@
<?php
namespace GuzzleHttp\Ring\Exception;
class RingException extends \RuntimeException {};

View file

@ -0,0 +1,125 @@
<?php
namespace GuzzleHttp\Ring\Future;
use GuzzleHttp\Ring\Exception\CancelledFutureAccessException;
use GuzzleHttp\Ring\Exception\RingException;
use React\Promise\PromiseInterface;
/**
* Implements common future functionality built on top of promises.
*/
trait BaseFutureTrait
{
/** @var callable */
private $waitfn;
/** @var callable */
private $cancelfn;
/** @var PromiseInterface */
private $wrappedPromise;
/** @var \Exception Error encountered. */
private $error;
/** @var mixed Result of the future */
private $result;
private $isRealized = false;
/**
* @param PromiseInterface $promise Promise to shadow with the future.
* @param callable $wait Function that blocks until the deferred
* computation has been resolved. This
* function MUST resolve the deferred value
* associated with the supplied promise.
* @param callable $cancel If possible and reasonable, provide a
* function that can be used to cancel the
* future from completing.
*/
public function __construct(
PromiseInterface $promise,
callable $wait = null,
callable $cancel = null
) {
$this->wrappedPromise = $promise;
$this->waitfn = $wait;
$this->cancelfn = $cancel;
}
public function wait()
{
if (!$this->isRealized) {
$this->addShadow();
if (!$this->isRealized && $this->waitfn) {
$this->invokeWait();
}
if (!$this->isRealized) {
$this->error = new RingException('Waiting did not resolve future');
}
}
if ($this->error) {
throw $this->error;
}
return $this->result;
}
public function promise()
{
return $this->wrappedPromise;
}
public function then(
callable $onFulfilled = null,
callable $onRejected = null,
callable $onProgress = null
) {
return $this->wrappedPromise->then($onFulfilled, $onRejected, $onProgress);
}
public function cancel()
{
if (!$this->isRealized) {
$cancelfn = $this->cancelfn;
$this->waitfn = $this->cancelfn = null;
$this->isRealized = true;
$this->error = new CancelledFutureAccessException();
if ($cancelfn) {
$cancelfn($this);
}
}
}
private function addShadow()
{
// Get the result and error when the promise is resolved. Note that
// calling this function might trigger the resolution immediately.
$this->wrappedPromise->then(
function ($value) {
$this->isRealized = true;
$this->result = $value;
$this->waitfn = $this->cancelfn = null;
},
function ($error) {
$this->isRealized = true;
$this->error = $error;
$this->waitfn = $this->cancelfn = null;
}
);
}
private function invokeWait()
{
try {
$wait = $this->waitfn;
$this->waitfn = null;
$wait();
} catch (\Exception $e) {
// Defer can throw to reject.
$this->error = $e;
$this->isRealized = true;
}
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace GuzzleHttp\Ring\Future;
/**
* Represents a future array that has been completed successfully.
*/
class CompletedFutureArray extends CompletedFutureValue implements FutureArrayInterface
{
public function __construct(array $result)
{
parent::__construct($result);
}
public function offsetExists($offset)
{
return isset($this->result[$offset]);
}
public function offsetGet($offset)
{
return $this->result[$offset];
}
public function offsetSet($offset, $value)
{
$this->result[$offset] = $value;
}
public function offsetUnset($offset)
{
unset($this->result[$offset]);
}
public function count()
{
return count($this->result);
}
public function getIterator()
{
return new \ArrayIterator($this->result);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace GuzzleHttp\Ring\Future;
use React\Promise\FulfilledPromise;
use React\Promise\RejectedPromise;
/**
* Represents a future value that has been resolved or rejected.
*/
class CompletedFutureValue implements FutureInterface
{
protected $result;
protected $error;
private $cachedPromise;
/**
* @param mixed $result Resolved result
* @param \Exception $e Error. Pass a GuzzleHttp\Ring\Exception\CancelledFutureAccessException
* to mark the future as cancelled.
*/
public function __construct($result, \Exception $e = null)
{
$this->result = $result;
$this->error = $e;
}
public function wait()
{
if ($this->error) {
throw $this->error;
}
return $this->result;
}
public function cancel() {}
public function promise()
{
if (!$this->cachedPromise) {
$this->cachedPromise = $this->error
? new RejectedPromise($this->error)
: new FulfilledPromise($this->result);
}
return $this->cachedPromise;
}
public function then(
callable $onFulfilled = null,
callable $onRejected = null,
callable $onProgress = null
) {
return $this->promise()->then($onFulfilled, $onRejected, $onProgress);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace GuzzleHttp\Ring\Future;
/**
* Represents a future array value that when dereferenced returns an array.
*/
class FutureArray implements FutureArrayInterface
{
use MagicFutureTrait;
public function offsetExists($offset)
{
return isset($this->_value[$offset]);
}
public function offsetGet($offset)
{
return $this->_value[$offset];
}
public function offsetSet($offset, $value)
{
$this->_value[$offset] = $value;
}
public function offsetUnset($offset)
{
unset($this->_value[$offset]);
}
public function count()
{
return count($this->_value);
}
public function getIterator()
{
return new \ArrayIterator($this->_value);
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace GuzzleHttp\Ring\Future;
/**
* Future that provides array-like access.
*/
interface FutureArrayInterface extends
FutureInterface,
\ArrayAccess,
\Countable,
\IteratorAggregate {};

View file

@ -0,0 +1,40 @@
<?php
namespace GuzzleHttp\Ring\Future;
use React\Promise\PromiseInterface;
use React\Promise\PromisorInterface;
/**
* Represents the result of a computation that may not have completed yet.
*
* You can use the future in a blocking manner using the wait() function, or
* you can use a promise from the future to receive the result when the future
* has been resolved.
*
* When the future is dereferenced using wait(), the result of the computation
* is cached and returned for subsequent calls to wait(). If the result of the
* computation has not yet completed when wait() is called, the call to wait()
* will block until the future has completed.
*/
interface FutureInterface extends PromiseInterface, PromisorInterface
{
/**
* Returns the result of the future either from cache or by blocking until
* it is complete.
*
* This method must block until the future has a result or is cancelled.
* Throwing an exception in the wait() method will mark the future as
* realized and will throw the exception each time wait() is called.
* Throwing an instance of GuzzleHttp\Ring\CancelledException will mark
* the future as realized, will not throw immediately, but will throw the
* exception if the future's wait() method is called again.
*
* @return mixed
*/
public function wait();
/**
* Cancels the future, if possible.
*/
public function cancel();
}

View file

@ -0,0 +1,12 @@
<?php
namespace GuzzleHttp\Ring\Future;
/**
* Represents a future value that responds to wait() to retrieve the promised
* value, but can also return promises that are delivered the value when it is
* available.
*/
class FutureValue implements FutureInterface
{
use BaseFutureTrait;
}

View file

@ -0,0 +1,32 @@
<?php
namespace GuzzleHttp\Ring\Future;
/**
* Implements common future functionality that is triggered when the result
* property is accessed via a magic __get method.
*
* @property mixed $_value Actual data used by the future. Accessing this
* property will cause the future to block if needed.
*/
trait MagicFutureTrait
{
use BaseFutureTrait;
/**
* This function handles retrieving the dereferenced result when requested.
*
* @param string $name Should always be "data" or an exception is thrown.
*
* @return mixed Returns the dereferenced data.
* @throws \RuntimeException
* @throws \GuzzleHttp\Ring\Exception\CancelledException
*/
public function __get($name)
{
if ($name !== '_value') {
throw new \RuntimeException("Class has no {$name} property");
}
return $this->_value = $this->wait();
}
}