ulmus-api/src/Transport/CurlTransport.php

196 lines
7.0 KiB
PHP

<?php
namespace Ulmus\Api\Transport;
use Ulmus\Api\Common\{ ContentTypeEnum, MethodEnum, HttpHeaderEnum };
use Ulmus\Api\Stream\JsonStream;
use Ulmus\Api\Stream\Stream;
use Psr\Http\Message\{RequestInterface, ResponseInterface, StreamInterface};
use Ulmus\Api\Response\{ Response, JsonResponse };
abstract class CurlTransport {
# Seconds
public int $timeout = 30;
# Seconds
public int $connectTimeout = 300;
public int $maximumRedirections = 10;
protected StreamInterface $stderr;
public function __construct(
public array $headers = [],
public array $curlOptions = [],
public null|CurlAuthorization $authorization = null,
public ContentTypeEnum $contentType = ContentTypeEnum::Json,
# Matching Guzzle's great user-agent syntax
public string $userAgent = "ulmus-api/1.0 curl/{curl} php/{php}",
public bool $debug = false,
) {
$this->userAgent = str_replace([ '{curl}', '{php}', ], [ curl_version()['version'] ?? "???", phpversion() ], $this->userAgent);
}
public function post(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Post, $url, $data, $headers, $curlOptions);
}
public function get(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Get, $url, $data, $headers, $curlOptions);
}
public function delete(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Delete, $url, $data, $headers, $curlOptions);
}
public function patch(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Patch, $url, $data, $headers, $curlOptions);
}
public function put(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed
{
return $this->request(MethodEnum::Put, $url, $data, $headers, $curlOptions);
}
public function fromRequest(RequestInterface $request) : ResponseInterface
{
return $this->request(MethodEnum::from($request->getMethod()), $request->getUri(), $request->getBody(), $request->getHeaders());
}
public function request(MethodEnum $method, string $url, mixed $data = null, array $headers = [], array $options = []) : ResponseInterface
{
$response = new Response();
if ($this->debug) {
$this->stderr = Stream::fromMemory();
$stderrResource = $this->stderr->detach();
}
$headers = array_merge_recursive(HttpHeaderEnum::normalizeHeaderArray($headers), HttpHeaderEnum::normalizeHeaderArray($this->headers));
$options += $this->curlOptions;
$this->applyMethod($method, $data, $headers, $options);
$headers += $this->authorization->headers;
$options += $this->authorization->options;
$options += [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => HttpHeaderEnum::compileHeaders($headers),
CURLOPT_MAXREDIRS => $this->maximumRedirections,
CURLOPT_TIMEOUT => $this->timeout * 1000,
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADERFUNCTION => function($curl, $headerLine) use (& $response) {
if ( strpos($headerLine, ':') ) {
list($key, $value) = array_map('trim', explode(':', $headerLine));
$response = $response->withHeader($key, $value);
}
elseif ( strtoupper(substr($headerLine, 0, 4)) === 'HTTP' ) {
list(,$code, $status) = explode(' ', trim($headerLine), 3) + [ null, null, null ];
$response = $response->withStatus($code, $status);
}
return strlen($headerLine);
}
] + ( $this->debug ? [
CURLOPT_VERBOSE => true,
CURLOPT_STDERR => $stderrResource,
] : [] );
$ch = curl_init();
foreach($options as $opt => $value) {
curl_setopt($ch, $opt, $value);
}
if ( false === ( $result = curl_exec($ch) ) ) {
$errno = curl_errno($ch);
$errors = array_filter([ curl_error($ch) , CurlErrors::CURL_CODES[$errno] ?? null ]);
throw new \Exception(implode(PHP_EOL, $errors), $errno);
}
if ($this->debug) {
$this->stderr->attach($stderrResource);
}
$response = $response->withBody( Stream::fromMemory($result) );
if ( $response->hasHeader('Content-Type') && false !== strpos($response->getHeader('Content-Type')[0], 'json') ) {
$response = JsonResponse::fromResponse($response);
}
return $response;
}
protected function applyMethod(MethodEnum $method, mixed &$data, array &$headers, array &$options) : void
{
switch ($method) {
case MethodEnum::Patch:
case MethodEnum::Put:
case MethodEnum::Delete:
case MethodEnum::Post:
if ($method === MethodEnum::Post) {
$options[CURLOPT_POST] = true;
}
else {
$options[CURLOPT_CUSTOMREQUEST] = $method->value;
}
$this->arrayToPostFields($data, $headers, $options);
break;
case MethodEnum::Get:
default:
}
}
protected function arrayToPostFields(mixed &$data, array &$headers, array &$options) : void
{
$headers["Content-Type"] = $this->contentType->value;
switch($this->contentType) {
case ContentTypeEnum::Json:
if ($data instanceof JsonStream) {
$json = $data->getContents();
}
elseif (is_string($data) && empty($data)) {
$json = $this->contentType->defaultBody();
}
else {
$json = json_encode($data, JsonStream::DEFAULT_JSON_ENCODING_FLAGS);
}
$options[CURLOPT_POSTFIELDS] = $json;
$headers['Accept'] ??= $this->contentType->value;
break;
case ContentTypeEnum::UrlEncoded:
$options[CURLOPT_POSTFIELDS] = http_build_query($data, '', '&');
break;
case ContentTypeEnum::Multipart:
$options[CURLOPT_POSTFIELDS] = $data;
break;
}
}
protected function unindexQueryStringArrays(string $query) : string
{
return preg_replace("/\[[0-9]+\]/simU", "[]", $query);
}
}