feat: request replay command for bpn api

This commit is contained in:
Björn Fromme
2026-03-16 11:59:12 +01:00
parent 8c8aae9a1e
commit 4f1a1cad8e
3 changed files with 221 additions and 0 deletions
+76
View File
@@ -468,6 +468,82 @@ class ApiClient
return $this->sendRequest(static::TYPE_PROMO_VOUCHER, $data);
}
/**
* Sends raw XML to the BPN API with key regeneration.
*
* Parses the XML to extract the request type, regenerates the authentication key
* with the current date, and sends the request. Returns the raw XML response.
*
* @param string $xml The raw XML request body
* @param bool $debug Enable debug mode (XML dumps)
*
* @return string The raw XML response
*
* @throws ApiClientException If the request fails or XML is invalid
*/
public function sendRawXml(string $xml, bool $debug = false): string
{
$requestId = date(DATE_ATOM).uniqid();
$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 <satz> element in XML');
}
$type = $satzNode->getAttribute('typ');
if ('' === $type) {
throw new ApiClientException('Missing typ attribute on <satz> element');
}
$keyNode = $doc->getElementsByTagName('key')->item(0);
if (null === $keyNode) {
throw new ApiClientException('Missing <key> element in XML');
}
$newKey = $this->createKey(
$this->config['bpn_username'],
$this->config['bpn_password'],
$type
);
$keyNode->nodeValue = $newKey;
$body = $doc->saveXML();
$this->logger->info('Sending raw XML request to BPN API', [
'requestId' => $requestId,
'type' => $type,
]);
if (true === $debug || true === $this->config['debug']) {
$this->dumpXmlToFile('request', $requestId, $body);
}
$socket = $this->connect(
$this->config['bpn_api_ip'],
$this->config['bpn_api_port'],
$this->config['max_retries'],
$this->config['connection_timeout'],
$this->config['stream_timeout'],
$this->config['total_timeout']
);
$this->send($socket, $body, $this->config['total_timeout']);
$response = $this->receive($socket, $this->config['total_timeout']);
$this->disconnect($socket);
$responseXml = substr($response, 10);
if (true === $debug || true === $this->config['debug']) {
$this->dumpXmlToFile('response', $requestId, $responseXml);
}
return $responseXml;
}
/**
* @throws ApiClientException
*/
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'bpn:replay',
description: 'Replay a BPN API request from an XML file',
)]
class BpnReplayCommand extends Command
{
public function __construct(
private readonly ApiClient $apiClient,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('file', InputArgument::REQUIRED, 'Path to the XML request file')
->addOption('dry-run', 'd', InputOption::VALUE_NONE, 'Show regenerated XML without sending')
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Save response to file')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filePath = $input->getArgument('file');
if (false === file_exists($filePath)) {
$io->error(sprintf('File not found: %s', $filePath));
return Command::FAILURE;
}
$xml = file_get_contents($filePath);
if (false === $xml) {
$io->error(sprintf('Unable to read file: %s', $filePath));
return Command::FAILURE;
}
$io->info(sprintf('Loaded XML from: %s', $filePath));
$type = $this->extractRequestType($xml);
if (null !== $type) {
$io->info(sprintf('Request type: %s', $type));
}
if (true === $input->getOption('dry-run')) {
$regeneratedXml = $this->regenerateKey($xml);
if (null === $regeneratedXml) {
$io->error('Failed to regenerate key in XML');
return Command::FAILURE;
}
$io->section('Regenerated XML (dry-run)');
$io->writeln($this->formatXml($regeneratedXml));
return Command::SUCCESS;
}
$io->info('Sending request to BPN API...');
try {
$response = $this->apiClient->sendRawXml($xml);
} catch (ApiClientException $e) {
$io->error(sprintf('API request failed: %s', $e->getMessage()));
return Command::FAILURE;
}
$outputFile = $input->getOption('output');
if (null !== $outputFile) {
if (false === file_put_contents($outputFile, $response)) {
$io->error(sprintf('Failed to write response to: %s', $outputFile));
return Command::FAILURE;
}
$io->success(sprintf('Response saved to: %s', $outputFile));
}
$io->section('Response');
$io->writeln($this->formatXml($response));
return Command::SUCCESS;
}
private function extractRequestType(string $xml): ?string
{
$doc = new \DOMDocument();
if (false === @$doc->loadXML($xml)) {
return null;
}
$satzNode = $doc->getElementsByTagName('satz')->item(0);
return $satzNode?->getAttribute('typ');
}
private function regenerateKey(string $xml): ?string
{
$doc = new \DOMDocument();
if (false === @$doc->loadXML($xml)) {
return null;
}
$keyNode = $doc->getElementsByTagName('key')->item(0);
if (null === $keyNode) {
return null;
}
$keyNode->nodeValue = '[KEY_WOULD_BE_REGENERATED]';
return $doc->saveXML();
}
private function formatXml(string $xml): string
{
$doc = new \DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
if (false === @$doc->loadXML($xml)) {
return $xml;
}
return $doc->saveXML() ?: $xml;
}
}