From 13385a10e3c808fe54e90e9ebb4a65967ac29fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 15 Jan 2025 15:11:25 +0100 Subject: [PATCH] feat: refactor API client to connect via socket --- .env | 3 +- composer.json | 1 + config/services.yaml | 3 +- src/BusProNet/ApiClient.php | 140 ++++++++++++++++++++++++++++-------- 4 files changed, 114 insertions(+), 33 deletions(-) diff --git a/.env b/.env index f4401f9..3844434 100644 --- a/.env +++ b/.env @@ -42,7 +42,8 @@ MAILER_DSN=null://null APP_BPN_USER= APP_BPN_PASSWORD= -APP_BPN_ENDPOINT= +APP_BPN_IP= +APP_BPN_PORT= APP_BPN_DEBUG=false API_KEYS= \ No newline at end of file diff --git a/composer.json b/composer.json index 3675724..ba43bff 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "ext-ctype": "*", "ext-iconv": "*", "ext-simplexml": "*", + "ext-sockets": "*", "doctrine/dbal": "^3", "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.3", diff --git a/config/services.yaml b/config/services.yaml index aa8b777..64b6ce1 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -32,7 +32,8 @@ services: $options: bpn_username: '%env(APP_BPN_USER)%' bpn_password: '%env(APP_BPN_PASSWORD)%' - bpn_url: '%env(APP_BPN_ENDPOINT)%' + bpn_api_ip: '%env(APP_BPN_IP)%' + bpn_api_port: '%env(APP_BPN_PORT)%' debug: '%env(bool:APP_BPN_DEBUG)%' App\Twig\AppRuntime: diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 71c809e..c3c08d9 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -5,11 +5,11 @@ namespace App\BusProNet; use App\BusProNet\ApiResponseParser\ResponseParser; use App\BusProNet\DataProcessor\BookingDataProcessor; use App\BusProNet\Exception\ApiClientException; +use App\BusProNet\Exception\ResponseParserException; use App\BusProNet\Model\BaseData; use App\BusProNet\Model\Booking; use App\BusProNet\Model\BookingUpdate; use App\BusProNet\Model\CrmAttributes; -use App\BusProNet\Model\File; use App\BusProNet\Model\Notification; use App\BusProNet\Model\PersonalData; use App\Form\Model\BookingData; @@ -18,7 +18,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; class ApiClient { @@ -32,18 +31,17 @@ class ApiClient private array $config; public function __construct( - private readonly HttpClientInterface $httpClient, private readonly SerializerInterface $serializer, - private readonly ResponseParser $responseParser, - private readonly LoggerInterface $logger, - array $options - ) - { + private readonly ResponseParser $responseParser, + private readonly LoggerInterface $logger, + array $options + ) { $this->config = $this->resolveOptions($options); } /** * @throws ApiClientException + * @throws ResponseParserException */ public function getPersonalData(string $email, string $password): Notification|PersonalData { @@ -61,6 +59,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function register(RegistrationData $registrationData): Notification { @@ -84,6 +83,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function resetPassword(string $email): Notification { @@ -100,6 +100,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function updatePersonalData(string $email, string $password, PersonalData $personalData): Notification|PersonalData { @@ -119,6 +120,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function updateNewsletterRegistration(string $email, string $password, PersonalData $personalData): Notification|PersonalData { @@ -141,6 +143,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function getBookings(string $email, string $password): Notification|BaseData { @@ -156,9 +159,6 @@ class ApiClient return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); } - /** - * @throws ApiClientException - */ public function getBooking(string $email, string $password, int $id): Notification|Booking { $data = [ @@ -174,6 +174,10 @@ class ApiClient return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); } + /** + * @throws ApiClientException + * @throws ResponseParserException + */ public function updateBooking(BookingData $formData, bool $dryRun = true): Notification|BookingUpdate { $mode = $dryRun ? 'Anfrage' : 'Buchung'; @@ -192,6 +196,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function getMutableData(int $id): Notification|BaseData { @@ -208,6 +213,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function getAvailabilities(int $id): Notification|BaseData { @@ -223,6 +229,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes { @@ -240,6 +247,7 @@ class ApiClient /** * @throws ApiClientException + * @throws ResponseParserException */ public function getBaseData(string $type): Notification|BaseData { @@ -253,8 +261,8 @@ class ApiClient } /** - * @return Notification|File|null * @throws ApiClientException + * @throws ResponseParserException */ public function getDocuments(string $email, string $password, int $id, string $type): mixed { @@ -274,6 +282,78 @@ class ApiClient /** * @throws ApiClientException */ + private function connect() + { + $tries = 1; + $errNo = $errStr = ''; + $maxRetries = $this->config['max_retries']; + $errorCodesForRetry = [ + SOCKET_ECONNREFUSED, + SOCKET_EBADF, + ]; + + $openSocket = function (&$errNo, &$errStr) { + return @fsockopen( + $this->config['bpn_api_ip'], + $this->config['bpn_api_port'], + $errNo, + $errStr, + 10 + ); + }; + + $socket = $openSocket($errNo, $errStr); + + while (false === $socket && true === in_array($errNo, $errorCodesForRetry) && $maxRetries > $tries) { + $this->logger->warning('Could not connect to socket, retrying', [ + 'error_message' => $errStr, + 'error_number' => $errNo, + ]); + ++$tries; + sleep(1); + $socket = $openSocket($errNo, $errStr); + } + + if (false !== $socket) { + stream_set_timeout($socket, 60); + } else { + $this->logger->error('Unable to open socket', [ + 'error_message' => $errStr, + 'error_number' => $errNo, + ]); + throw new ApiClientException('Unable to open socket'); + } + + return $socket; + } + + private function send($socket, string $data): void + { + // message length is prepended to actual message + $send = sprintf('%010s', strlen($data)) . $data; + fwrite($socket, $send); + } + + private function receive($socket): string + { + $response = ''; + + while (false === feof($socket)) { + $response .= fread($socket, 4096); + } + + return $response; + } + + private function disconnect($socket): void + { + @fclose($socket); + } + + /** + * @throws ApiClientException + * @throws ResponseParserException + */ private function sendRequest(string $type, array $data): mixed { $requestId = date(DATE_ATOM).uniqid(); @@ -286,31 +366,23 @@ class ApiClient ]) ; + if (true === $this->config['debug']) { $this->dumpXmlToFile('request', $requestId, $body); } - try { - $response = $this->httpClient->request('GET', $this->config['bpn_url'], [ - 'query' => [ - 'operation' => $body, - ], - 'verify_peer' => false, - 'verify_host' => false, - ]); + $socket = $this->connect(); + $this->send($socket, $body); + $response = $this->receive($socket); + // message length (10 bytes) is prepended to actual message + $xml = substr($response, 10); + $this->disconnect($socket); - $xml = $response->getContent(); - - if (true === $this->config['debug']) { - $this->dumpXmlToFile('response', $requestId, $xml); - } - - return $this->responseParser->parseXmlString($type, $xml); - } catch (\Throwable $e) { + if (true === $this->config['debug']) { + $this->dumpXmlToFile('response', $requestId, $xml); } - $this->logger->error('API error', ['error' => $e->getMessage()]); - throw new ApiClientException($e->getMessage()); + return $this->responseParser->parseXmlString($type, $xml); } private function dumpXmlToFile(string $type, string $requestId, string $body): void @@ -332,8 +404,14 @@ class ApiClient private function resolveOptions(array $options): array { $optionsResolver = new OptionsResolver(); - $optionsResolver->setRequired(['bpn_url', 'bpn_username', 'bpn_password']); + $optionsResolver->setRequired([ + 'bpn_username', + 'bpn_password', + 'bpn_api_ip', + 'bpn_api_port', + ]); $optionsResolver->setDefaults([ + 'max_retries' => 25, 'debug' => false, 'target_folder_dumps' => '/var/www/html/var/bpn', ]);