From 0a55544ca8d7f372dcf8ef4fa09c8ae65c9092d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 2 Oct 2026 13:53:01 +0200 Subject: [PATCH] refactor: move buspro XML protocol into XmlChannel --- config/services.yaml | 2 +- src/BusProNet/ApiClient.php | 300 ++---------------- src/BusProNet/Transport/RequestIdSequence.php | 31 ++ src/BusProNet/Transport/XmlChannel.php | 259 +++++++++++++++ src/Service/XmlDumpReader.php | 2 +- src/Service/XmlDumpWriter.php | 35 ++ 6 files changed, 348 insertions(+), 281 deletions(-) create mode 100644 src/BusProNet/Transport/RequestIdSequence.php create mode 100644 src/BusProNet/Transport/XmlChannel.php create mode 100644 src/Service/XmlDumpWriter.php diff --git a/config/services.yaml b/config/services.yaml index 85e6a630..95a75fb5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -128,7 +128,7 @@ services: - '../src/Model' - '../src/Kernel.php' - App\BusProNet\ApiClient: + App\BusProNet\Transport\XmlChannel: arguments: $logger: '@monolog.logger.bpn' $options: diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 5977d50f..08e9bb78 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -5,8 +5,6 @@ namespace App\BusProNet; use App\BusProNet\DataProcessor\BookingDataProcessor; use App\BusProNet\DataProcessor\VoucherSalePayloadBuilder; use App\BusProNet\Exception\ApiClientException; -use App\BusProNet\Exception\ImmediateConnectionCloseException; -use App\BusProNet\Exception\ResponseParserException; use App\BusProNet\Model\Agency; use App\BusProNet\Model\BaseData; use App\BusProNet\Model\Booking; @@ -26,19 +24,10 @@ use App\BusProNet\Model\VoucherPaymentMethod; use App\BusProNet\Model\VoucherPreview; use App\BusProNet\Model\VoucherSaleResponse; use App\BusProNet\Model\VoucherTemplates; -use App\BusProNet\Transport\SocketTransport; -use App\BusProNet\XmlParser\ApiResponseParser; +use App\BusProNet\Transport\XmlChannel; use App\Form\Model\BookingDto; use App\Form\Model\RegistrationDto; use App\Form\Model\VoucherPurchaseDto; -use App\Service\RequestIdGenerator; -use League\Flysystem\FilesystemException; -use League\Flysystem\FilesystemOperator; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Serializer\Encoder\XmlEncoder; -use Symfony\Component\Serializer\SerializerInterface; class ApiClient { @@ -62,24 +51,11 @@ class ApiClient public const TYPE_VOUCHER_PREVIEW = 'GUTSCHEINVORSCHAU'; public const TYPE_VOUCHER_SALE = 'GUTSCHEINBUCHUNG'; - /** @var array */ - private array $config; - private int $requestCounter = 0; - - /** @param array $options */ public function __construct( - private readonly SerializerInterface $serializer, - private readonly ApiResponseParser $responseParser, - private readonly FilesystemOperator $xmlDump, - private readonly LoggerInterface $logger, + private readonly XmlChannel $channel, private readonly BookingDataProcessor $bookingDataProcessor, private readonly VoucherSalePayloadBuilder $voucherSalePayloadBuilder, - private readonly RequestStack $requestStack, - private readonly RequestIdGenerator $requestIdGenerator, - private readonly SocketTransport $transport, - array $options, ) { - $this->config = $this->resolveOptions($options); } /** @@ -173,7 +149,7 @@ class ApiClient */ public function updateBooking(BookingDto $formData, bool $debug = false): Notification|BookingUpdate { - return $this->sendRequest(static::TYPE_BOOKING_UPDATE, [ + return $this->channel->request(static::TYPE_BOOKING_UPDATE, [ 'buchungsart' => Constants::BOOKING_TYPE_BOOKING, ...$this->bookingDataProcessor->createUpdateRequestPayload($formData), ], [], $debug); @@ -206,7 +182,7 @@ class ApiClient */ public function getMutableData(int $dateId): Notification|BaseData { - return $this->sendRequest(static::TYPE_MUTABLE_DATA, ['idreise' => $dateId]); + return $this->channel->request(static::TYPE_MUTABLE_DATA, ['idreise' => $dateId]); } /** @@ -214,7 +190,7 @@ class ApiClient */ public function getAvailabilities(int $dateId): Notification|ServiceAvailabilityResponse { - return $this->sendRequest(static::TYPE_AVAILABILITY, ['idreise' => $dateId]); + return $this->channel->request(static::TYPE_AVAILABILITY, ['idreise' => $dateId]); } /** @@ -222,7 +198,7 @@ class ApiClient */ public function getAvailabilitiesExtended(int $dateId): Notification|ExtendedServiceAvailabilityResponse { - return $this->sendRequest(static::TYPE_AVAILABILITY_EXTENDED, ['idreise' => $dateId]); + return $this->channel->request(static::TYPE_AVAILABILITY_EXTENDED, ['idreise' => $dateId]); } /** @@ -230,7 +206,7 @@ class ApiClient */ public function getHotelAvailability(int $dateId, int $hotelId, \DateTimeInterface $dateTo): Notification|BaseData { - return $this->sendRequest(static::TYPE_AVAILABILITY_HOTEL, [ + return $this->channel->request(static::TYPE_AVAILABILITY_HOTEL, [ 'idreise' => $dateId, 'idpartner' => $hotelId, 'terminbis' => $dateTo->format('d.m.Y'), @@ -250,7 +226,7 @@ class ApiClient */ public function getBaseData(string $type): Notification|BaseData { - return $this->sendRequest($type); + return $this->channel->request($type); } /** @@ -266,7 +242,7 @@ class ApiClient */ public function getTravelData(int $travelId, ?int $hotelId = null): Notification|Travel { - return $this->sendRequest(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId], ['hotelId' => $hotelId]); + return $this->channel->request(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId], ['hotelId' => $hotelId]); } /** @@ -277,9 +253,7 @@ class ApiClient */ public function getTravelDataXml(int $travelId): string { - $data = $this->envelope(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId]); - - return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data)); + return $this->channel->requestRaw(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId]); } /** @@ -287,7 +261,7 @@ class ApiClient */ public function getProducts(): Notification|BaseData { - return $this->sendRequest(static::TYPE_PRODUCTS); + return $this->channel->request(static::TYPE_PRODUCTS); } /** @@ -299,7 +273,7 @@ class ApiClient */ public function getAgencies(): array|Notification { - return $this->sendRequest(static::TYPE_AGENCIES); + return $this->channel->request(static::TYPE_AGENCIES); } /** @@ -310,7 +284,7 @@ class ApiClient */ public function validatePurchaseVoucher(string $redemptionCode): Notification|PurchaseVoucher { - return $this->sendRequest(static::TYPE_PURCHASE_VOUCHER, ['einloesecode' => $redemptionCode]); + return $this->channel->request(static::TYPE_PURCHASE_VOUCHER, ['einloesecode' => $redemptionCode]); } /** @@ -322,7 +296,7 @@ class ApiClient */ public function validatePromoVoucher(string $promoCode, int $travelId, float $participantPrice): Notification|PromoVoucher { - return $this->sendRequest(static::TYPE_PROMO_VOUCHER, [ + return $this->channel->request(static::TYPE_PROMO_VOUCHER, [ 'aktionsgutschein' => $promoCode, 'idreise' => $travelId, 'preis' => number_format($participantPrice, 2, ',', ''), @@ -336,7 +310,7 @@ class ApiClient */ public function getVoucherTemplates(): Notification|VoucherTemplates { - return $this->sendRequest(static::TYPE_VOUCHER_TEMPLATES); + return $this->channel->request(static::TYPE_VOUCHER_TEMPLATES); } /** @@ -348,7 +322,7 @@ class ApiClient */ public function getVoucherPaymentMethods(): array|Notification { - return $this->sendRequest(static::TYPE_VOUCHER_PAYMENT_METHODS); + return $this->channel->request(static::TYPE_VOUCHER_PAYMENT_METHODS); } /** @@ -358,7 +332,7 @@ class ApiClient */ public function getVoucherPreview(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherPreview { - return $this->sendRequest(static::TYPE_VOUCHER_PREVIEW, $this->voucherSalePayloadBuilder->buildPreviewPayload($voucherPurchaseDto)); + return $this->channel->request(static::TYPE_VOUCHER_PREVIEW, $this->voucherSalePayloadBuilder->buildPreviewPayload($voucherPurchaseDto)); } /** @@ -394,7 +368,7 @@ class ApiClient */ public function sendRawXml(string $xml, bool $debug = false): string { - return $this->executeWithRetry(fn () => $this->doSendRawXml($xml, $debug)); + return $this->channel->replay($xml, $debug); } /** @@ -402,7 +376,7 @@ class ApiClient */ private function sendBooking(BookingDto $bookingDto, string $bookingType, bool $debug): Notification|BookingResponse { - return $this->sendRequest(static::TYPE_BOOKING, $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, $bookingType), [], $debug); + return $this->channel->request(static::TYPE_BOOKING, $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, $bookingType), [], $debug); } /** @@ -410,7 +384,7 @@ class ApiClient */ private function sendVoucherSale(VoucherPurchaseDto $voucherPurchaseDto, string $saleType): Notification|VoucherSaleResponse { - return $this->sendRequest(static::TYPE_VOUCHER_SALE, $this->voucherSalePayloadBuilder->buildSalePayload($voucherPurchaseDto, $saleType)); + return $this->channel->request(static::TYPE_VOUCHER_SALE, $this->voucherSalePayloadBuilder->buildSalePayload($voucherPurchaseDto, $saleType)); } /** @@ -420,238 +394,6 @@ class ApiClient */ private function customerRequest(string $art, array $fields, bool $debug = false): mixed { - return $this->sendRequest(static::TYPE_CUSTOMER_DATA, ['art' => $art, ...$fields], [], $debug); - } - - /** - * @param array $fields - * @param array $additionalArgs - * - * @throws ApiClientException - */ - private function sendRequest(string $type, array $fields = [], array $additionalArgs = [], bool $debug = false): mixed - { - $data = $this->envelope($type, $fields); - - return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type); - } - - /** - * The authentication every request starts with, followed by the command's own fields. - * - * @param array $fields - * - * @return array - */ - private function envelope(string $type, array $fields = []): array - { - return [ - 'user' => $this->config['bpn_username'], - 'key' => $this->createKey($type), - 'satz' => ['@typ' => $type], - ...$fields, - ]; - } - - /** - * Executes an operation with automatic retry on immediate connection close. - * - * When the BPN server is busy, it may close connections immediately without responding. - * This method detects such conditions and automatically retries after a short delay. - * - * @throws ImmediateConnectionCloseException If all retry attempts fail - */ - private function executeWithRetry(callable $operation, ?string $type = null): mixed - { - $maxAttempts = $this->config['busy_retry_attempts']; - $retryDelay = $this->config['busy_retry_delay']; - $typeContext = null !== $type ? ['type' => $type] : []; - $lastException = null; - - for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) { - try { - return $operation(); - } catch (ImmediateConnectionCloseException $e) { - $lastException = $e; - - if ($attempt < $maxAttempts) { - $this->logger->warning('BPN server busy, retrying request', [ - 'attempt' => $attempt, - 'maxAttempts' => $maxAttempts, - 'retryDelay' => $retryDelay, - ...$typeContext, - ]); - sleep($retryDelay); - } - } - } - - $this->logger->error('BPN server busy after all retry attempts', ['attempts' => $maxAttempts, ...$typeContext]); - - throw $lastException; - } - - /** - * @param array $data - * @param array $additionalArgs - * - * @throws ApiClientException - * @throws ImmediateConnectionCloseException - */ - private function doSendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed - { - $requestId = $this->getRequestId(); - $dump = $debug || true === $this->config['debug']; - - $xml = $this->exchange($requestId, $this->serializeXml($data), $type, 'Sending request to BPN API', $dump); - - return $this->parseResponse($type, $xml, $additionalArgs, $requestId); - } - - /** - * @param array $data - * - * @throws ApiClientException - * @throws ImmediateConnectionCloseException - */ - private function doSendRequestRaw(array $data): string - { - $requestId = $this->getRequestId(); - - $body = $this->serializeXml($data); - - return $this->exchange($requestId, $body, $data['satz']['@typ'], 'Sending raw request to BPN API', true === $this->config['debug']); - } - - /** - * @throws ApiClientException - * @throws ImmediateConnectionCloseException - */ - private function doSendRawXml(string $xml, bool $debug = false): string - { - $requestId = $this->getRequestId(); - - $doc = new \DOMDocument(); - if (false === @$doc->loadXML($xml)) { - throw new ApiClientException('Invalid XML provided'); - } - - $satzNode = $doc->getElementsByTagName('satz')->item(0); - if (null === $satzNode) { - throw new ApiClientException('Missing element in XML'); - } - - $type = $satzNode->getAttribute('typ'); - if ('' === $type) { - throw new ApiClientException('Missing typ attribute on element'); - } - - $keyNode = $doc->getElementsByTagName('key')->item(0); - if (null === $keyNode) { - throw new ApiClientException('Missing element in XML'); - } - - $keyNode->nodeValue = $this->createKey($type); - - return $this->exchange($requestId, $doc->saveXML(), $type, 'Sending raw XML request to BPN API', $debug || true === $this->config['debug']); - } - - /** - * @throws ApiClientException - * @throws ImmediateConnectionCloseException - */ - private function exchange(string $requestId, string $body, string $type, string $logMessage, bool $dump): string - { - if ($dump) { - $this->dumpXmlToFile('request', $requestId, $body); - } - - $response = $this->transport->exchange($body, $logMessage, ['requestId' => $requestId, 'type' => $type]); - - if ($dump) { - $this->dumpXmlToFile('response', $requestId, $response); - } - - return $response; - } - - /** - * @param array $additionalArgs - * - * @throws ApiClientException - */ - private function parseResponse(string $type, string $xml, array $additionalArgs, string $requestId, bool $dumpOnFailure = true): mixed - { - try { - return $this->responseParser->parseXmlString($type, $xml, $additionalArgs); - } catch (ResponseParserException $e) { - // Keep the evidence even outside debug mode: without the raw response a parse - // failure is not diagnosable after the fact. app:cleanup:xml-dumps prunes it. - if ($dumpOnFailure) { - $this->dumpXmlToFile('response', $requestId, $xml); - } - - $this->logger->error('Unable to parse response received from API', [ - 'request_id' => $requestId, - 'type' => $type, - 'response_length' => strlen($xml), - 'error_message' => $e->getMessage(), - ]); - - throw new ApiClientException('Unable to parse response received from API: '.$e->getMessage(), 0, $e); - } - } - - /** @param array $data */ - private function serializeXml(array $data): string - { - return $this->serializer->serialize($data, 'xml', [ - XmlEncoder::ROOT_NODE_NAME => 'anfrage', - XmlEncoder::ENCODING => 'UTF-8', - ]); - } - - private function dumpXmlToFile(string $type, string $requestId, string $body): void - { - try { - $this->xmlDump->write($requestId.'_'.$type.'.xml', $body); - } catch (FilesystemException $e) { - } - } - - private function getRequestId(): string - { - $baseId = $this->requestStack->getMainRequest()?->attributes->get('request_id') - ?? $this->requestIdGenerator->generateBaseId(); - - return $baseId.'_'.++$this->requestCounter; - } - - private function createKey(string $type): string - { - $date = (new \DateTimeImmutable())->format('Ymd'); - - return md5($this->config['bpn_username'].$this->config['bpn_password'].$date.$type); - } - - /** - * @param array $options - * - * @return array - */ - private function resolveOptions(array $options): array - { - $optionsResolver = new OptionsResolver(); - $optionsResolver->setRequired([ - 'bpn_username', - 'bpn_password', - ]); - $optionsResolver->setDefaults([ - 'debug' => false, - 'busy_retry_attempts' => 3, - 'busy_retry_delay' => 1, - ]); - - return $optionsResolver->resolve($options); + return $this->channel->request(static::TYPE_CUSTOMER_DATA, ['art' => $art, ...$fields], [], $debug); } } diff --git a/src/BusProNet/Transport/RequestIdSequence.php b/src/BusProNet/Transport/RequestIdSequence.php new file mode 100644 index 00000000..85463531 --- /dev/null +++ b/src/BusProNet/Transport/RequestIdSequence.php @@ -0,0 +1,31 @@ +requestStack->getMainRequest()?->attributes->get('request_id') + ?? $this->requestIdGenerator->generateBaseId(); + + return $baseId.'_'.++$this->counter; + } +} diff --git a/src/BusProNet/Transport/XmlChannel.php b/src/BusProNet/Transport/XmlChannel.php new file mode 100644 index 00000000..d7639d2f --- /dev/null +++ b/src/BusProNet/Transport/XmlChannel.php @@ -0,0 +1,259 @@ + */ + private array $config; + + /** @param array $options */ + public function __construct( + private readonly SerializerInterface $serializer, + private readonly ApiResponseParser $responseParser, + private readonly XmlDumpWriter $dumpWriter, + private readonly LoggerInterface $logger, + private readonly RequestIdSequence $requestIds, + private readonly SocketTransport $transport, + array $options, + ) { + $this->config = $this->resolveOptions($options); + } + + /** + * Sends command $type with $fields and returns the parsed response. + * + * @param array $fields + * @param array $parserArgs + * + * @throws ApiClientException + */ + public function request(string $type, array $fields = [], array $parserArgs = [], bool $debug = false): mixed + { + $data = $this->envelope($type, $fields); + + return $this->executeWithRetry(function () use ($type, $data, $parserArgs, $debug) { + $requestId = $this->requestIds->next(); + $dump = $debug || true === $this->config['debug']; + + $xml = $this->exchange($requestId, $this->serializeXml($data), $type, 'Sending request to BPN API', $dump); + + return $this->parseResponse($type, $xml, $parserArgs, $requestId); + }, $type); + } + + /** + * Like request(), but returns the unparsed response. + * + * @param array $fields + * + * @throws ApiClientException + */ + public function requestRaw(string $type, array $fields = []): string + { + $data = $this->envelope($type, $fields); + + return $this->executeWithRetry(function () use ($type, $data) { + $requestId = $this->requestIds->next(); + $body = $this->serializeXml($data); + + return $this->exchange($requestId, $body, $type, 'Sending raw request to BPN API', true === $this->config['debug']); + }); + } + + /** + * Sends a recorded request as is, except for the key, which is regenerated for the current + * date. Returns the unparsed response. + * + * @throws ApiClientException + */ + public function replay(string $xml, bool $debug = false): string + { + return $this->executeWithRetry(fn () => $this->doReplay($xml, $debug)); + } + + /** + * The authentication every request starts with, followed by the command's own fields. + * + * @param array $fields + * + * @return array + */ + private function envelope(string $type, array $fields): array + { + return [ + 'user' => $this->config['bpn_username'], + 'key' => $this->createKey($type), + 'satz' => ['@typ' => $type], + ...$fields, + ]; + } + + private function createKey(string $type): string + { + $date = (new \DateTimeImmutable())->format('Ymd'); + + return md5($this->config['bpn_username'].$this->config['bpn_password'].$date.$type); + } + + /** + * Executes an operation with automatic retry on immediate connection close. + * + * When the BPN server is busy, it may close connections immediately without responding. + * This method detects such conditions and automatically retries after a short delay. + * + * @throws ImmediateConnectionCloseException If all retry attempts fail + */ + private function executeWithRetry(callable $operation, ?string $type = null): mixed + { + $maxAttempts = $this->config['busy_retry_attempts']; + $retryDelay = $this->config['busy_retry_delay']; + $typeContext = null !== $type ? ['type' => $type] : []; + $lastException = null; + + for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) { + try { + return $operation(); + } catch (ImmediateConnectionCloseException $e) { + $lastException = $e; + + if ($attempt < $maxAttempts) { + $this->logger->warning('BPN server busy, retrying request', [ + 'attempt' => $attempt, + 'maxAttempts' => $maxAttempts, + 'retryDelay' => $retryDelay, + ...$typeContext, + ]); + sleep($retryDelay); + } + } + } + + $this->logger->error('BPN server busy after all retry attempts', ['attempts' => $maxAttempts, ...$typeContext]); + + throw $lastException; + } + + /** + * @throws ApiClientException + * @throws ImmediateConnectionCloseException + */ + private function doReplay(string $xml, bool $debug): string + { + $requestId = $this->requestIds->next(); + + $doc = new \DOMDocument(); + if (false === @$doc->loadXML($xml)) { + throw new ApiClientException('Invalid XML provided'); + } + + $satzNode = $doc->getElementsByTagName('satz')->item(0); + if (null === $satzNode) { + throw new ApiClientException('Missing element in XML'); + } + + $type = $satzNode->getAttribute('typ'); + if ('' === $type) { + throw new ApiClientException('Missing typ attribute on element'); + } + + $keyNode = $doc->getElementsByTagName('key')->item(0); + if (null === $keyNode) { + throw new ApiClientException('Missing element in XML'); + } + + $keyNode->nodeValue = $this->createKey($type); + + return $this->exchange($requestId, $doc->saveXML(), $type, 'Sending raw XML request to BPN API', $debug || true === $this->config['debug']); + } + + /** + * @throws ApiClientException + * @throws ImmediateConnectionCloseException + */ + private function exchange(string $requestId, string $body, string $type, string $logMessage, bool $dump): string + { + if ($dump) { + $this->dumpWriter->write($requestId, XmlDumpWriter::REQUEST, $body); + } + + $response = $this->transport->exchange($body, $logMessage, ['requestId' => $requestId, 'type' => $type]); + + if ($dump) { + $this->dumpWriter->write($requestId, XmlDumpWriter::RESPONSE, $response); + } + + return $response; + } + + /** + * @param array $parserArgs + * + * @throws ApiClientException + */ + private function parseResponse(string $type, string $xml, array $parserArgs, string $requestId, bool $dumpOnFailure = true): mixed + { + try { + return $this->responseParser->parseXmlString($type, $xml, $parserArgs); + } catch (ResponseParserException $e) { + // Keep the evidence even outside debug mode: without the raw response a parse + // failure is not diagnosable after the fact. app:cleanup:xml-dumps prunes it. + if ($dumpOnFailure) { + $this->dumpWriter->write($requestId, XmlDumpWriter::RESPONSE, $xml); + } + + $this->logger->error('Unable to parse response received from API', [ + 'request_id' => $requestId, + 'type' => $type, + 'response_length' => strlen($xml), + 'error_message' => $e->getMessage(), + ]); + + throw new ApiClientException('Unable to parse response received from API: '.$e->getMessage(), 0, $e); + } + } + + /** @param array $data */ + private function serializeXml(array $data): string + { + return $this->serializer->serialize($data, 'xml', [ + XmlEncoder::ROOT_NODE_NAME => 'anfrage', + XmlEncoder::ENCODING => 'UTF-8', + ]); + } + + /** + * @param array $options + * + * @return array + */ + private function resolveOptions(array $options): array + { + $optionsResolver = new OptionsResolver(); + $optionsResolver->setRequired([ + 'bpn_username', + 'bpn_password', + ]); + $optionsResolver->setDefaults([ + 'debug' => false, + 'busy_retry_attempts' => 3, + 'busy_retry_delay' => 1, + ]); + + return $optionsResolver->resolve($options); + } +} diff --git a/src/Service/XmlDumpReader.php b/src/Service/XmlDumpReader.php index 2c6854b5..302703ca 100644 --- a/src/Service/XmlDumpReader.php +++ b/src/Service/XmlDumpReader.php @@ -37,7 +37,7 @@ class XmlDumpReader return []; } - $pattern = '/^'.preg_quote($requestId, '/').'(_\d+)?_(request|response)\.xml$/'; + $pattern = '/^'.preg_quote($requestId, '/').'(_\d+)?_('.XmlDumpWriter::REQUEST.'|'.XmlDumpWriter::RESPONSE.')\.xml$/'; $matches = []; foreach ($this->xmlDump->listContents('.') as $item) { diff --git a/src/Service/XmlDumpWriter.php b/src/Service/XmlDumpWriter.php new file mode 100644 index 00000000..9a431bcd --- /dev/null +++ b/src/Service/XmlDumpWriter.php @@ -0,0 +1,35 @@ +xmlDump->write($requestId.'_'.$direction.'.xml', $body); + } catch (FilesystemException) { + } + } +}