Commit 8294bfc4 authored by TheBigB's avatar TheBigB
Browse files

Merge branch 'develop' of bitbucket.org:1of0/curly into develop

parents 0213ead5 09081737
Pipeline #4004555 failed with stage
in 1 minute and 6 seconds
# Curly
Curly is an object oriented wrapper around PHP's cURL extension.
## Basic usage
To execute requests you may provide a URL and HTTP method, but you may also provide an instance of PSR-7's
`RequestInterface` as a base to configure the cURL channel.
```php
$curly = new Curly();
// Using PSR-7 RequestInterface implementation
$request = (new Request)
->withMethod(Curly::HTTP_POST)
->withUri(new Uri('http://example.com'))
->withHeader('Accepts', 'application/json');
$response = $curly->request($request);
// Using plain URL and method
$response = $curly->requestByUrl('http://example.com', Curly::HTTP_DELETE);
```
By default the `requestByUrl()` and `request()` methods will return a `ResponseInterface`. To process the response
manually, you may configure callbacks or configure a custom handler (which under water will configure callbacks, but
provides a cleaner programming interface).
## Custom configuration
### cURL options
The options that would normally be set through `curl_setopt` must be set through a `CurlyOptions` instance. The
`CurlyOptions` instance can be reused over multiple requests.
### Custom handlers
Instead of manually configuring callbacks in the `CurlyOptions` object, you may extend the `AbstractHandler` class to
hook into events. The library comes with two implementations of the `AbstractHandler`. The `CancellableHandler` and
`StreamHandler`.
The `CancellableHandler` is provided a callback during instantiation. During the transfer, cURL's progress event is
routed to the handler, which in turn invokes the callback to determine whether the transfer should be aborted.
The `StreamHandler` decorates the `CancellableHandler` and is an example of a handler that hooks into cURL's read and
write callbacks. It probably isn't very useful since setting the `inputStream` and `outputStream` options in the
`CurlyOptions` object would achieve more or less the same, but helps demonstrate the usage of the callbacks.
......@@ -5,13 +5,14 @@
"license": "MIT",
"require":
{
"php": ">=5.6.0",
"psr/http-message": "1.0",
"zendframework/zend-diactoros": "1.1.4"
"php": ">=5.5.0",
"psr/http-message": "^1.0",
"1of0/streams": "^0.1.3",
"zendframework/zend-diactoros": "^1.1.0"
},
"require-dev":
{
"phpunit/phpunit": "4.8.*"
"phpunit/phpunit": "^5.0.0"
},
"autoload":
{
......
This diff is collapsed.
......@@ -14,8 +14,18 @@ use RuntimeException;
BinarySafe::initialize();
/**
* Class BinarySafe
*
* Collection of helper functions that ensure proper function when dealing with unicode data.
*
* @package OneOfZero\Curly
*/
class BinarySafe
{
/**
* Default maximum for write failures
*/
const MAX_WRITE_FAILURES = 5;
/**
......@@ -24,7 +34,7 @@ class BinarySafe
private static $mbFunctionsAvailable = false;
/**
*
* Detects whether mb_* functions are available.
*/
public static function initialize()
{
......@@ -32,6 +42,8 @@ class BinarySafe
}
/**
* Binary-safe strlen implementation; uses mb_strlen if available.
*
* @param $input
* @return int
* @throws RuntimeException
......@@ -54,6 +66,8 @@ class BinarySafe
}
/**
* Binary-safe substr implementation; uses mb_substr if available.
*
* @param string $input
* @param int $start
* @param int|null $length
......@@ -86,6 +100,8 @@ class BinarySafe
}
/**
* Robust stream writing method that has tolerance for write failures.
*
* @param StreamInterface $stream
* @param string $data
* @param int $maxWriteFailures
......
......@@ -9,26 +9,19 @@
namespace OneOfZero\Curly;
class CancellationToken
/**
* Interface CancellationCallbackInterface
*
* Defines an interface for a cancellation callback.
*
* @package OneOfZero\Curly
*/
interface CancellationCallbackInterface
{
/**
* @var bool $isCancelled
*/
private $isCancelled;
/**
* @return bool
*/
public function isCanceled()
{
return $this->isCancelled;
}
/**
* Should return true to signal cancellation.
*
* @return bool
*/
public function cancel()
{
$this->isCancelled = true;
}
public function isCanceled();
}
......@@ -10,31 +10,40 @@
namespace OneOfZero\Curly;
use OneOfZero\Curly\Handlers\AbstractHandler;
use OneOfZero\Streams\SharedStreamInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Request;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
class Curly
/**
* Class Curly
*
* Object oriented wrapper around cURL with PSR-7 support.
*
* @package OneOfZero\Curly
*/
class Curly implements HttpClientInterface
{
const HTTP_GET = 'GET';
const HTTP_POST = 'POST';
const HTTP_PUT = 'PUT';
const HTTP_DELETE = 'DELETE';
const HTTP_HEAD = 'HEAD';
const HTTP_PATCH = 'PATCH';
/**
* Holds the cURL options relevant to this instance.
*
* @var CurlyOptions $options
*/
private $options;
/**
* Holds the configured handler (if any).
*
* @var AbstractHandler $customHandler;
*/
private $customHandler;
/**
* Creates an instance of Curly, optionally pre-configuring it with a CurlyOptions instance.
*
* @param CurlyOptions $options
*/
public function __construct(CurlyOptions $options = null)
......@@ -43,39 +52,67 @@ class Curly
}
/**
* Executes a request with the provided URL and method.
*
* Returns a PSR-7 response object.
*
* @param string $url
* @param string $method
*
* @return ResponseInterface
*/
public function requestByUrl($url, $method = 'GET')
{
return $this->request(new Request($url, $method));
}
/**
* Executes a request with the provided PSR-7 request object.
*
* Returns a PSR-7 response object.
*
* @param RequestInterface $request
* @return Response
*
* @return ResponseInterface
*/
public function execute(RequestInterface $request)
public function request(RequestInterface $request)
{
$options = clone $this->options;
$this->applyRequest($request, $options);
if ($this->customHandler !== null)
{
$this->customHandler->registerCallbacks($options);
}
$headerStream = $this->prepareHeaderStream($options);
$responseStream = $this->prepareResponseStream($options);
$channel = curl_init();
if ($this->customHandler !== null)
{
$this->customHandler->registerCallbacks($channel);
}
$options->apply($channel);
curl_exec($channel);
$status = curl_getinfo($channel, CURLINFO_HTTP_CODE);
return new Response($responseStream, $status, $this->parseHeaderStream($headerStream));
return new Response($responseStream ?: 'php://memory', $status, $this->parseHeaderStream($headerStream));
}
/**
* Prepares a stream to store headers in.
*
* @param CurlyOptions $options
*
* @return Stream
*/
private function prepareHeaderStream(CurlyOptions $options)
{
if ($options->onHeader !== null)
{
return null;
}
if ($options->outputHeaderStream === null)
{
$stream = fopen('php://memory', 'r+');
......@@ -89,11 +126,19 @@ class Curly
}
/**
* Prepares a stream to store the response in.
*
* @param CurlyOptions $options
*
* @return Stream
*/
private function prepareResponseStream(CurlyOptions $options)
{
if ($options->onWrite !== null)
{
return null;
}
if ($options->outputStream === null)
{
$stream = fopen('php://memory', 'r+');
......@@ -107,6 +152,8 @@ class Curly
}
/**
* Configures the provided CurlyOptions instance to represent the provided PSR-7 request.
*
* @param RequestInterface $request
* @param CurlyOptions $options
*/
......@@ -137,13 +184,19 @@ class Curly
{
$options->upload = true;
$options->expectedInputSize = $stream->getSize();
$options->inputStream = $stream->detach();
$options->inputStream = ($stream instanceof SharedStreamInterface)
? $stream->getResource()
: $stream->detach()
;
}
}
/**
* Returns the headers from the provided PSR-7 request as a string array of formatted headers.
*
* @param RequestInterface $request
* @return array
*
* @return string[]
*/
private function getFormattedHeaders(RequestInterface $request)
{
......@@ -159,11 +212,20 @@ class Curly
}
/**
* Parses the headers from the provided header stream, and returns them as an array compatible with the PSR-7
* response object.
*
* @param StreamInterface $headerStream
*
* @return array
*/
private function parseHeaderStream(StreamInterface $headerStream)
private function parseHeaderStream(StreamInterface $headerStream = null)
{
if ($headerStream === null)
{
return [];
}
$headerLines = explode("\r\n", strval($headerStream));
$headerStream->rewind();
......@@ -176,10 +238,9 @@ class Curly
continue;
}
list($name, $value) = explode(':', $line);
$name = trim($name);
list($name, $value) = explode(':', $line, 2);
$normalizedName = strtolower($name);
$value = trim($value);
$value = trim($value, "\t ");
if (!array_key_exists($normalizedName, $headerMap))
{
......@@ -199,6 +260,8 @@ class Curly
#region // Generic getters and setters
/**
* Gets the CurlyOptions for this instance.
*
* @return CurlyOptions
*/
public function getOptions()
......@@ -207,6 +270,8 @@ class Curly
}
/**
* Sets the provided CurlyOptions for this instance.
*
* @param CurlyOptions $options
*/
public function setOptions(CurlyOptions $options = null)
......@@ -219,6 +284,8 @@ class Curly
}
/**
* Gets the custom handler for this instance (if any).
*
* @return AbstractHandler
*/
public function getCustomHandler()
......@@ -227,6 +294,8 @@ class Curly
}
/**
* Sets the custom handler for this instance.
*
* @param AbstractHandler $customHandler
*/
public function setCustomHandler(AbstractHandler $customHandler)
......
......@@ -11,11 +11,21 @@ namespace OneOfZero\Curly;
use ReflectionClass;
/**
* Class CurlyOptions
*
* Makes supported cURL options available through properties.
*
* Note that some properties have different names from the options they represent. See the code file for a mapping
* between the properties and the CURLOPT_* constants.
*
* @package OneOfZero\Curly
*/
class CurlyOptions
{
#region // Mapping between property names and cURL constants
const OPTION_MAP = [
protected static $__optionMap = [
'followRedirects' => CURLOPT_FOLLOWLOCATION,
'maxRedirects' => CURLOPT_MAXREDIRS,
'followPostRedirects' => CURLOPT_POSTREDIR,
......@@ -61,6 +71,7 @@ class CurlyOptions
'failOnErrorStatus' => CURLOPT_DNS_USE_GLOBAL_CACHE,
'successAliases' => CURLOPT_HTTP200ALIASES,
'noBody' => CURLOPT_NOBODY,
'noProgress' => CURLOPT_NOPROGRESS,
'isVerbose' => CURLOPT_VERBOSE,
'timeout' => CURLOPT_TIMEOUT_MS,
'encoding' => CURLOPT_ENCODING,
......@@ -85,7 +96,11 @@ class CurlyOptions
'range' => CURLOPT_RANGE,
'referer' => CURLOPT_REFERER,
'userAgent' => CURLOPT_USERAGENT,
'headers' => CURLOPT_HTTPHEADER
'headers' => CURLOPT_HTTPHEADER,
'onHeader' => CURLOPT_HEADERFUNCTION,
'onProgress' => CURLOPT_PROGRESSFUNCTION,
'onRead' => CURLOPT_READFUNCTION,
'onWrite' => CURLOPT_WRITEFUNCTION,
];
#endregion
......@@ -431,6 +446,13 @@ class CurlyOptions
*/
public $noBody;
/**
* CURLOPT_NOPROGRESS
* @link http://curl.haxx.se/libcurl/c/CURLOPT_NOPROGRESS.html
* @var bool $noProgress
*/
public $noProgress = true;
/**
* CURLOPT_VERBOSE
* @link http://curl.haxx.se/libcurl/c/CURLOPT_VERBOSE.html
......@@ -620,7 +642,41 @@ class CurlyOptions
#endregion
#region // Callbacks
/**
* CURLOPT_HEADERFUNCTION (callable)
* @link http://curl.haxx.se/libcurl/c/CURLOPT_HEADERFUNCTION.html
* @var Callable $onHeader
*/
public $onHeader;
/**
* CURLOPT_PROGRESSFUNCTION (callable)
* @link http://curl.haxx.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
* @var Callable $onProgress
*/
public $onProgress;
/**
* CURLOPT_READFUNCTION (callable)
* @link http://curl.haxx.se/libcurl/c/CURLOPT_READFUNCTION.html
* @var Callable $onRead
*/
public $onRead;
/**
* CURLOPT_WRITEFUNCTION (callable)
* @link http://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.html
* @var Callable $onWrite
*/
public $onWrite;
#endregion
/**
* Creates an instance of CurlyOptions, optionally providing an array of values to pre-configure.
*
* @param array $options
*/
public function __construct(array $options = [])
......@@ -638,6 +694,8 @@ class CurlyOptions
}
/**
* Creates and returns a clone of this object.
*
* @return CurlyOptions
*/
public function __clone()
......@@ -646,12 +704,15 @@ class CurlyOptions
}
/**
* Converts the options configured in this object to an array that is compatible with curl_setopt_array().
*
* @param bool $useNativeCurlConstants
*
* @return array
*/
public function toArray($useNativeCurlConstants = true)
{
$map = self::OPTION_MAP;
$map = self::$__optionMap;
$class = new ReflectionClass(get_class());
$array = [];
......@@ -679,6 +740,8 @@ class CurlyOptions
}
/**
* Applies the options configured in this object on the provided channel.
*
* @param Resource $channel
*/
public function apply($channel)
......
......@@ -11,6 +11,11 @@ namespace OneOfZero\Curly\Exceptions;
use Exception;
/**
* Class NotSupportedException
*
* @package OneOfZero\Curly\Exceptions
*/
class NotSupportedException extends Exception
{
}
<?php
namespace OneOfZero\Curly;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Uri;
class ExtendedServerRequest extends ServerRequest
{
/**
* Returns a copy of this request with the provided URI pieces as URI.
*
* @param string $pieces,...
*
* @return static
*/
public function withUriString($pieces)
{
// TODO: Fix this properly - https://tools.ietf.org/html/rfc3986#section-5.2
$pieces = array_map(function($item) { return trim($item, '/'); }, func_get_args());
return $this->withUri(new Uri(implode('/', $pieces)));
}
/**
* Returns a copy of this request with the provided string as body.
*
* @param string $content
*
* @return static
*/
public function withStringBody($content)
{
$stream = new SharedStream('php://memory', 'r+');
BinarySafe::write($stream, $content);
$stream->rewind();
return $this->withBody($stream);
}
/**
* Returns a copy of this request with the provided form JSON serialized as the request body.
*
* @param array $form
* @param bool $sortFieldsByName
*
* @return static
*/
public function withJsonForm(array $form, $sortFieldsByName = false)
{
if ($sortFieldsByName)
{
ksort($form);
}
return $this->withJsonString(json_encode($form));
}
/**
* Returns a copy of this request with the provided object JSON serialized as the request body.
*
* @param object $object
*
* @return static
*/
public function withJsonObject($object)
{
return $this->withJsonString(json_encode($object));
}
/**
* Returns a copy of this request with the provided JSON as the request body.
*
* @param string $json
*
* @return static
*/
public function withJsonString($json)
{
return $this
->withHeader('Content-Type', 'application/json')
->withStringBody($json)