Skip to content

Commit

Permalink
Merge pull request #296 from koriym/curl
Browse files Browse the repository at this point in the history
Change HTTP client in symfony/cache to Curl
  • Loading branch information
koriym authored May 23, 2024
2 parents e5fddca + 012e54c commit 35ac6ef
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 126 deletions.
7 changes: 2 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"require": {
"php": "^8.1",
"ext-filter": "*",
"ext-curl": "*",
"justinrainbow/json-schema": "^5.2.10",
"koriym/attributes": "^1.0",
"koriym/http-constants": "^1.1",
Expand All @@ -22,11 +23,7 @@
"ray/aop": "^2.12.3",
"ray/di": "^2.13",
"ray/web-param-module": "^2.1.1",
"rize/uri-template": "^0.3",
"symfony/http-client": "^5.2 || ^6.0 || ^7.0",
"symfony/http-client-contracts": "^2.3 || ^3.0",
"nikic/php-parser": "^4.10",
"symfony/polyfill-php81": "^1.23"
"rize/uri-template": "^0.3"
},
"require-dev": {
"phpunit/phpunit": "^9.5.10",
Expand Down
121 changes: 121 additions & 0 deletions src/HttpRequestCurl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace BEAR\Resource;

use CurlHandle;

use function count;
use function curl_close;
use function curl_exec;
use function curl_getinfo;
use function curl_init;
use function curl_setopt;
use function explode;
use function http_build_query;
use function json_decode;
use function strpos;
use function strtolower;
use function substr;
use function trim;

use const CURLINFO_CONTENT_TYPE;
use const CURLINFO_HEADER_SIZE;
use const CURLINFO_HTTP_CODE;
use const CURLOPT_CUSTOMREQUEST;
use const CURLOPT_HEADER;
use const CURLOPT_HTTPHEADER;
use const CURLOPT_POSTFIELDS;
use const CURLOPT_RETURNTRANSFER;
use const CURLOPT_URL;

/**
* Sends a HTTP request using cURL
*
* @psalm-type RequestOptions = array<null>|array{"body?": string, "headers?": array<string, string>}
* @psalm-type RequestHeaders = array<string, string>
* @psalm-type Body = array<mixed>
*/
final class HttpRequestCurl implements HttpRequestInterface
{
public function __construct(
private HttpRequestHeaders $requestHeaders,
) {
}

/** @inheritdoc */
public function request(string $method, string $uri, array $query): array
{
$body = http_build_query($query);
$curl = $this->initializeCurl($method, $uri, $body);
$response = (string) curl_exec($curl);
$code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
$headerSize = (int) curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$headerString = substr($response, 0, $headerSize);
$view = substr($response, $headerSize);
$headers = $this->parseResponseHeaders($headerString);
curl_close($curl);

$body = $this->parseBody($curl, $view);

return [
'code' => $code,
'headers' => $headers,
'body' => $body,
'view' => $view,
];
}

private function initializeCurl(string $method, string $uri, string $body): CurlHandle
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_URL, $uri);

if ($this->requestHeaders->headers !== []) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->requestHeaders->headers);
}

if ($body !== '') {
// Set the request body
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
}

curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);

return $curl;
}

/** @return array<string, string> */
private function parseResponseHeaders(string $responseHeaders): array
{
$responseHeadersArray = [];
$headerLines = explode("\r\n", $responseHeaders);
foreach ($headerLines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) !== 2) {
continue;
}

$key = $parts[0];

$responseHeadersArray[$key] = trim($parts[1]);
}

return $responseHeadersArray;
}

/** @return array<mixed> */
private function parseBody(CurlHandle $curl, string $view): array
{
$responseBody = [];
$contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
if (strpos(strtolower($contentType), 'application/json') !== false) {
return (array) json_decode($view, true);
}

return $responseBody;
}
}
14 changes: 14 additions & 0 deletions src/HttpRequestHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace BEAR\Resource;

final class HttpRequestHeaders
{
/** @param array<string> $headers */
public function __construct(
public array $headers = [],
) {
}
}
27 changes: 27 additions & 0 deletions src/HttpRequestInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace BEAR\Resource;

