meraproject/vendor/yoomoney/yookassa-sdk-php/lib/Client/BaseClient.php
keboss-m 5c21d25d45 Initial commit: Merakomis portal, Docker stack and user-reader API.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:04:05 +03:00

488 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 Exception;
use Psr\Log\LoggerInterface;
use YooKassa\Common\Exceptions\ApiConnectionException;
use YooKassa\Common\Exceptions\ApiException;
use YooKassa\Common\Exceptions\AuthorizeException;
use YooKassa\Common\Exceptions\BadApiRequestException;
use YooKassa\Common\Exceptions\ExtensionNotFoundException;
use YooKassa\Common\Exceptions\ForbiddenException;
use YooKassa\Common\Exceptions\InternalServerError;
use YooKassa\Common\Exceptions\JsonException;
use YooKassa\Common\Exceptions\NotFoundException;
use YooKassa\Common\Exceptions\ResponseProcessingException;
use YooKassa\Common\Exceptions\TooManyRequestsException;
use YooKassa\Common\Exceptions\UnauthorizedException;
use YooKassa\Common\LoggerWrapper;
use YooKassa\Common\ResponseObject;
use YooKassa\Helpers\Config\ConfigurationLoader;
use YooKassa\Helpers\Config\ConfigurationLoaderInterface;
use YooKassa\Helpers\SecurityHelper;
class BaseClient
{
/** Точка входа для запроса к API по магазину */
const ME_PATH = '/me';
/** Точка входа для запросов к API по платежам */
const PAYMENTS_PATH = '/payments';
/** Точка входа для запросов к API по возвратам */
const REFUNDS_PATH = '/refunds';
/** Точка входа для запросов к API по вебхукам */
const WEBHOOKS_PATH = '/webhooks';
/** Точка входа для запросов к API по чекам */
const RECEIPTS_PATH = '/receipts';
/** Точка входа для запросов к API по сделкам */
const DEALS_PATH = '/deals';
/** Точка входа для запросов к API по выплатам */
const PAYOUTS_PATH = '/payouts';
/** Точка входа для запросов к API по персональным данным */
const PERSONAL_DATA_PATH = '/personal_data';
/** Точка входа для запросов к API по участникам СБП */
const SBP_BANKS_PATH = '/sbp_banks';
/** Точка входа для запросов к API по самозанятым */
const SELF_EMPLOYED_PATH = '/self_employed';
/** Имя HTTP заголовка, используемого для передачи idempotence key */
const IDEMPOTENCY_KEY_HEADER = 'Idempotence-Key';
/**
* Значение по умолчанию времени ожидания между запросами при отправке повторного запроса в случае получения
* ответа с HTTP статусом 202
*/
const DEFAULT_DELAY = 1800;
/** Значение по умолчанию количества попыток получения информации от API если пришёл ответ с HTTP статусом 202 */
const DEFAULT_TRIES_COUNT = 3;
/** Значение по умолчанию количества попыток получения информации от API если пришёл ответ с HTTP статусом 202 */
const DEFAULT_ATTEMPTS_COUNT = 3;
/**
* CURL клиент
*
* @var null|ApiClientInterface
*/
protected $apiClient;
/**
* shopId магазина
*
* @var string|int
*/
protected $login;
/**
* Секретный ключ магазина
*
* @var string
*/
protected $password;
/**
* Настройки для CURL клиента
*
* @var array
*/
protected $config;
/**
* Время через которое будут осуществляться повторные запросы
*
* Значение по умолчанию - 1800 миллисекунд.
*
* @var int Значение в миллисекундах
*/
protected $timeout;
/**
* Количество повторных запросов при ответе API статусом 202
*
* Значение по умолчанию 3
*
* @var int
*/
protected $attempts;
/**
* Объект для логирования работы SDK
*
* @var LoggerInterface|null
*/
protected $logger;
/**
* Constructor
*
* @param ApiClientInterface|null $apiClient
* @param ConfigurationLoaderInterface|null $configLoader
*/
public function __construct(ApiClientInterface $apiClient = null, ConfigurationLoaderInterface $configLoader = null)
{
if ($apiClient === null) {
$apiClient = new CurlClient();
}
if ($configLoader === null) {
$configLoader = new ConfigurationLoader();
}
$config = $configLoader->load()->getConfig();
$this->setConfig($config);
$apiClient->setConfig($config);
$this->attempts = self::DEFAULT_ATTEMPTS_COUNT;
$this->apiClient = $apiClient;
}
/**
* Устанавливает авторизацию по логин/паролю
*
* @example 01-client.php 7 1 Пример авторизации
*
* @param string $login
* @param string $password
*
* @return $this
*/
public function setAuth($login, $password)
{
$this->login = $login;
$this->password = $password;
$this->apiClient
->setBearerToken(null)
->setShopId($this->login)
->setShopPassword($this->password);
return $this;
}
/**
* Устанавливает авторизацию по Oauth-токену
*
* @example 01-client.php 9 1 Пример авторизации
*
* @param string $token
*
* @return $this
*/
public function setAuthToken($token)
{
$this->apiClient
->setShopId(null)
->setShopPassword(null)
->setBearerToken($token);
return $this;
}
/**
* Возвращает CURL клиента для работы с API
*
* @return ApiClientInterface
*/
public function getApiClient()
{
return $this->apiClient;
}
/**
* Устанавливает CURL клиента для работы с API
*
* @param ApiClientInterface $apiClient
*
* @return $this
*/
public function setApiClient(ApiClientInterface $apiClient)
{
$this->apiClient = $apiClient;
$this->apiClient->setConfig($this->config);
$this->apiClient->setLogger($this->logger);
return $this;
}
/**
* Устанавливает логгер приложения
*
* @param null|callable|object|LoggerInterface $value Инстанс логгера
*/
public function setLogger($value)
{
if ($value === null || $value instanceof LoggerInterface) {
$this->logger = $value;
} else {
$this->logger = new LoggerWrapper($value);
}
if ($this->apiClient !== null) {
$this->apiClient->setLogger($this->logger);
}
}
/**
* Возвращает настройки клиента
*
* @return array
*/
public function getConfig()
{
return $this->config;
}
/**
* Устанавливает настройки клиента
*
* @param array $config
*/
public function setConfig($config)
{
$this->config = $config;
}
/**
* Установка значения задержки между повторными запросами
*
* @param int $timeout
*
* @return $this
*/
public function setRetryTimeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* Установка значения количества попыток повторных запросов при статусе 202
*
* @param int $attempts
*
* @return $this
*/
public function setMaxRequestAttempts($attempts)
{
$this->attempts = $attempts;
return $this;
}
/**
* Метод проверяет, находится ли IP адрес среди IP адресов Юkassa, с которых отправляются уведомления
*
* @param string $ip IPv4 или IPv6 адрес webhook уведомления
* @return bool
*
* @throws Exception Выбрасывается, если будет передан IP адрес неверного формата
*/
public function isNotificationIPTrusted($ip)
{
$securityHelper = new SecurityHelper();
return $securityHelper->isIPTrusted($ip);
}
/**
* Кодирует массив данных в JSON строку
*
* @param array $serializedData Массив данных для кодировки
*
* @return string Строка JSON
* @throws Exception Выбрасывается, если не удалось конвертировать данные в строку JSON
*/
protected function encodeData($serializedData)
{
if ($serializedData === array()) {
return '{}';
}
if (defined('JSON_UNESCAPED_UNICODE') && defined('JSON_UNESCAPED_SLASHES')) {
$encoded = json_encode($serializedData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
$encoded = self::unescaped(json_encode($serializedData));
}
if ($encoded === false) {
$errorCode = json_last_error();
throw new JsonException("Failed serialize json.", $errorCode);
}
return $encoded;
}
/**
* Убирает лишние обратные слэши, а также декодирует строку UTF-8 в нормальный вид
*
* Вспомогательная функция для старых версий PHP
*
* @param string $json Строка JSON
* @return string|false Отформатированная строка JSON
*/
private static function unescaped($json)
{
if ($json === false) {
return false;
}
$json = str_replace('\\/', '/', $json);
return preg_replace_callback('/\\\\u(\w{4})/', function ($matches) {
return html_entity_decode('&#x' . $matches[1] . ';', ENT_COMPAT, 'UTF-8');
}, $json);
}
/**
* Декодирует JSON строку в массив данных
*
* @param ResponseObject $response Объект ответа на запрос к API
*
* @return array Массив данных
*/
protected function decodeData(ResponseObject $response)
{
$resultArray = json_decode($response->getBody(), true);
if ($resultArray === null) {
throw new JsonException('Failed to decode response', json_last_error());
}
return $resultArray;
}
/**
* Выбрасывает исключение по коду ошибки
*
* @param ResponseObject $response
*
* @throws ApiException Неожиданный код ошибки.
* @throws BadApiRequestException Неправильный запрос. Чаще всего этот статус выдается из-за нарушения правил взаимодействия с API.
* @throws ForbiddenException Секретный ключ или OAuth-токен верный, но не хватает прав для совершения операции.
* @throws InternalServerError Технические неполадки на стороне ЮKassa. Результат обработки запроса неизвестен. Повторите запрос позднее с тем же ключом идемпотентности.
* @throws NotFoundException Ресурс не найден.
* @throws ResponseProcessingException Запрос был принят на обработку, но она не завершена.
* @throws TooManyRequestsException Превышен лимит запросов в единицу времени. Попробуйте снизить интенсивность запросов.
* @throws UnauthorizedException Неверное имя пользователя или пароль или невалидный OAuth-токен при аутентификации.
* @throws AuthorizeException Ошибка авторизации. Не установлен заголовок.
*/
protected function handleError(ResponseObject $response)
{
switch ($response->getCode()) {
case BadApiRequestException::HTTP_CODE:
throw new BadApiRequestException($response->getHeaders(), $response->getBody());
break;
case ForbiddenException::HTTP_CODE:
throw new ForbiddenException($response->getHeaders(), $response->getBody());
break;
case UnauthorizedException::HTTP_CODE:
throw new UnauthorizedException($response->getHeaders(), $response->getBody());
break;
case InternalServerError::HTTP_CODE:
throw new InternalServerError($response->getHeaders(), $response->getBody());
break;
case NotFoundException::HTTP_CODE:
throw new NotFoundException($response->getHeaders(), $response->getBody());
break;
case TooManyRequestsException::HTTP_CODE:
throw new TooManyRequestsException($response->getHeaders(), $response->getBody());
break;
case ResponseProcessingException::HTTP_CODE:
throw new ResponseProcessingException($response->getHeaders(), $response->getBody());
break;
default:
if ($response->getCode() > 399) {
throw new ApiException(
'Unexpected response error code',
$response->getCode(),
$response->getHeaders(),
$response->getBody()
);
}
}
}
/**
* Задержка между повторными запросами
*
* @param ResponseObject $response Объект ответа на запрос к API
*/
protected function delay($response)
{
$timeout = $this->timeout;
$responseData = $this->decodeData($response);
if ($timeout) {
$delay = $timeout;
} else {
if (isset($responseData['retry_after'])) {
$delay = $responseData['retry_after'];
} else {
$delay = self::DEFAULT_DELAY;
}
}
usleep($delay * 1000);
}
/**
* Выполнение запроса и обработка 202 статуса
*
* @param string $path URL запроса
* @param string $method HTTP метод
* @param array $queryParams Массив GET параметров запроса
* @param string|null $httpBody Тело запроса
* @param array $headers Массив заголовков запроса
*
* @return mixed|ResponseObject
* @throws ApiException
* @throws AuthorizeException
* @throws ApiConnectionException
* @throws ExtensionNotFoundException
*/
protected function execute($path, $method, $queryParams, $httpBody = null, $headers = array())
{
$attempts = $this->attempts;
$response = $this->apiClient->call($path, $method, $queryParams, $httpBody, $headers);
while (in_array($response->getCode(), array(202, 500)) && $attempts > 0) {
$this->delay($response);
$attempts--;
$response = $this->apiClient->call($path, $method, $queryParams, $httpBody, $headers);
}
return $response;
}
}