Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 112 additions & 62 deletions src/Client.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace Http\Client\Curl;

use Http\Client\Exception;
Expand All @@ -18,39 +19,42 @@
*
* @author Михаил Красильников <[email protected]>
* @author Blake Williams <[email protected]>
* @author Dmitry Arhitector <[email protected]>
*
* @api
* @since 1.0
*/
class Client implements HttpClient, HttpAsyncClient
{
/**

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra line?

/**
* cURL options
*
* @var array
*/
private $options;
protected $options;

/**
* cURL response parser
*
* @var ResponseParser
*/
private $responseParser;
protected $responseParser;

/**
* cURL synchronous requests handle
*
* @var resource|null
*/
private $handle = null;
protected $handle = null;

/**
* Simultaneous requests runner
*
* @var MultiRunner|null
*/
private $multiRunner = null;
protected $multiRunner = null;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra line.


/**
* Create new client
Expand All @@ -68,29 +72,17 @@ class Client implements HttpClient, HttpAsyncClient
*
* @since 1.0
*/
public function __construct(
MessageFactory $messageFactory,
StreamFactory $streamFactory,
array $options = []
) {
public function __construct(MessageFactory $messageFactory, StreamFactory $streamFactory, array $options = [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this, this is because PSR conforming.

{
$this->responseParser = new ResponseParser($messageFactory, $streamFactory);
$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
* @param array $options custom curl options
*
* @return ResponseInterface
*
Expand All @@ -99,27 +91,24 @@ public function __destruct()
*
* @since 1.0
*/
public function sendRequest(RequestInterface $request)
public function sendRequest(RequestInterface $request, array $options = [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add this. Use the constructor.

{
$options = $this->createCurlOptions($request);

if (is_resource($this->handle)) {
curl_reset($this->handle);
} else {
$this->handle = curl_init();
}

$options = $this->createCurlOptions($request, $options);

curl_setopt_array($this->handle, $options);
$raw = curl_exec($this->handle);

if (curl_errno($this->handle) > 0) {
if ( ! curl_exec($this->handle)) {
throw new RequestException(curl_error($this->handle), $request);
}

$info = curl_getinfo($this->handle);

try {
$response = $this->responseParser->parse($raw, $info);
$response = $this->responseParser->parse($options[CURLOPT_FILE], curl_getinfo($this->handle));
} catch (\Exception $e) {
throw new RequestException($e->getMessage(), $request, $e);
}
Expand All @@ -130,6 +119,7 @@ public function sendRequest(RequestInterface $request)
* Sends a PSR-7 request in an asynchronous way.
*
* @param RequestInterface $request
* @param array $options custom curl options
*
* @return Promise
*
Expand All @@ -138,66 +128,111 @@ public function sendRequest(RequestInterface $request)
*
* @since 1.0
*/
public function sendAsyncRequest(RequestInterface $request)
public function sendAsyncRequest(RequestInterface $request, array $options = [])
{
if (!$this->multiRunner instanceof MultiRunner) {
if ( ! $this->multiRunner instanceof MultiRunner) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this. No spaces (PSR)

$this->multiRunner = new MultiRunner($this->responseParser);
}

$handle = curl_init();
$options = $this->createCurlOptions($request);
$options = $this->createCurlOptions($request, $options);

curl_setopt_array($handle, $options);

$core = new PromiseCore($request, $handle);
$promise = new CurlPromise($core, $this->multiRunner);

$this->multiRunner->add($core);

return $promise;
}

/**
* Get parser
*
* @return ResponseParser
*/
public function getResponseParser()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary.

{
return $this->responseParser;
}

/**
* Release resources if still active
*/
public function __destruct()
{
if (is_resource($this->handle)) {
curl_close($this->handle);
}
}

/**
* Generates cURL options
*
* @param RequestInterface $request
* @param array $options custom curl options
*
* @throws \UnexpectedValueException if unsupported HTTP version requested
*
* @return array
*/
private function createCurlOptions(RequestInterface $request)
{
$options = $this->options;

$options[CURLOPT_HEADER] = true;
$options[CURLOPT_RETURNTRANSFER] = true;
$options[CURLOPT_FOLLOWLOCATION] = false;

protected function createCurlOptions(RequestInterface $request, array $options = [])
{
// Invalid overwrite Curl options.
$options = array_diff_key($options + $this->options, array_flip([CURLOPT_INFILE, CURLOPT_INFILESIZE]));
$options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion());
$options[CURLOPT_HEADERFUNCTION] = [$this->getResponseParser(), 'headerHandler'];
$options[CURLOPT_URL] = (string) $request->getUri();
$options[CURLOPT_RETURNTRANSFER] = false;
$options[CURLOPT_FILE] = $this->getResponseParser()->getTemporaryStream();
$options[CURLOPT_HEADER] = false;

if (in_array($request->getMethod(), ['OPTIONS', 'POST', 'PUT'], true)) {
// cURL allows request body only for these methods.
$body = (string) $request->getBody();
if ('' !== $body) {
$options[CURLOPT_POSTFIELDS] = $body;
}
}
// These methods do not transfer body.
// You can specify any method you'd like, including a custom method that might not be part of RFC 7231 (like "MOVE").
if (in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE', 'CONNECT'])) {
if ($request->getMethod() == 'HEAD') {
$options[CURLOPT_NOBODY] = true;

if ($request->getMethod() === '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();
}
unset($options[CURLOPT_READFUNCTION], $options[CURLOPT_WRITEFUNCTION]);
}
} else {
$body = clone $request->getBody();
$size = $body->getSize();

$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);
if ($size === null || $size > 1048576) {
$body->rewind();
$options[CURLOPT_UPLOAD] = true;

if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}
// Avoid full loading large or unknown size body into memory. Not replace CURLOPT_READFUNCTION.
if (isset($options[CURLOPT_READFUNCTION]) && is_callable($options[CURLOPT_READFUNCTION])) {
$body = $body->detach();
$options[CURLOPT_READFUNCTION] = function ($curlHandler, $handler, $length) use ($body, $options) {
return call_user_func($options[CURLOPT_READFUNCTION], $curlHandler, $body, $length);
};
} else {
$options[CURLOPT_READFUNCTION] = function ($curl, $handler, $length) use ($body) {
return $body->read($length);
};
}
} else {
// Send the body as a string if the size is less than 1MB.
$options[CURLOPT_POSTFIELDS] = (string) $request->getBody();
}
}

return $options;
}
if ($request->getMethod() != 'GET') {
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}

$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);

if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}

return $options;
}

/**
* Return cURL constant for specified HTTP version
Expand All @@ -208,7 +243,7 @@ private function createCurlOptions(RequestInterface $request)
*
* @return int
*/
private function getProtocolVersion($requestVersion)
protected function getProtocolVersion($requestVersion)
{
switch ($requestVersion) {
case '1.0':
Expand All @@ -219,8 +254,10 @@ private function getProtocolVersion($requestVersion)
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;
}

Expand All @@ -232,23 +269,36 @@ private function getProtocolVersion($requestVersion)
*
* @return string[]
*/
private function createHeaders(RequestInterface $request, array $options)
protected function createHeaders(RequestInterface $request, array $options)
{
$curlHeaders = [];
$headers = array_keys($request->getHeaders());

if ( ! $request->hasHeader('Expect')) {
$curlHeaders[] = 'Expect:';
}

if ( ! $request->hasHeader('Accept')) {
$curlHeaders[] = 'Accept: */*';
}

foreach ($headers as $name) {
if (strtolower($name) === 'content-length') {
$values = [0];

if (array_key_exists(CURLOPT_POSTFIELDS, $options)) {
$values = [strlen($options[CURLOPT_POSTFIELDS])];
}
} else {
$values = $request->getHeader($name);
}

foreach ($values as $value) {
$curlHeaders[] = $name . ': ' . $value;
$curlHeaders[] = $name. ': '.$value;
}
}

return $curlHeaders;
}
}

}
Loading