/**
* Sends a HTTP request
*/
interface HttpRequestInterface
{
/**
* Sends a HTTP request
*
* @param string $method The HTTP method (GET, POST, PUT, DELETE, etc.).
* @param string $uri The URL of the request.
* @param array<string, mixed> $query An associative array of query parameters.
*
* @return array{body: array<mixed>, code: int, headers: array<string, string>, view: string}
* An associative array containing the response information.
* - code: The HTTP response code.
* - headers: An array of response headers.
* - body: The parsed response body.
* - view: The raw response body.
*/
public function request(string $method, string $uri, array $query): array;
}
96 changes: 13 additions & 83 deletions src/HttpResourceObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@

namespace BEAR\Resource;

use BadFunctionCallException;
use InvalidArgumentException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

use function count;
use function is_array;
use function strtoupper;
use function ucwords;

/**
* @method HttpResourceObject get(AbstractUri|string $uri, array $params = [])
Expand All @@ -28,78 +20,9 @@
*/
final class HttpResourceObject extends ResourceObject implements InvokeRequestInterface
{
/** {@inheritDoc} */
public $body;

/** @psalm-suppress PropertyNotSetInConstructor */
private ResponseInterface $response;

public function __construct(
private readonly HttpClientInterface $client,
private HttpRequestInterface $httpRequest,
) {
unset($this->code, $this->headers, $this->body, $this->view);
}

/**
* @param 'code'|'headers'|'body'|'view'|string $name
*
* @return array<int|string, mixed>|int|string
*/
public function __get(string $name): array|int|string
{
if ($name === 'code') {
return $this->response->getStatusCode();
}

if ($name === 'headers') {
/** @var array<string, array<string>> $headers */
$headers = $this->response->getHeaders();

return $this->formatHeader($headers);
}

if ($name === 'body') {
return $this->response->toArray();
}

if ($name === 'view') {
return $this->response->getContent();
}

throw new InvalidArgumentException($name);
}

/**
* @param array<string, array<string>> $headers
*
* @return array<string, string|array<string>>
*/
private function formatHeader(array $headers): array
{
$formated = [];
foreach ($headers as $key => $header) {
$ucFirstKey = ucwords($key);
$formated[$ucFirstKey] = count($header) === 1 ? $header[0] : $header;
}

return $formated;
}

public function __set(string $name, mixed $value): void
{
unset($value);

throw new BadFunctionCallException($name);
}

public function __isset(string $name): bool
{
return isset($this->{$name});
}

public function __toString(): string
{
return $this->response->getContent();
}

/** @SuppressWarnings(PHPMD.CamelCaseMethodName) */
Expand All @@ -110,15 +33,22 @@ public function _invokeRequest(InvokerInterface $invoker, AbstractRequest $reque
return $this->request($request);
}

public function request(AbstractRequest $request): self
public function request(AbstractRequest $request): ResourceObject
{
$uri = $request->resourceObject->uri;
$method = strtoupper($uri->method);
$options = $method === 'GET' ? ['query' => $uri->query] : ['body' => $uri->query];
$clientOptions = isset($uri->query['_options']) && is_array($uri->query['_options']) ? $uri->query['_options'] : [];
$options += $clientOptions;
$this->response = $this->client->request($method, (string) $uri, $options);
[
'code' => $this->code,
'headers' => $this->headers,
'body' => $this->body,
'view' => $this->view,
] = $this->httpRequest->request($method, (string) $uri, $uri->query);

return $this;
}

public function __toString(): string
{
return $this->view;
}
}
9 changes: 6 additions & 3 deletions src/Module/HttpClientModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace BEAR\Resource\Module;

use BEAR\Resource\HttpRequestCurl;
use BEAR\Resource\HttpRequestHeaders;
use BEAR\Resource\HttpRequestInterface;
use Ray\Di\AbstractModule;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* Provides HttpClientInterface bindings
* Provides HttpRequestCurl bindings
*/
final class HttpClientModule extends AbstractModule
{
Expand All @@ -17,6 +19,7 @@ final class HttpClientModule extends AbstractModule
*/
protected function configure(): void
{
$this->bind(HttpClientInterface::class)->toProvider(HttpClientProvider::class);
$this->bind(HttpRequestInterface::class)->to(HttpRequestCurl::class);
$this->bind(HttpRequestHeaders::class);
}
}
21 changes: 0 additions & 21 deletions src/Module/HttpClientProvider.php

This file was deleted.

Loading

0 comments on commit 35ac6ef

Please sign in to comment.