adasd
This commit is contained in:
@@ -13,9 +13,9 @@
|
|||||||
{ "name": "db.schema", "label": "DB Schema", "type": "text", "required": false },
|
{ "name": "db.schema", "label": "DB Schema", "type": "text", "required": false },
|
||||||
{ "name": "db.user", "label": "DB User", "type": "text", "required": false },
|
{ "name": "db.user", "label": "DB User", "type": "text", "required": false },
|
||||||
{ "name": "db.password", "label": "DB Passwort", "type": "password", "required": false },
|
{ "name": "db.password", "label": "DB Passwort", "type": "password", "required": false },
|
||||||
{ "name": "provider", "label": "FX Provider", "type": "text", "required": false, "help": "Aktuell getestet mit currencyapi." },
|
{ "name": "provider", "label": "FX Provider", "type": "text", "required": false, "help": "Unterstuetzt legacy currencyapi.net und currencyapi.com v3." },
|
||||||
{ "name": "api_url", "label": "FX API URL", "type": "text", "required": false },
|
{ "name": "api_url", "label": "FX API URL", "type": "text", "required": false, "help": "Nur die Basis-URL eintragen, z.B. https://api.currencyapi.com oder https://currencyapi.net." },
|
||||||
{ "name": "currencies_url", "label": "FX Currencies URL", "type": "text", "required": false },
|
{ "name": "currencies_url", "label": "FX Currencies URL", "type": "text", "required": false, "help": "Nur die Basis-URL fuer den Waehrungskatalog, z.B. https://api.currencyapi.com." },
|
||||||
{ "name": "api_key", "label": "FX API Key", "type": "password", "required": false },
|
{ "name": "api_key", "label": "FX API Key", "type": "password", "required": false },
|
||||||
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
||||||
{ "name": "cache_ttl_sec", "label": "Datei-Cache TTL (Sek.)", "type": "number", "required": false },
|
{ "name": "cache_ttl_sec", "label": "Datei-Cache TTL (Sek.)", "type": "number", "required": false },
|
||||||
|
|||||||
@@ -342,35 +342,15 @@ final class FxRatesService
|
|||||||
|
|
||||||
private function fetchLatestPayload(string $baseCurrency, ?array $currencies = null): array
|
private function fetchLatestPayload(string $baseCurrency, ?array $currencies = null): array
|
||||||
{
|
{
|
||||||
if (!function_exists('curl_init')) {
|
$request = $this->buildLatestRequest($baseCurrency, $currencies);
|
||||||
throw new \RuntimeException('curl_init ist nicht verfuegbar.');
|
if ($request === null) {
|
||||||
}
|
|
||||||
|
|
||||||
$url = $this->buildLatestUrl($baseCurrency);
|
|
||||||
if ($url === null) {
|
|
||||||
throw new \RuntimeException('FX-URL oder API-Key fehlt.');
|
throw new \RuntimeException('FX-URL oder API-Key fehlt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$ch = curl_init();
|
$payload = $this->requestJson($request['url'], $request['headers'] ?? [], 'FX-Kurse konnten nicht geladen werden.');
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_URL => $url,
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
|
||||||
CURLOPT_TIMEOUT => $this->timeoutSeconds(),
|
|
||||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
|
||||||
]);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
||||||
$curlError = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($response === false || $curlError !== '' || $httpStatus >= 400) {
|
if ($this->isCurrencyApiCom()) {
|
||||||
throw new \RuntimeException('FX-Kurse konnten nicht geladen werden.');
|
return $this->normalizeCurrencyApiComLatestPayload($payload, $baseCurrency, $currencies);
|
||||||
}
|
|
||||||
|
|
||||||
$payload = json_decode((string) $response, true);
|
|
||||||
if (!is_array($payload)) {
|
|
||||||
throw new \RuntimeException('FX-Antwort ist kein gueltiges JSON.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$rates = [];
|
$rates = [];
|
||||||
@@ -416,20 +396,107 @@ final class FxRatesService
|
|||||||
|
|
||||||
private function fetchCurrenciesPayload(): array
|
private function fetchCurrenciesPayload(): array
|
||||||
{
|
{
|
||||||
if (!function_exists('curl_init')) {
|
$request = $this->buildCurrenciesRequest();
|
||||||
throw new \RuntimeException('curl_init ist nicht verfuegbar.');
|
if ($request === null) {
|
||||||
}
|
|
||||||
|
|
||||||
$apiKey = $this->apiKey();
|
|
||||||
if ($apiKey === '') {
|
|
||||||
throw new \RuntimeException('FX-API-Key fehlt.');
|
throw new \RuntimeException('FX-API-Key fehlt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = sprintf(
|
$payload = $this->requestJson($request['url'], $request['headers'] ?? [], 'Waehrungskatalog konnte nicht geladen werden.');
|
||||||
'%s/api/v2/currencies?output=json&key=%s',
|
|
||||||
$this->currenciesApiUrl(),
|
if ($this->isCurrencyApiCom()) {
|
||||||
rawurlencode($apiKey)
|
return $this->normalizeCurrencyApiComCurrenciesPayload($payload);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (($payload['valid'] ?? false) !== true || !is_array($payload['currencies'] ?? null)) {
|
||||||
|
throw new \RuntimeException($this->extractProviderError($payload, 'Waehrungskatalog konnte nicht geladen werden.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLatestRequest(string $baseCurrency, ?array $currencies = null): ?array
|
||||||
|
{
|
||||||
|
$apiKey = $this->apiKey();
|
||||||
|
if ($this->isCurrencyApiCom()) {
|
||||||
|
if ($apiKey === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = ['base_currency=' . rawurlencode($baseCurrency)];
|
||||||
|
$normalizedCurrencies = [];
|
||||||
|
foreach ($currencies ?? [] as $currency) {
|
||||||
|
$currency = $this->normalizeCurrency((string) $currency);
|
||||||
|
if ($currency !== '' && $currency !== $baseCurrency) {
|
||||||
|
$normalizedCurrencies[] = $currency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($normalizedCurrencies !== []) {
|
||||||
|
$query[] = 'currencies=' . rawurlencode(implode(',', array_values(array_unique($normalizedCurrencies))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => $this->apiUrl() . '/v3/latest?' . implode('&', $query),
|
||||||
|
'headers' => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'apikey: ' . $apiKey,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->provider() === 'currencyapi') {
|
||||||
|
if ($apiKey === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => sprintf(
|
||||||
|
'%s/api/v2/rates?base=%s&output=json&key=%s',
|
||||||
|
$this->apiUrl(),
|
||||||
|
rawurlencode($baseCurrency),
|
||||||
|
rawurlencode($apiKey)
|
||||||
|
),
|
||||||
|
'headers' => ['Accept: application/json'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => sprintf('%s/latest?base=%s', $this->apiUrl(), rawurlencode($baseCurrency)),
|
||||||
|
'headers' => ['Accept: application/json'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCurrenciesRequest(): ?array
|
||||||
|
{
|
||||||
|
$apiKey = $this->apiKey();
|
||||||
|
if ($apiKey === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isCurrencyApiCom()) {
|
||||||
|
return [
|
||||||
|
'url' => $this->currenciesApiUrl() . '/v3/currencies',
|
||||||
|
'headers' => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'apikey: ' . $apiKey,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => sprintf(
|
||||||
|
'%s/api/v2/currencies?output=json&key=%s',
|
||||||
|
$this->currenciesApiUrl(),
|
||||||
|
rawurlencode($apiKey)
|
||||||
|
),
|
||||||
|
'headers' => ['Accept: application/json'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requestJson(string $url, array $headers, string $fallbackError): array
|
||||||
|
{
|
||||||
|
if (!function_exists('curl_init')) {
|
||||||
|
throw new \RuntimeException('curl_init ist nicht verfuegbar.');
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
@@ -437,7 +504,7 @@ final class FxRatesService
|
|||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
CURLOPT_TIMEOUT => $this->timeoutSeconds(),
|
CURLOPT_TIMEOUT => $this->timeoutSeconds(),
|
||||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
CURLOPT_HTTPHEADER => $headers !== [] ? $headers : ['Accept: application/json'],
|
||||||
]);
|
]);
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
$httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
@@ -445,38 +512,119 @@ final class FxRatesService
|
|||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($response === false || $curlError !== '' || $httpStatus >= 400) {
|
if ($response === false || $curlError !== '' || $httpStatus >= 400) {
|
||||||
throw new \RuntimeException('Waehrungskatalog konnte nicht geladen werden.');
|
$payload = is_string($response) ? json_decode($response, true) : null;
|
||||||
|
throw new \RuntimeException($this->extractProviderError(is_array($payload) ? $payload : [], $fallbackError));
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = json_decode((string) $response, true);
|
$payload = json_decode((string) $response, true);
|
||||||
if (!is_array($payload) || ($payload['valid'] ?? false) !== true) {
|
if (!is_array($payload)) {
|
||||||
throw new \RuntimeException($this->extractProviderError(is_array($payload) ? $payload : [], 'Waehrungskatalog konnte nicht geladen werden.'));
|
throw new \RuntimeException('FX-Antwort ist kein gueltiges JSON.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildLatestUrl(string $baseCurrency): ?string
|
private function normalizeCurrencyApiComLatestPayload(array $payload, string $baseCurrency, ?array $currencies = null): array
|
||||||
{
|
{
|
||||||
$apiKey = $this->apiKey();
|
$rawRates = is_array($payload['data'] ?? null) ? $payload['data'] : null;
|
||||||
if ($this->provider() === 'currencyapi') {
|
if ($rawRates === null) {
|
||||||
if ($apiKey === '') {
|
throw new \RuntimeException($this->extractProviderError($payload, 'FX-Kurse konnten nicht geladen werden.'));
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
'%s/api/v2/rates?base=%s&output=json&key=%s',
|
|
||||||
$this->apiUrl(),
|
|
||||||
rawurlencode($baseCurrency),
|
|
||||||
rawurlencode($apiKey)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('%s/latest?base=%s', $this->apiUrl(), rawurlencode($baseCurrency));
|
$filter = [];
|
||||||
|
foreach ($currencies ?? [] as $currency) {
|
||||||
|
$currency = $this->normalizeCurrency((string) $currency);
|
||||||
|
if ($currency !== '' && $currency !== $baseCurrency) {
|
||||||
|
$filter[$currency] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rates = [];
|
||||||
|
foreach ($rawRates as $code => $rateData) {
|
||||||
|
$code = $this->normalizeCurrency((string) $code);
|
||||||
|
if ($code === '' || $code === $baseCurrency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($filter !== [] && !isset($filter[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = is_array($rateData) ? ($rateData['value'] ?? null) : null;
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rates[$code] = (float) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'base' => $baseCurrency,
|
||||||
|
'date' => $payload['meta']['last_updated_at'] ?? null,
|
||||||
|
'rates' => $rates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeCurrencyApiComCurrenciesPayload(array $payload): array
|
||||||
|
{
|
||||||
|
$rawCurrencies = is_array($payload['data'] ?? null) ? $payload['data'] : null;
|
||||||
|
if ($rawCurrencies === null) {
|
||||||
|
throw new \RuntimeException($this->extractProviderError($payload, 'Waehrungskatalog konnte nicht geladen werden.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencies = [];
|
||||||
|
foreach ($rawCurrencies as $code => $currencyData) {
|
||||||
|
$normalizedCode = $this->normalizeCurrency((string) $code);
|
||||||
|
if ($normalizedCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = '';
|
||||||
|
if (is_array($currencyData)) {
|
||||||
|
$name = trim((string) ($currencyData['name'] ?? $currencyData['name_plural'] ?? $currencyData['code'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencies[$normalizedCode] = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'valid' => true,
|
||||||
|
'currencies' => $currencies,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractProviderError(array $payload, string $fallback): string
|
private function extractProviderError(array $payload, string $fallback): string
|
||||||
{
|
{
|
||||||
|
$error = $payload['error'] ?? null;
|
||||||
|
if (is_array($error)) {
|
||||||
|
foreach (['message', 'info', 'code'] as $field) {
|
||||||
|
$value = $error[$field] ?? null;
|
||||||
|
if (is_string($value) && trim($value) !== '') {
|
||||||
|
return trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $payload['errors'] ?? null;
|
||||||
|
if (is_array($errors)) {
|
||||||
|
foreach ($errors as $entry) {
|
||||||
|
if (is_string($entry) && trim($entry) !== '') {
|
||||||
|
return trim($entry);
|
||||||
|
}
|
||||||
|
if (is_array($entry)) {
|
||||||
|
foreach (['message', 'detail', 'title'] as $field) {
|
||||||
|
$value = $entry[$field] ?? null;
|
||||||
|
if (is_string($value) && trim($value) !== '') {
|
||||||
|
return trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (['error', 'message', 'msg'] as $field) {
|
foreach (['error', 'message', 'msg'] as $field) {
|
||||||
$value = $payload[$field] ?? null;
|
$value = $payload[$field] ?? null;
|
||||||
if (is_string($value) && trim($value) !== '') {
|
if (is_string($value) && trim($value) !== '') {
|
||||||
@@ -539,6 +687,18 @@ final class FxRatesService
|
|||||||
return rtrim((string) ($this->settings['currencies_url'] ?? $this->apiUrl()), '/');
|
return rtrim((string) ($this->settings['currencies_url'] ?? $this->apiUrl()), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isCurrencyApiCom(): bool
|
||||||
|
{
|
||||||
|
foreach ([$this->apiUrl(), $this->currenciesApiUrl()] as $url) {
|
||||||
|
$host = strtolower((string) parse_url($url, PHP_URL_HOST));
|
||||||
|
if ($host !== '' && str_contains($host, 'currencyapi.com')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function apiKey(): string
|
private function apiKey(): string
|
||||||
{
|
{
|
||||||
return trim((string) ($this->settings['api_key'] ?? ''));
|
return trim((string) ($this->settings['api_key'] ?? ''));
|
||||||
|
|||||||
Reference in New Issue
Block a user