diff --git a/src/Client.php b/src/Client.php index 444596a..a61f3a9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1,4 +1,5 @@ * @author Blake Williams + * @author Dmitry Arhitector * * @api * @since 1.0 */ class Client implements HttpClient, HttpAsyncClient { - /** + + /** * 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; + /** * Create new client @@ -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 = []) + { $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 * @@ -99,27 +91,24 @@ public function __destruct() * * @since 1.0 */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request, array $options = []) { - $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); } @@ -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 * @@ -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) { $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() + { + 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 @@ -208,7 +243,7 @@ private function createCurlOptions(RequestInterface $request) * * @return int */ - private function getProtocolVersion($requestVersion) + protected function getProtocolVersion($requestVersion) { switch ($requestVersion) { case '1.0': @@ -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; } @@ -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; } -} + +} \ No newline at end of file diff --git a/src/ResponseParser.php b/src/ResponseParser.php index 714f832..73f740f 100644 --- a/src/ResponseParser.php +++ b/src/ResponseParser.php @@ -1,4 +1,5 @@ + * @author Dmitry Arhitector */ class ResponseParser { + + /** + * Raw response headers + * + * @var array + */ + protected $headers = []; + /** * PSR-7 message factory * * @var MessageFactory */ - private $messageFactory; + protected $messageFactory; /** * PSR-7 stream factory * * @var StreamFactory */ - private $streamFactory; + protected $streamFactory; + + /** + * Temporary resource + * + * @var resource + */ + protected $temporaryStream; + + /** + * Receive redirect + * + * @var bool + */ + protected $followLocation = false; + /** * Create new parser. @@ -41,35 +66,90 @@ public function __construct(MessageFactory $messageFactory, StreamFactory $strea $this->streamFactory = $streamFactory; } + /** + * Get factory + * + * @return MessageFactory + */ + public function getMessageFactory() + { + return $this->messageFactory; + } + + /** + * Get factory + * + * @return StreamFactory + */ + public function getStreamFactory() + { + return $this->streamFactory; + } + + /** + * Temporary body (fix out of memory) + * + * @return resource + */ + public function getTemporaryStream() + { + if ( ! is_resource($this->temporaryStream)) + { + $this->temporaryStream = fopen('php://temp', 'w+'); + } + + return $this->temporaryStream; + } + /** * Parse cURL response * - * @param string $raw raw response - * @param array $info cURL response info + * @param resource $raw raw response + * @param array $info cURL response info * * @return ResponseInterface * * @throws \InvalidArgumentException * @throws \RuntimeException */ - public function parse($raw, array $info) + public function parse($raw = null, array $info) { - $response = $this->messageFactory->createResponse(); - - $headerSize = $info['header_size']; - $rawHeaders = substr($raw, 0, $headerSize); + if (empty($raw)) // fix promise out of memory + { + $raw = $this->getTemporaryStream(); + } $parser = new HeadersParser(); - $response = $parser->parseString($rawHeaders, $response); - /* - * substr can return boolean value for empty string. But createStream does not support - * booleans. Converting to string. - */ - $content = (string) substr($raw, $headerSize); - $stream = $this->streamFactory->createStream($content); - $response = $response->withBody($stream); + $response = $parser->parseArray($this->headers, $this->messageFactory->createResponse()); + $response = $response->withBody($this->streamFactory->createStream($raw)); + + $this->temporaryStream = null; return $response; } + + /** + * Save the response headers + * + * @param resource $handler curl handler + * @param string $header raw header + * + * @return integer + */ + public function headerHandler($handler, $header) + { + $this->headers[] = $header; + + if ($this->followLocation) { + $this->followLocation = false; + $this->headers = [$header]; + } else if ( ! trim($header)) { + $this->followLocation = true; + //$this->parse(null, []); + } + + return strlen($header); + } + } diff --git a/src/Tools/HeadersParser.php b/src/Tools/HeadersParser.php index 8644570..11b0c44 100644 --- a/src/Tools/HeadersParser.php +++ b/src/Tools/HeadersParser.php @@ -1,4 +1,5 @@ parseArray(explode("\r\n", $headers), $response); } -} +} \ No newline at end of file