Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change HTTP client in symfony/cache to Curl #296

Merged
merged 19 commits into from
May 23, 2024
Merged
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
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,
koriym marked this conversation as resolved.
Show resolved Hide resolved
) {
}

/** @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;
koriym marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading