Поддержка мониторинга (#8) и рефакторинг

- абстрактный класс AtolClient:
    - больше не наследуется от клиента guzzle, но содержит его объект
    - из Kkt вынесены методы, отвечающие за формирование запроса, отправку и получение ответа, в т.ч. авторизацию
- переименованы исключения TooLongKktLoginException, TooLongKktPasswordException, EmptyKktLoginException и EmptyKktPasswordException
- мелочи по AuthFailedException
- заготовки тестов AtolClient и KktMonitor
This commit is contained in:
Anthony Axenov 2021-11-18 00:01:53 +08:00
parent 77481884ad
commit 03591600dd
11 changed files with 544 additions and 47 deletions

298
src/Api/AtolClient.php Normal file
View File

@ -0,0 +1,298 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types = 1);
namespace AtolOnline\Api;
use AtolOnline\{
Constants\Constraints,
Exceptions\AuthFailedException,
Exceptions\EmptyLoginException,
Exceptions\EmptyPasswordException,
Exceptions\TooLongLoginException,
Exceptions\TooLongPasswordException};
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
/**
* Класс для подключения АТОЛ Онлайн API
*/
abstract class AtolClient
{
/**
* @var bool Флаг тестового режима
*/
protected bool $test_mode = true;
/**
* @var Client HTTP-клиент для работы с API
*/
protected Client $http;
/**
* @var string|null Логин доступа к API
*/
private ?string $login = null;
/**
* @var string|null Пароль доступа к API (readonly)
*/
private ?string $password = null;
/**
* @var string|null Токен авторизации
*/
private ?string $token = null;
/**
* @var KktResponse|null Последний ответ сервера АТОЛ
*/
private ?KktResponse $response;
/**
* Конструктор
*
* @param string|null $login
* @param string|null $password
* @param array $config
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws TooLongLoginException
* @throws TooLongPasswordException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html Допустимые параметры для $config
*/
public function __construct(
?string $login = null,
?string $password = null,
array $config = []
) {
$this->http = new Client(array_merge($config, [
'http_errors' => $config['http_errors'] ?? false,
]));
$login && $this->setLogin($login);
$password && $this->setPassword($password);
}
/**
* Возвращает установленный флаг тестового режима
*
* @return bool
*/
public function isTestMode(): bool
{
return $this->test_mode;
}
/**
* Устанавливает флаг тестового режима
*
* @param bool $test_mode
* @return $this
*/
public function setTestMode(bool $test_mode): self
{
$this->test_mode = $test_mode;
return $this;
}
/**
* Возвращает текущий токен авторизации
*
* @return string|null
*/
public function getToken(): ?string
{
return $this->token;
}
/**
* Устанавливает токен авторизации
*
* @param string|null $token
* @return $this
*/
public function setToken(?string $token): AtolClient
{
$this->token = $token;
return $this;
}
/**
* Возвращает последний ответ сервера
*
* @return KktResponse|null
*/
public function getResponse(): ?KktResponse
{
return $this->response;
}
/**
* Возвращает логин доступа к API
*
* @return string|null
*/
protected function getLogin(): ?string
{
return $this->login;
}
/**
* Устанавливает логин доступа к API
*
* @param string $login
* @return $this
* @throws EmptyLoginException
* @throws TooLongLoginException
*/
public function setLogin(string $login): self
{
$login = trim($login);
if (empty($login)) {
throw new EmptyLoginException();
} elseif (mb_strlen($login) > Constraints::MAX_LENGTH_LOGIN) {
throw new TooLongLoginException($login, Constraints::MAX_LENGTH_LOGIN);
}
$this->login = $login;
return $this;
}
/**
* Возвращает пароль доступа к API
*
* @return string|null
*/
protected function getPassword(): ?string
{
return $this->password;
}
/**
* Устанавливает пароль доступа к API
*
* @param string $password
* @return $this
* @throws EmptyPasswordException Пароль ККТ не может быть пустым
* @throws TooLongPasswordException Слишком длинный пароль ККТ
*/
public function setPassword(string $password): self
{
if (empty($password)) {
throw new EmptyPasswordException();
} elseif (mb_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) {
throw new TooLongPasswordException($password, Constraints::MAX_LENGTH_PASSWORD);
}
$this->password = $password;
return $this;
}
/**
* Возвращает набор заголовков для HTTP-запроса
*
* @return array
*/
private function getHeaders(): array
{
$headers['Content-type'] = 'application/json; charset=utf-8';
if ($this->getToken()) {
$headers['Token'] = $this->getToken();
}
return $headers;
}
/**
* Возвращает полный URL для запроса
*
* @param string $method
* @return string
*/
protected function getUrlToMethod(string $method): string
{
return $this->getMainEndpoint() . '/' . trim($method);
}
/**
* Отправляет авторизационный запрос на сервер АТОЛ и возвращает авторизационный токен
*
* @return string|null
* @throws AuthFailedException
* @throws EmptyPasswordException
* @throws EmptyLoginException
* @throws GuzzleException
*/
protected function doAuth(): ?string
{
$result = $this->sendRequest('POST', $this->getAuthEndpoint(), [
'login' => $this->getLogin() ?? throw new EmptyLoginException(),
'pass' => $this->getPassword() ?? throw new EmptyPasswordException(),
]);
if (!$result->isValid() || !$result->getContent()->token) {
throw new AuthFailedException($result);
}
return $result->getContent()?->token;
}
/**
* Отправляет запрос и возвращает декодированный ответ
*
* @param string $http_method Метод HTTP
* @param string $url URL
* @param array|null $data Данные для передачи
* @param array|null $options Параметры Guzzle
* @return KktResponse
* @throws GuzzleException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
protected function sendRequest(
string $http_method,
string $url,
?array $data = null,
?array $options = null
): KktResponse {
$http_method = strtoupper(trim($http_method));
$options['headers'] = array_merge($this->getHeaders(), $options['headers'] ?? []);
if ($http_method != 'GET') {
$options['json'] = $data;
}
$response = $this->http->request($http_method, $url, $options);
return $this->response = new KktResponse($response);
}
/**
* Выполняет авторизацию на сервере АТОЛ
*
* Авторизация выолнится только если неизвестен токен
*
* @param string|null $login
* @param string|null $password
* @return bool
* @throws AuthFailedException
* @throws TooLongLoginException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws TooLongPasswordException
* @throws GuzzleException
*/
abstract public function auth(?string $login = null, ?string $password = null): bool;
/**
* Возвращает URL для запроса авторизации
*
* @return string
*/
abstract protected function getAuthEndpoint(): string;
/**
* Возвращает URL для запросов
*
* @return string
*/
abstract protected function getMainEndpoint(): string;
}

