feat: request replay command for bpn api
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-sockets": "*",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user