Viewing file: CurlClient.php (14.56 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * The MIT License. * * Copyright (c) 2023 "YooMoney", NBСO LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */
namespace YooKassa\Client;
use CurlHandle; use Psr\Log\LoggerInterface; use YooKassa\Common\Exceptions\ApiConnectionException; use YooKassa\Common\Exceptions\ApiException; use YooKassa\Common\Exceptions\AuthorizeException; use YooKassa\Common\Exceptions\ExtensionNotFoundException; use YooKassa\Common\ResponseObject; use YooKassa\Helpers\RawHeadersParser;
/** * Класс, представляющий модель CurlClient. * * Класс клиента Curl запросов. * * @category Class * @package YooKassa * @author cms@yoomoney.ru * @link https://yookassa.ru/developers/api */ class CurlClient implements ApiClientInterface { /** @var array Настройки клиента */ private array $config = [];
/** @var int|null shopId магазина */ private ?int $shopId = null;
/** @var string|null Секретный ключ магазина */ private ?string $shopPassword = null;
/** @var null|string OAuth токен */ private ?string $bearerToken = null;
/** @var int Настройка параметра CURLOPT_TIMEOUT */ private int $timeout = 80;
/** @var int Настройка параметра CURLOPT_CONNECTTIMEOUT */ private int $connectionTimeout = 30;
/** @var null|string Настройка прокси-сервера, если нужен */ private ?string $proxy = null;
/** @var UserAgent Строка user-agent для статистики */ private UserAgent $userAgent;
/** @var bool Настройка удержания соединения */ private bool $keepAlive = true;
/** @var array Заголовки по умолчанию */ private array $defaultHeaders = [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ];
/** @var CurlHandle|null Текущий ресурс для работы с curl */ private ?CurlHandle $curl = null;
/** @var null|LoggerInterface Объект для логирования запросов */ private ?LoggerInterface $logger = null;
/** * CurlClient constructor. */ public function __construct() { $this->userAgent = new UserAgent(); }
public function setLogger(?LoggerInterface $logger): void { $this->logger = $logger; }
/** * {@inheritdoc} * * @param string $path URL запроса * @param string $method HTTP метод * @param array $queryParams Массив GET параметров запроса * @param null|string $httpBody Тело запроса * @param array $headers Массив заголовков запроса * * @throws ApiConnectionException * @throws ApiException * @throws AuthorizeException * @throws ExtensionNotFoundException */ public function call(string $path, string $method, array $queryParams, ?string $httpBody = null, array $headers = []): ResponseObject { $headers = $this->prepareHeaders($headers);
$this->logRequestParams($path, $method, $queryParams, $httpBody, $headers);
$url = $this->prepareUrl($path, $queryParams);
$this->prepareCurl($method, $url, $httpBody, $this->implodeHeaders($headers));
[$httpHeaders, $httpBody, $responseInfo] = $this->sendRequest();
if (!$this->keepAlive) { $this->closeCurlConnection(); }
$this->logResponse($httpBody, $responseInfo, $httpHeaders);
return new ResponseObject([ 'code' => $responseInfo['http_code'], 'headers' => $httpHeaders, 'body' => $httpBody, ]); }
/** * Устанавливает параметры CURL. * * @param string $optionName Имя параметра * @param mixed $optionValue Значение параметра */ public function setCurlOption(string $optionName, mixed $optionValue): bool { return curl_setopt($this->curl, $optionName, $optionValue); }
/** * Close connection. */ public function closeCurlConnection(): void { if (null !== $this->curl) { curl_close($this->curl); } }
/** * Выполняет запрос, получает и возвращает обработанный ответ * * @throws ApiConnectionException */ public function sendRequest(): array { $response = curl_exec($this->curl); $httpHeaderSize = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); $httpHeaders = RawHeadersParser::parse(substr($response, 0, $httpHeaderSize)); $httpBody = substr($response, $httpHeaderSize); $responseInfo = curl_getinfo($this->curl); $curlError = curl_error($this->curl); $curlErrno = curl_errno($this->curl); if (false === $response) { $this->handleCurlError($curlError, $curlErrno); }
return [$httpHeaders, $httpBody, $responseInfo]; }
/** * Устанавливает тело запроса. * * @param string $method HTTP метод * @param string|null $httpBody Тело запроса */ public function setBody(string $method, ?string $httpBody = null): void { $this->setCurlOption(CURLOPT_CUSTOMREQUEST, $method); if (!empty($httpBody)) { $this->setCurlOption(CURLOPT_POSTFIELDS, $httpBody); } }
/** * Устанавливает shopId магазина. * * @param mixed $shopId shopId магазина * * @return $this */ public function setShopId(mixed $shopId): self { $this->shopId = (int) $shopId;
return $this; }
/** * Устанавливает секретный ключ магазина. * * @param string|null $shopPassword Секретный ключ магазина * * @return $this */ public function setShopPassword(?string $shopPassword): self { $this->shopPassword = $shopPassword;
return $this; }
/** * Возвращает значение параметра CURLOPT_TIMEOUT. */ public function getTimeout(): int { return $this->timeout; }
/** * Устанавливает значение параметра CURLOPT_TIMEOUT. * * @param int $timeout Максимальное количество секунд для выполнения функций cURL */ public function setTimeout(int $timeout): void { $this->timeout = $timeout; }
/** * Возвращает значение параметра CURLOPT_CONNECTTIMEOUT. */ public function getConnectionTimeout(): int { return $this->connectionTimeout; }
/** * Устанавливает значение параметра CURLOPT_CONNECTTIMEOUT. * * @param int $connectionTimeout Число секунд ожидания при попытке подключения */ public function setConnectionTimeout(int $connectionTimeout = 30): void { $this->connectionTimeout = $connectionTimeout; }
/** * Возвращает настройки прокси. */ public function getProxy(): string { return $this->proxy; }
/** * Устанавливает настройки прокси. * * @param string $proxy Прокси сервер */ public function setProxy(string $proxy): void { $this->proxy = $proxy; }
/** * Возвращает настройки. */ public function getConfig(): array { return $this->config; }
/** * Устанавливает настройки. * * @param array $config Настройки клиента */ public function setConfig(array $config): void { $this->config = $config; }
/** * Возвращает UserAgent. */ public function getUserAgent(): UserAgent { return $this->userAgent; }
/** * Устанавливает OAuth-токен магазина. * * @param null|string $bearerToken OAuth-токен магазина * * @return $this */ public function setBearerToken(?string $bearerToken): self { $this->bearerToken = $bearerToken;
return $this; }
/** * Устанавливает флаг сохранения соединения. * * @param bool $keepAlive Флаг сохранения настроек * * @return $this */ public function setKeepAlive(bool $keepAlive): self { $this->keepAlive = $keepAlive;
return $this; }
public function setAdvancedCurlOptions(): void { }
/** * @return void * * @throws ExtensionNotFoundException */ private function initCurl(): void { if (!extension_loaded('curl')) { throw new ExtensionNotFoundException('curl'); }
if (!$this->curl || !$this->keepAlive) { $this->curl = curl_init(); }
}
/** * @throws ApiConnectionException */ private function handleCurlError(string $error, int $errno): void { $msg = match ($errno) { CURLE_COULDNT_CONNECT, CURLE_COULDNT_RESOLVE_HOST, CURLE_OPERATION_TIMEOUTED => 'Could not connect to YooKassa API. Please check your internet connection and try again.', CURLE_SSL_CACERT, CURLE_SSL_PEER_CERTIFICATE => 'Could not verify SSL certificate.', default => 'Unexpected error communicating.', }; $msg .= sprintf("\n\n(Network error [errno %s]: %s)", $errno, $error);
throw new ApiConnectionException($msg); }
/** * Возвращает базовый URL для API Кассы * * @return string */ private function getUrl(): string { $config = $this->config;
return (string) $config['url']; }
/** * @throws AuthorizeException */ private function prepareHeaders(array $headers): array { $headers = array_merge($this->defaultHeaders, $headers);
$headers[UserAgent::HEADER] = $this->getUserAgent()->getHeaderString();
if ($this->shopId && $this->shopPassword) { $encodedAuth = base64_encode($this->shopId . ':' . $this->shopPassword); $headers['Authorization'] = 'Basic ' . $encodedAuth; } elseif ($this->bearerToken) { $headers['Authorization'] = 'Bearer ' . $this->bearerToken; }
if (empty($headers['Authorization'])) { throw new AuthorizeException('Authorization headers not set'); }
return $headers; }
private function implodeHeaders(array $headers): array { return array_map(static fn ($key, $value) => $key . ':' . $value, array_keys($headers), $headers); }
private function logRequestParams(string $path, string $method, array $queryParams = [], ?string $httpBody = null, array $headers = []): void { if (null !== $this->logger) { $message = 'Send request: ' . $method . ' ' . $path; $context = []; if (!empty($queryParams)) { $context['_params'] = $queryParams; } if (!empty($httpBody)) { $data = json_decode($httpBody, true); if (JSON_ERROR_NONE !== json_last_error()) { $data = $httpBody; } $context['_body'] = $data; } if (!empty($headers)) { $context['_headers'] = $headers; } $this->logger->info($message, $context); } }
private function prepareUrl(string $path, array $queryParams): string { $url = $this->getUrl() . $path;
if (!empty($queryParams)) { $url .= '?' . http_build_query($queryParams); }
return $url; }
private function logResponse(string $httpBody, array $responseInfo, array $httpHeaders): void { if (null !== $this->logger) { $message = 'Response with code ' . $responseInfo['http_code'] . ' received.'; $context = []; if (!empty($httpBody)) { $data = json_decode($httpBody, true); if (JSON_ERROR_NONE !== json_last_error()) { $data = $httpBody; } $context['_body'] = $data; } if (!empty($httpHeaders)) { $context['_headers'] = $httpHeaders; } $this->logger->info($message, $context); } }
/** * @throws ExtensionNotFoundException */ private function prepareCurl(string $method, string $url, ?string $httpBody = null, array $headers = []): void { $this->initCurl();
$this->setCurlOption(CURLOPT_URL, $url);
$this->setCurlOption(CURLOPT_RETURNTRANSFER, true);
$this->setCurlOption(CURLOPT_HEADER, true);
$this->setCurlOption(CURLOPT_BINARYTRANSFER, true);
if ($this->proxy) { $this->setCurlOption(CURLOPT_PROXY, $this->proxy); $this->setCurlOption(CURLOPT_HTTPPROXYTUNNEL, true); }
$this->setBody($method, $httpBody);
$this->setCurlOption(CURLOPT_HTTPHEADER, $headers);
$this->setCurlOption(CURLOPT_CONNECTTIMEOUT, $this->connectionTimeout);
$this->setCurlOption(CURLOPT_TIMEOUT, $this->timeout);
$this->setAdvancedCurlOptions(); } }
|