Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
kierwils committed Oct 10, 2024
1 parent dcc265b commit 0e1bbf5
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 109 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "codin/http-client",
"version": "1.0.0",
"description": "Tiny PSR-18 Http Client",
"license": "MIT",
"type": "library",
Expand All @@ -17,7 +18,7 @@
"psr/http-client-implementation": "1.0"
},
"require": {
"php": ">=7.4",
"php": ">=8.3",
"ext-curl": "*",
"nyholm/psr7": "@stable",
"psr/http-client": "@stable",
Expand Down Expand Up @@ -50,7 +51,7 @@
"phpstan analyse",
"phpspec run"
],
"uninstall": [
"clean": [
"rm -rf ./bin",
"rm -rf ./vendor",
"rm ./composer.lock"
Expand Down
1 change: 0 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ parameters:
bootstrapFiles:
- %currentWorkingDirectory%/vendor/autoload.php
inferPrivatePropertyTypeFromConstructor: true
checkMissingIterableValueType: false
154 changes: 70 additions & 84 deletions src/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,61 @@

namespace Codin\HttpClient;

use CurlHandle;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;

class HttpClient implements ClientInterface
readonly class HttpClient implements ClientInterface
{
public const VERSION = '1.0';

protected ResponseFactoryInterface $responseFactory;

protected StreamFactoryInterface $streamFactory;

protected array $options;

protected bool $debug;

protected array $metrics = [];

/**
* @var \CurlHandle
* @param array<string, mixed> $options
*/
protected $session;

public function __construct(
ResponseFactoryInterface $responseFactory,
StreamFactoryInterface $streamFactory,
array $options = [],
bool $debug = false
private ResponseFactoryInterface $responseFactory,
private StreamFactoryInterface $streamFactory,
private array $options = [],
) {
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory;
$this->options = $options;
$this->debug = $debug;
$this->session = curl_init();
}

public function __destruct()
private function parseHeaders(ResponseInterface $response, StreamInterface $headers): ResponseInterface
{
if (is_resource($this->session)) {
curl_close($this->session);
$data = rtrim((string) $headers);
$parts = explode("\r\n\r\n", $data);
$last = array_pop($parts);
$lines = explode("\r\n", $last);
$status = array_shift($lines);

if (is_string($status) && strpos($status, 'HTTP/') === 0) {
[$version, $status, $message] = explode(' ', substr($status, strlen('http/')), 3);
$response = $response->withProtocolVersion($version)->withStatus((int) $status, $message);
}

return array_reduce($lines, static function (ResponseInterface $response, string $line): ResponseInterface {
[$name, $value] = explode(':', $line, 2);
return $response->withHeader($name, $value);
}, $response);
}

public function getMetrics(): array
private function buildResponse(StreamInterface $headers, StreamInterface $body): ResponseInterface
{
return $this->metrics;
if ($body->isSeekable()) {
$body->rewind();
}
$response = $this->responseFactory->createResponse(200)->withBody($body);

return $this->parseHeaders($response, $headers);
}

protected function buildOptions(RequestInterface $request): array
/**
* @return array<int, mixed>
*/
private function buildOptions(RequestInterface $request): array
{
$options = [
CURLOPT_URL => (string) $request->getUri(),
Expand All @@ -71,16 +74,9 @@ protected function buildOptions(RequestInterface $request): array
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_COOKIEFILE => '',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CUSTOMREQUEST => $request->getMethod(),
];

if ('POST' === $request->getMethod()) {
$options[CURLOPT_POST] = true;
} elseif ('HEAD' === $request->getMethod()) {
$options[CURLOPT_NOBODY] = true;
} else {
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}

if ($request->getProtocolVersion() === '1.1') {
$options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($request->getProtocolVersion() === '2.0') {
Expand All @@ -102,15 +98,26 @@ protected function buildOptions(RequestInterface $request): array
}
}

if (in_array($request->getMethod(), ['PUT', 'POST', 'PATCH'])) {
if ('POST' !== $request->getMethod()) {
$options[CURLOPT_UPLOAD] = true;
if ($request->getBody()->getSize() > 0) {
$size = $request->hasHeader('Content-Length')
? (int) $request->getHeaderLine('Content-Length')
: null;

$options[CURLOPT_UPLOAD] = true;

// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$options[CURLOPT_HTTPHEADER][] = 'Expect:';
}

if ($request->hasHeader('Content-Length')) {
$options[CURLOPT_INFILESIZE] = $request->getHeader('Content-Length')[0];
} elseif (!$request->hasHeader('Transfer-Encoding')) {
$options[CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked';
// cURL sometimes adds a content-type by default. Prevent this.
if (!$request->hasHeader('Content-Type')) {
$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
}

if ($size !== null) {
$options[CURLOPT_INFILESIZE] = $size;
$request = $request->withoutHeader('Content-Length');
}

if ($request->getBody()->isSeekable() && $request->getBody()->tell() > 0) {
Expand All @@ -122,46 +129,20 @@ protected function buildOptions(RequestInterface $request): array
};
}

return $this->options + $options;
}

protected function parseHeaders(ResponseInterface $response, StreamInterface $headers): ResponseInterface
{
$data = rtrim((string) $headers);
$parts = explode("\r\n\r\n", $data);
$last = array_pop($parts);
$lines = explode("\r\n", $last);
$status = array_shift($lines);

if (is_string($status) && strpos($status, 'HTTP/') === 0) {
[$version, $status, $message] = explode(' ', substr($status, strlen('http/')), 3);
$response = $response->withProtocolVersion($version)->withStatus((int) $status, $message);
}

return array_reduce($lines, static function (ResponseInterface $response, string $line): ResponseInterface {
[$name, $value] = explode(':', $line, 2);
return $response->withHeader($name, $value);
}, $response);
}

protected function buildResponse(StreamInterface $headers, StreamInterface $body): ResponseInterface
{
if ($body->isSeekable()) {
$body->rewind();
}
$response = $this->responseFactory->createResponse(200)->withBody($body);

return $this->parseHeaders($response, $headers);
return $options;
}

protected function prepareSession(RequestInterface $request): array
/**
* @return array{0: StreamInterface, 1: StreamInterface}
*/
private function prepareSession(RequestInterface $request, CurlHandle $session): array
{
curl_setopt_array($this->session, $this->buildOptions($request));
curl_setopt_array($session, $this->buildOptions($request));

$headers = $this->streamFactory->createStream('');

curl_setopt(
$this->session,
$session,
CURLOPT_HEADERFUNCTION,
static function ($session, string $data) use ($headers): int {
return $headers->write($data);
Expand All @@ -171,7 +152,7 @@ static function ($session, string $data) use ($headers): int {
$body = $this->streamFactory->createStream('');

curl_setopt(
$this->session,
$session,
CURLOPT_WRITEFUNCTION,
static function ($session, string $data) use ($body): int {
return $body->write($data);
Expand All @@ -183,16 +164,21 @@ static function ($session, string $data) use ($body): int {

public function sendRequest(RequestInterface $request): ResponseInterface
{
[$headers, $body] = $this->prepareSession($request);
$session = curl_init();

[$headers, $body] = $this->prepareSession($request, $session);

$result = curl_exec($this->session);
if ($this->debug) {
$this->metrics = curl_getinfo($this->session);
$result = curl_exec($session);
if (isset($this->options['metrics']) && is_callable($this->options['metrics'])) {
$metrics = curl_getinfo($session);
$this->options['metrics']($metrics);
}
curl_reset($this->session);
$errorMessage = curl_error($session);
$errorCode = curl_errno($session);
curl_close($session);

if (false === $result) {
throw new Exceptions\TransportError(curl_error($this->session), curl_errno($this->session), $request);
throw new Exceptions\TransportError($errorMessage, $errorCode, $request);
}

$response = $this->buildResponse($headers, $body);
Expand Down
15 changes: 9 additions & 6 deletions src/MultipartBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@

class MultipartBuilder
{
protected StreamInterface $stream;
private StreamInterface $stream;

protected string $boundary;

public function __construct(StreamFactoryInterface $streamFactory, ?string $boundary = null)
{
public function __construct(
StreamFactoryInterface $streamFactory,
private ?string $boundary = null
) {
$this->stream = $streamFactory->createStream('');
$this->boundary = null === $boundary ? uniqid('', true) : $boundary;
}

protected function write(string $data, string $newline = "\r\n"): void
private function write(string $data, string $newline = "\r\n"): void
{
$this->stream->write($data . $newline);
}

/**
* @param array<string, string> $headers
*/
public function add(string $name, string $data, array $headers = []): void
{
$headers = array_change_key_case($headers);
Expand Down
30 changes: 14 additions & 16 deletions src/RequestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;

class RequestBuilder
{
Expand All @@ -24,35 +25,31 @@ public function __construct(
}

/**
* @param array $options['headers']
* @param array $options['query']
* @param StreamInterface $options['stream']
* @param string $options['body']
* @param array $options['json']
* @param array $options['multipart']
* @param array $options['form']
* @param array<string, mixed> $options
*/
public function build(string $method, string $url, array $options = []): RequestInterface
{
$request = $this->serverRequestFactory->createServerRequest(strtoupper($method), $url);

if (isset($options['headers'])) {
if (isset($options['headers']) && is_array($options['headers'])) {
foreach ($options['headers'] as $name => $value) {
$request = $request->withHeader($name, $value);
}
}

if (isset($options['query'])) {
$uri = $request->getUri()
->withQuery(http_build_query($options['query']));
$encoding = isset($options['encoding']) && is_int($options['encoding']) ? $options['encoding'] : PHP_QUERY_RFC1738;

if (isset($options['query']) && is_array($options['query'])) {
$queryString = http_build_query($options['query'], encoding_type: $encoding);
$uri = $request->getUri()->withQuery($queryString);
$request = $request->withUri($uri);
}

if (isset($options['stream'])) {
if (isset($options['stream']) && $options['stream'] instanceof StreamInterface) {
$request = $request->withBody($options['stream']);
}

if (isset($options['body'])) {
if (isset($options['body']) && is_string($options['body'])) {
$body = $this->streamFactory->createStream($options['body']);
$request = $request->withBody($body);
}
Expand All @@ -66,7 +63,7 @@ public function build(string $method, string $url, array $options = []): Request
$request = $request->withBody($body);
}

if (isset($options['multipart'])) {
if (isset($options['multipart']) && is_array($options['multipart'])) {
$multipart = new MultipartBuilder($this->streamFactory);

foreach ($options['multipart'] as $name => $value) {
Expand All @@ -76,8 +73,9 @@ public function build(string $method, string $url, array $options = []): Request
$request = $multipart->attach($request);
}

if (isset($options['form'])) {
$body = $this->streamFactory->createStream(http_build_query($options['form']));
if (isset($options['form']) && is_array($options['form'])) {
$queryString = http_build_query($options['form'], encoding_type: $encoding);
$body = $this->streamFactory->createStream($queryString);
$request = $request
->withBody($body)
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
Expand Down

0 comments on commit 0e1bbf5

Please sign in to comment.