Files
api/services/nginx/app/classes/fxratesapi.php
T

538 lines
17 KiB
PHP

<?php
namespace classes;
require_once WD . '/modules/fxratesapi/fxratesapi_c.php';
/** Actions */
require_once WD . '/modules/fxratesapi/actions/convert_rate_a.php';
use Exception;
use fxratesapi\actions\convert_rate_a;
use fxratesapi\fxratesapi_c;
use interfaces\fxratesapi_i;
use objects\fxratesapi_conversion_rates_o;
use Throwable;
class fxratesapi implements fxratesapi_i
{
/**
* Configuration of the fxratesapi module
* @var fxratesapi_c
*/
public fxratesapi_c $config;
/**
* API URL
* @var string
*/
private string $api_url = 'https://api.fxratesapi.com/';
/**
* ACTION: CONVERT RATE
* @see convert_rate_a
* @notation This action is when a currency conversion request is made, and logs the request in the database
* @var convert_rate_a
*/
private convert_rate_a $convert_rate;
public function __construct()
{
$this->config = new fxratesapi_c();
/** Actions */
$this->convert_rate = new convert_rate_a();
}
/**
* @inheritDoc
*/
function isCached(string $base, string $target): bool
{
// Check if the base currency is valid
self::requireValidCurrency($base);
// Check if the target currency is valid
self::requireValidCurrency($base);
// Check if the request is cached
$fxratesapi_lookups = new fxratesapi_conversion_rates_o();
return $fxratesapi_lookups->isCached($base, $target);
}
/**
* @inheritDoc
*/
function requireValidCurrency(string $currency): void
{
// Check if the currency is valid
if (!preg_match('/^[A-Z]{3}$/', $currency)) {
throw new Exception('Invalid currency');
}
}
/**
* @inheritDoc
*/
function getCachedResponse(string $base, string $target): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Get the cached response from the fxratesapi request log
$fxratesapi_lookups = new fxratesapi_conversion_rates_o();
$fxratesapi_lookups->getCachedResponse($base, $target);
return $fxratesapi_lookups;
}
/**
* @inheritDoc
* @throws Exception If the base or target currency is invalid
* @throws Exception If the endpoint is invalid
* @throws Exception If the response is invalid
* @throws Exception If the response is not valid JSON
* @throws Exception If the request fails
*/
function getConversionRate(string $base, string $target, string $endpoint, array $data): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Send the request
// Return the conversion rate
return self::sendRequest($base, $target, $endpoint, $data);
}
/**
* @inheritDoc
* @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
*/
function sendRequest(
string $base,
string $target,
string $endpoint,
array $data = [],
string $method = 'GET'
): object
{
// Validate the module is enabled
self::requireModuleEnabled();
// Validate the base and target currencies
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Validate the secret key
self::requireValidSecretKey();
// Reserve quota for the outbound provider call. Cached conversion reads return before this point.
$this->reserveRateFetchQuota($base, $target, $endpoint, $method);
// Send the request
$response = match ($method) {
'GET' => self::sendGetRequest($base, $target, $endpoint, $data),
'POST' => self::sendPostRequest($base, $target, $endpoint, $data),
'PUT' => self::sendPutRequest($base, $target, $endpoint, $data),
'DELETE' => self::sendDeleteRequest($base, $target, $endpoint, $data),
default => self::exception(
$base,
$target,
[
'method' => $method,
'endpoint' => $endpoint,
'data' => $data,
'license_plate' => $base,
'response' => null,
'status_code' => 400,
'error' => 'Invalid request method',
],
500
),
};
// Add the usage to the request log
$this->convert_rate->convert_rate($base, $target, $response, 200);
// Add the request to the log
self::addRequestToLog($base, $target, $endpoint, $response);
// Return the response
return $response;
}
/**
* @inheritDoc
*/
function requireModuleEnabled(): void
{
// Check if the module is enabled
if (!$this->config->enabled->isTrue()) {
throw new Exception('The fxratesapi module is not enabled');
}
}
/**
* @inheritDoc
*/
function requireDailyLimitNotExceeded(): void
{
// Check if the daily limit is exceeded
if ($this->getDailyRequestCounter() >= (int)$this->config->daily_limit->getVariableValue()) {
throw new Exception('Daily limit exceeded');
}
}
/**
* @inheritDoc
*/
function getDailyRequestCounter(): int
{
try {
return (new module_usage_service())->currentUsedQuantity('fxratesapi', 'rate_fetch_calls');
} catch (Throwable) {
}
// Count the rows from the fxratesapi request log that was made today
$fxratesapi_lookups = new fxratesapi_conversion_rates_o();
$fxratesapi_lookups->getTodayCount();
return $fxratesapi_lookups->getTodayCount();
}
/**
* @throws Exception
*/
private function reserveRateFetchQuota(string $base, string $target, string $endpoint, string $method): void
{
(new module_usage_service())->reserveOrFail('fxratesapi', 'rate_fetch_calls', 1, [
'base' => $base,
'target' => $target,
'endpoint' => $endpoint,
'method' => strtoupper($method),
]);
}
/**
* @inheritDoc
*/
function requireValidSecretKey(): void
{
// Check if the secret key is valid
if ($this->config->secret_key->getVariableValue() === null) {
self::exception(
'',
'',
[
'status_code' => 500,
'error' => 'Invalid secret key defined in the config (fxratesapi_secret_key_c)',
],
500
);
}
}
/**
* @throws Exception
*/
function exception(string $base, string $target, array $data, int $status_code = 500): object
{
// Check if the response is valid JSON
if (!json_decode($data['response'])) {
throw new Exception('Invalid response');
}
// Add the request to the log
$this->convert_rate->convert_rate($base, $target, $data, $status_code);
return throw new Exception($data['error'] ?? 'An error occurred while processing the request, in ' . $this->config->getModuleName() . ' module');
}
/**
* @inheritDoc
* @throws Exception
*/
function sendGetRequest(string $base, string $target, string $endpoint, array $data): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Add the base and target currencies to the data array
$data['base'] = $base;
// Build the query string if any data is provided
$query_string = '';
if (!empty($data)) {
$query_string = '?' . http_build_query($data);
}
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $query_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set HTTP headers including the X-AUTH-TOKEN
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
]);
// Execute the request and handle response
$output = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
self::exception(
$base,
$target,
[
'method' => 'GET',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => 500,
'error' => curl_error($ch),
],
500
);
}
// Get the status code
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response is valid JSON
if (!json_decode($output)) {
$exception_message = match ($status_code) {
401 => 'Unauthorized',
404 => 'Not found',
429 => 'Too many requests',
default => 'Invalid response',
};
self::exception(
$base,
$target,
[
'method' => 'GET',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => $status_code,
'error' => $exception_message,
],
$status_code
);
}
return json_decode($output);
}
/**
* @inheritDoc
* @throws Exception If the request fails
* @throws Exception If the response is invalid
* @throws Exception If the response is not valid JSON
* @throws Exception If the base or target currency is invalid
* @throws Exception If the endpoint is invalid
*/
function sendPostRequest(string $base, string $target, string $endpoint, array $data): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set HTTP headers including the X-AUTH-TOKEN
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
]);
// Execute the request and handle response
$output = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
self::exception(
$base,
$target,
[
'method' => 'POST',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => 500,
'error' => curl_error($ch),
],
500
);
}
// Get the status code
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response is valid JSON
if (!json_decode($output)) {
self::exception(
$base,
$target,
[
'method' => 'POST',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => $status_code,
'error' => 'Invalid response',
],
500
);
}
return json_decode($output);
}
/**
* @inheritDoc
* @throws Exception If the request fails
* @throws Exception If the response is invalid
* @throws Exception If the response is not valid JSON
* @throws Exception If the base or target currency is invalid
* @throws Exception If the endpoint is invalid
*/
function sendPutRequest(string $base, string $target, string $endpoint, array $data): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set HTTP headers including the X-AUTH-TOKEN
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
]);
// Execute the request and handle response
$output = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
self::exception(
$base,
$target,
[
'method' => 'PUT',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => 500,
'error' => curl_error($ch),
],
500
);
}
// Get the status code
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response is valid JSON
if (!json_decode($output)) {
self::exception(
$base,
$target,
[
'method' => 'PUT',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => $status_code,
'error' => 'Invalid response',
],
500
);
}
return json_decode($output);
}
/**
* @inheritDoc
* @throws Exception If the request fails
* @throws Exception If the response is invalid
* @throws Exception If the response is not valid JSON
* @throws Exception If the base or target currency is invalid
* @throws Exception If the endpoint is invalid
*/
function sendDeleteRequest(string $base, string $target, string $endpoint, array $data): object
{
// Check if the base and target currencies are valid
self::requireValidCurrency($base);
self::requireValidCurrency($target);
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set HTTP headers including the X-AUTH-TOKEN
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
]);
// Execute the request and handle response
$output = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
self::exception(
$base,
$target,
[
'method' => 'DELETE',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => 500,
'error' => curl_error($ch),
],
500
);
}
// Get the status code
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response is valid JSON
if (!json_decode($output)) {
self::exception(
$base,
$target,
[
'method' => 'DELETE',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => $status_code,
'error' => 'Invalid response',
],
500
);
}
return json_decode($output);
}
/**
* @inheritDoc
* @throws Exception
*/
function addRequestToLog(string $base, string $target, string $endpoint, object $response): void
{
// Add the request to the fxratesapi request log
$fxratesapi_lookups = new fxratesapi_conversion_rates_o();
$fxratesapi_lookups->add($base, $target, json_encode($response), $endpoint);
// Check if the request was added successfully
if (!$fxratesapi_lookups->id) {
throw new Exception('Failed to add request to log');
}
}
}