View File

@ -17,15 +17,15 @@ use AtolOnline\{
Entities\Document,
Exceptions\AuthFailedException,
Exceptions\EmptyCorrectionInfoException,
Exceptions\EmptyKktLoginException,
Exceptions\EmptyKktPasswordException,
Exceptions\EmptyLoginException,
Exceptions\EmptyPasswordException,
Exceptions\InvalidCallbackUrlException,
Exceptions\InvalidDocumentTypeException,
Exceptions\InvalidInnLengthException,
Exceptions\InvalidUuidException,
Exceptions\TooLongCallbackUrlException,
Exceptions\TooLongKktLoginException,
Exceptions\TooLongKktPasswordException,
Exceptions\TooLongLoginException,
Exceptions\TooLongPasswordException,
Exceptions\TooLongPaymentAddressException,
Exceptions\TooManyItemsException,
Exceptions\TooManyVatsException,
@ -70,10 +70,6 @@ class Kkt extends Client
* @param string|null $pass
* @param bool $test_mode Флаг тестового режима
* @param array $guzzle_config Конфигурация GuzzleHttp
* @throws EmptyKktLoginException Логин ККТ не может быть пустым
* @throws TooLongKktLoginException Слишком длинный логин ККТ
* @throws EmptyKktPasswordException Пароль ККТ не может быть пустым
* @throws TooLongKktPasswordException Слишком длинный пароль ККТ
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
public function __construct(
@ -126,15 +122,15 @@ class Kkt extends Client
*
* @param string $login
* @return $this
* @throws EmptyKktLoginException Логин ККТ не может быть пустым
* @throws TooLongKktLoginException Слишком длинный логин ККТ
* @throws EmptyLoginException Логин ККТ не может быть пустым
* @throws TooLongLoginException Слишком длинный логин ККТ
*/
public function setLogin(string $login): Kkt
{
if (empty($login)) {
throw new EmptyKktLoginException();
throw new EmptyLoginException();
} elseif (mb_strlen($login) > Constraints::MAX_LENGTH_LOGIN) {
throw new TooLongKktLoginException($login, Constraints::MAX_LENGTH_LOGIN);
throw new TooLongLoginException($login, Constraints::MAX_LENGTH_LOGIN);
}
$this->kkt_config['prod']['login'] = $login;
return $this;
@ -155,15 +151,15 @@ class Kkt extends Client
*
* @param string $password
* @return $this
* @throws EmptyKktPasswordException Пароль ККТ не может быть пустым
* @throws TooLongKktPasswordException Слишком длинный пароль ККТ
* @throws EmptyPasswordException Пароль ККТ не может быть пустым
* @throws TooLongPasswordException Слишком длинный пароль ККТ
*/
public function setPassword(string $password): Kkt
{
if (empty($password)) {
throw new EmptyKktPasswordException();
throw new EmptyPasswordException();
} elseif (mb_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) {
throw new TooLongKktPasswordException($password, Constraints::MAX_LENGTH_PASSWORD);
throw new TooLongPasswordException($password, Constraints::MAX_LENGTH_PASSWORD);
}
$this->kkt_config['prod']['pass'] = $password;
return $this;
@ -208,16 +204,6 @@ class Kkt extends Client
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['callback_url'];
}
/**
* Возвращает последний ответ сервера
*
* @return KktResponse|null
*/
public function getLastResponse(): ?KktResponse
{
return $this->last_response;
}
/**
* Возвращает флаг тестового режима
*

124
src/Api/KktMonitor.php Normal file
View File

@ -0,0 +1,124 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types = 1);
namespace AtolOnline\Api;
use GuzzleHttp\Exception\GuzzleException;
use stdClass;
/**
* Класс для мониторинга ККТ
*
* @see https://online.atol.ru/files/API_service_information.pdf Документация
*/
class KktMonitor extends AtolClient
{
/**
* @inheritDoc
*/
protected function getAuthEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/api/auth/v1/gettoken'
: 'https://online.atol.ru/api/auth/v1/gettoken';
}
/**
* @inheritDoc
*/
protected function getMainEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/api/kkt/v1'
: 'https://online.atol.ru/api/kkt/v1';
}
/**
* @inheritDoc
*/
public function auth(?string $login = null, ?string $password = null): bool
{
if (empty($this->getToken())) {
$login && $this->setLogin($login);
$password && $this->setPassword($password);
if ($token = $this->doAuth()) {
$this->setToken($token);
}
}
return !empty($this->getToken());
}
/**
* Получает от API информацию обо всех ККТ и ФН в рамках группы
*
* @param int|null $limit
* @param int|null $offset
* @return KktResponse
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9
*/
protected function fetchAll(?int $limit = null, ?int $offset = null): KktResponse
{
$params = [];
$limit && $params['limit'] = $limit;
$offset && $params['offset'] = $offset;
return $this->sendRequest('GET', self::getUrlToMethod('cash-registers'), $params);
}
/**
* Возвращает информацию обо всех ККТ и ФН в рамках группы
*
* @param int|null $limit
* @param int|null $offset
* @return KktResponse
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9
*/
public function getAll(?int $limit = null, ?int $offset = null): KktResponse
{
return $this->fetchAll($limit, $offset);
}
/**
* Получает от API информацию о конкретной ККТ по её серийному номеру
*
* @param string $serial_number
* @return KktResponse
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11
*/
protected function fetchOne(string $serial_number): KktResponse
{
return $this->sendRequest(
'GET',
self::getUrlToMethod('cash-registers') . '/' . trim($serial_number),
options: [
'headers' => [
'Accept' => 'application/hal+json',
],
]
);
}
/**
* Возвращает информацию о конкретной ККТ по её серийному номеру
*
* @todo кастовать к отдельному классу со своими геттерами
* @param string $serial_number
* @return stdClass
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11
*/
public function getOne(string $serial_number): stdClass
{
return $this->fetchOne($serial_number)->getContent()->data;
}
}

View File

@ -14,6 +14,7 @@ namespace AtolOnline\Api;
use JsonSerializable;
use Psr\Http\Message\ResponseInterface;
use stdClass;
use Stringable;
/**
* Класс AtolResponse, описывающий ответ от ККТ
@ -21,7 +22,7 @@ use stdClass;
* @property mixed $error
* @package AtolOnline\Api
*/
class KktResponse implements JsonSerializable
class KktResponse implements JsonSerializable, Stringable
{
/**
* @var int Код ответа сервера
@ -29,9 +30,9 @@ class KktResponse implements JsonSerializable
protected int $code;
/**
* @var stdClass Содержимое ответа сервера
* @var stdClass|array|null Содержимое ответа сервера
*/
protected $content;
protected stdClass|array|null $content;
/**
* @var array Заголовки ответа
@ -66,9 +67,9 @@ class KktResponse implements JsonSerializable
* @param $name
* @return mixed
*/
public function __get($name)
public function __get($name): mixed
{
return $this->getContent()->$name;
return $this->getContent()?->$name;
}
/**
@ -84,9 +85,9 @@ class KktResponse implements JsonSerializable
/**
* Возвращает объект результата запроса
*
* @return stdClass|null
* @return mixed
*/
public function getContent(): ?stdClass
public function getContent(): mixed
{
return $this->content;
}
@ -107,7 +108,7 @@ class KktResponse implements JsonSerializable
/**
* Возвращает текстовое представление
*/
public function __toString()
public function __toString(): string
{
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_UNICODE);
}
@ -115,7 +116,7 @@ class KktResponse implements JsonSerializable
/**
* @inheritDoc
*/
public function jsonSerialize()
public function jsonSerialize(): array
{
return [
'code' => $this->code,

View File

@ -16,26 +16,26 @@ use Exception;
use Throwable;
/**
* Исключение, возникающее при работе с АТОЛ Онлайн
* Исключение, возникающее при неудачной авторизации
*/
class AuthFailedException extends Exception
{
/**
* Конструктор
*
* @param KktResponse $last_response
* @param KktResponse $response
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(KktResponse $last_response, $message = "", $code = 0, Throwable $previous = null)
public function __construct(KktResponse $response, $message = "", $code = 0, Throwable $previous = null)
{
$message = $last_response->isValid()
$message = $response->isValid()
? $message
: '[' . $last_response->error->code . '] ' . $last_response->error->text .
'. ERROR_ID: ' . $last_response->error->error_id .
'. TYPE: ' . $last_response->error->type;
$code = $last_response->isValid() ? $code : $last_response->error->code;
: '[' . $response->error->code . '] ' . $response->error->text .
'. ERROR_ID: ' . $response->error->error_id .
'. TYPE: ' . $response->error->type;
$code = $response->isValid() ? $code : $response->error->code;
parent::__construct($message, $code, $previous);
}
}

View File

@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать пустой логин ККТ
*/
class EmptyKktLoginException extends AtolException
class EmptyLoginException extends AtolException
{
/**
* @var string Сообщение об ошибке

View File

@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать пустой пароль ККТ
*/
class EmptyKktPasswordException extends AtolException
class EmptyPasswordException extends AtolException
{
/**
* @var string Сообщение об ошибке

View File

@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный логин ККТ
*/
class TooLongKktLoginException extends BasicTooLongException
class TooLongLoginException extends BasicTooLongException
{
/**
* @var string Сообщение об ошибке

View File

@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный пароль ККТ
*/
class TooLongKktPasswordException extends BasicTooLongException
class TooLongPasswordException extends BasicTooLongException
{
/**
* @var string Сообщение об ошибке

48
tests/AtolClientTest.php Normal file
View File

@ -0,0 +1,48 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Tests;
use PHPUnit\Framework\TestCase;
class AtolClientTest extends TestCase
{
public function testAuth()
{
}
public function testSetPassword()
{
}
public function testSetTestMode()
{
}
public function testGetToken()
{
}
public function testSetLogin()
{
}
public function testSetToken()
{
}
public function testGetResponse()
{
}
public function testIsTestMode()
{
}
}

40
tests/KktMonitorTest.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Tests;
use PHPUnit\Framework\TestCase;
class KktMonitorTest extends TestCase
{
public function testSetToken()
{
}
public function testGetResponse()
{
}
public function testSetLogin()
{
}
public function testAuth()
{
}
public function testGetToken()
{
}
public function testSetPassword()
{
}
}