refactor
- add documentation for EF API calling implementation - greater adherence to standards - better DI logic
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
# API Server - Regole e Skill Set
|
||||||
|
|
||||||
|
Queste regole definiscono il comportamento per tutti gli sviluppi futuri su questa repository, al fine di mantenere l'architettura pulita e scalabile.
|
||||||
|
|
||||||
|
1. **Dependency Injection Obbligatoria (IoC)**
|
||||||
|
Non utilizzare mai chiamate a metodi statici per accedere a servizi o configurazioni (es. evitare `Config::get()` o `HttpClient::get()`).
|
||||||
|
Tutte le dipendenze devono essere iniettate tramite costruttore. Il container in `bootstrap.php` provvederà all'autowiring automatico.
|
||||||
|
|
||||||
|
2. **Namespace e Struttura dei Controller**
|
||||||
|
Tutti i nuovi Controller devono essere posizionati all'interno della cartella relativa alla loro versione (es. `src/Controllers/V1/`) e devono avere il namespace corretto (es. `namespace Controllers\V1;`). Questo permette al `Router` di mapparli automaticamente partendo dalle route versionate (es. `/v1/risorsa`).
|
||||||
|
|
||||||
|
3. **Risposte HTTP Standard**
|
||||||
|
Non utilizzare mai funzioni di output diretto (come `echo`, `print` o `header()`) all'interno dei Controller.
|
||||||
|
Usa sempre l'oggetto `Response` (iniettato come parametro o generato internamente) e chiama il metodo `$res->json($payload, $statusCode)` per uniformare l'output.
|
||||||
|
|
||||||
|
4. **Gestione Errori e Sicurezza**
|
||||||
|
Non includere mai stack trace o dettagli sensibili (segreti, stringhe di connessione) nelle risposte JSON d'errore o nei log generici.
|
||||||
|
Lascia che le eccezioni vengano catturate dal Global Exception Handler o usa il `LoggerInterface` per tracciare i problemi a livello server.
|
||||||
|
|
||||||
|
5. **Validazione dell'Input**
|
||||||
|
Assicurati sempre di validare l'input proveniente da `$req->body()` o dai parametri URL prima di processarlo con la business logic applicativa. (Consigliato l'uso di DTO).
|
||||||
+20
-6
@@ -15,26 +15,40 @@ use Services\ExternalApiService;
|
|||||||
|
|
||||||
$container = new Container();
|
$container = new Container();
|
||||||
|
|
||||||
|
$config = new Config();
|
||||||
|
$container->singleton(Config::class, function() use ($config) {
|
||||||
|
return $config;
|
||||||
|
});
|
||||||
|
|
||||||
|
$container->singleton(\Core\HttpClient::class, function() {
|
||||||
|
return new \Core\HttpClient();
|
||||||
|
});
|
||||||
|
|
||||||
// Rate limiter driver configurabile via config or env: 'file' or 'memory'
|
// Rate limiter driver configurabile via config or env: 'file' or 'memory'
|
||||||
$rlDriver = Config::get('rate_limiter_driver', 'file');
|
$rlDriver = $config->get('rate_limiter_driver', 'file');
|
||||||
|
|
||||||
if ($rlDriver === 'memory') {
|
if ($rlDriver === 'memory') {
|
||||||
$container->singleton(RateLimiterInterface::class, function($c) {
|
$container->singleton(RateLimiterInterface::class, function($c) {
|
||||||
$requests = (int) \Core\Config::get('rate_limit_requests', 100);
|
$config = $c->make(Config::class);
|
||||||
$window = (int) \Core\Config::get('rate_limit_window_seconds', 60);
|
$requests = (int) $config->get('rate_limit_requests', 100);
|
||||||
|
$window = (int) $config->get('rate_limit_window_seconds', 60);
|
||||||
return new InMemoryRateLimiter($requests, $window);
|
return new InMemoryRateLimiter($requests, $window);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$container->singleton(RateLimiterInterface::class, function($c) {
|
$container->singleton(RateLimiterInterface::class, function($c) {
|
||||||
$dir = Config::get('rate_limit_storage_dir', sys_get_temp_dir() . '/api_rate_limit');
|
$config = $c->make(Config::class);
|
||||||
return new FileRateLimiter($dir);
|
$dir = $config->get('rate_limit_storage_dir', sys_get_temp_dir() . '/api_rate_limit');
|
||||||
|
$requests = (int) $config->get('rate_limit_requests', 100);
|
||||||
|
$window = (int) $config->get('rate_limit_window_seconds', 60);
|
||||||
|
return new FileRateLimiter($dir, $requests, $window);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger binding as PSR LoggerInterface
|
// Logger binding as PSR LoggerInterface
|
||||||
$container->singleton(LoggerInterface::class, function($c) {
|
$container->singleton(LoggerInterface::class, function($c) {
|
||||||
|
$config = $c->make(Config::class);
|
||||||
// LoggerFactory::create returns a Monolog\Logger instance
|
// LoggerFactory::create returns a Monolog\Logger instance
|
||||||
return LoggerFactory::create(Config::get('log_ident', 'api'));
|
return LoggerFactory::create($config->get('log_ident', 'api'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// External API service binding
|
// External API service binding
|
||||||
|
|||||||
Binary file not shown.
@@ -8,6 +8,22 @@ use Core\RateLimiter\RateLimiterInterface;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
$logger = $container->make(LoggerInterface::class);
|
$logger = $container->make(LoggerInterface::class);
|
||||||
|
|
||||||
|
set_exception_handler(function (\Throwable $e) use ($logger) {
|
||||||
|
$logger->error('Uncaught Exception: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
http_response_code(500);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'error' => 'Internal Server Error',
|
||||||
|
'message' => 'An unexpected error occurred.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
$limiter = $container->make(RateLimiterInterface::class);
|
$limiter = $container->make(RateLimiterInterface::class);
|
||||||
|
|
||||||
$router = new Router($container); // vedi nota: router può ricevere container
|
$router = new Router($container); // vedi nota: router può ricevere container
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ use Core\Response;
|
|||||||
use Core\Config;
|
use Core\Config;
|
||||||
use Core\HttpClient;
|
use Core\HttpClient;
|
||||||
|
|
||||||
class ExampleController {
|
class ExampleController
|
||||||
|
{
|
||||||
|
private $config;
|
||||||
|
private $httpClient;
|
||||||
|
|
||||||
public function test(Request $req, Response $res) {
|
public function __construct(Config $config, HttpClient $httpClient)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
$url = Config::get('external_api_base_url') . '/status';
|
public function test(Request $req, Response $res)
|
||||||
|
{
|
||||||
|
|
||||||
$response = HttpClient::get($url, [
|
$url = $this->config->get('external_api_base_url') . '/status';
|
||||||
'Authorization' => 'Bearer ' . Config::secret('external_api_token')
|
|
||||||
|
$response = $this->httpClient->get($url, [
|
||||||
|
'Authorization' => 'Bearer ' . $this->config->secret('external_api_token')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $res->json([
|
return $res->json([
|
||||||
|
|||||||
+14
-14
@@ -2,36 +2,36 @@
|
|||||||
namespace Core;
|
namespace Core;
|
||||||
|
|
||||||
class Config {
|
class Config {
|
||||||
private static $config;
|
private $config;
|
||||||
private static $secrets;
|
private $secrets;
|
||||||
|
|
||||||
private static function loadConfig() {
|
private function loadConfig() {
|
||||||
if (!self::$config) {
|
if ($this->config === null) {
|
||||||
$file = __DIR__ . '/../../config/config.php';
|
$file = __DIR__ . '/../../config/config.php';
|
||||||
self::$config = file_exists($file) ? require $file : [];
|
$this->config = file_exists($file) ? require $file : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function loadSecrets() {
|
private function loadSecrets() {
|
||||||
if (!self::$secrets) {
|
if ($this->secrets === null) {
|
||||||
$file = __DIR__ . '/../../config/secrets.php';
|
$file = __DIR__ . '/../../config/secrets.php';
|
||||||
self::$secrets = file_exists($file) ? require $file : [];
|
$this->secrets = file_exists($file) ? require $file : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get($key, $default = null) {
|
public function get($key, $default = null) {
|
||||||
self::loadConfig();
|
$this->loadConfig();
|
||||||
if (getenv(strtoupper($key)) !== false) {
|
if (getenv(strtoupper($key)) !== false) {
|
||||||
return getenv(strtoupper($key));
|
return getenv(strtoupper($key));
|
||||||
}
|
}
|
||||||
return self::$config[$key] ?? $default;
|
return $this->config[$key] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function secret($key, $default = null) {
|
public function secret($key, $default = null) {
|
||||||
self::loadSecrets();
|
$this->loadSecrets();
|
||||||
if (getenv(strtoupper($key)) !== false) {
|
if (getenv(strtoupper($key)) !== false) {
|
||||||
return getenv(strtoupper($key));
|
return getenv(strtoupper($key));
|
||||||
}
|
}
|
||||||
return self::$secrets[$key] ?? $default;
|
return $this->secrets[$key] ?? $default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace Core;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ElixFormsException extends Exception {
|
||||||
|
// Custom exception per errori specifici di elixForms (es. login, lookup)
|
||||||
|
}
|
||||||
+38
-21
@@ -5,41 +5,58 @@ use GuzzleHttp\Client;
|
|||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
|
||||||
class HttpClient {
|
class HttpClient {
|
||||||
private static $client;
|
private $client;
|
||||||
|
|
||||||
private static function client() {
|
private function client() {
|
||||||
if (!self::$client) {
|
if ($this->client === null) {
|
||||||
self::$client = new Client([
|
$this->client = new Client([
|
||||||
'timeout' => 10.0,
|
'timeout' => 10.0,
|
||||||
'http_errors' => false
|
'http_errors' => false
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return self::$client;
|
return $this->client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get(string $url, array $headers = []) {
|
public function get(string $url, array $headers = []) {
|
||||||
try {
|
return $this->request('GET', $url, null, $headers);
|
||||||
$resp = self::client()->request('GET', $url, ['headers' => $headers]);
|
|
||||||
return [
|
|
||||||
'status' => $resp->getStatusCode(),
|
|
||||||
'body' => json_decode($resp->getBody()->getContents(), true)
|
|
||||||
];
|
|
||||||
} catch (RequestException $e) {
|
|
||||||
return ['status' => 500, 'body' => null, 'error' => $e->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function post(string $url, $data = null, array $headers = []) {
|
public function post(string $url, $data = null, array $headers = []) {
|
||||||
|
return $this->request('POST', $url, $data, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function request(string $method, string $url, $data, array $headers) {
|
||||||
try {
|
try {
|
||||||
$options = ['headers' => $headers];
|
$options = ['headers' => $headers];
|
||||||
if ($data !== null) $options['json'] = $data;
|
if ($data !== null) {
|
||||||
$resp = self::client()->request('POST', $url, $options);
|
$options['json'] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resp = $this->client()->request($method, $url, $options);
|
||||||
|
|
||||||
|
$status = $resp->getStatusCode();
|
||||||
|
$bodyRaw = $resp->getBody()->getContents();
|
||||||
|
$contentType = $resp->getHeaderLine('Content-Type');
|
||||||
|
|
||||||
|
$isJson = stripos($contentType, 'application/json') !== false;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => $resp->getStatusCode(),
|
'status' => $status,
|
||||||
'body' => json_decode($resp->getBody()->getContents(), true)
|
'headers' => $resp->getHeaders(),
|
||||||
|
'body' => $bodyRaw,
|
||||||
|
'is_json' => $isJson,
|
||||||
|
'json' => $isJson ? json_decode($bodyRaw, true) : null
|
||||||
];
|
];
|
||||||
|
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
return ['status' => 500, 'body' => null, 'error' => $e->getMessage()];
|
return [
|
||||||
|
'status' => 500,
|
||||||
|
'headers' => [],
|
||||||
|
'body' => null,
|
||||||
|
'is_json' => false,
|
||||||
|
'json' => null,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Monolog\Formatter\JsonFormatter;
|
|||||||
|
|
||||||
class LoggerFactory {
|
class LoggerFactory {
|
||||||
public static function create(string $name = null): Logger {
|
public static function create(string $name = null): Logger {
|
||||||
$ident = $name ?? Config::get('log_ident', 'api');
|
$ident = $name ?? 'api';
|
||||||
$logger = new Logger($ident);
|
$logger = new Logger($ident);
|
||||||
$handler = new SyslogHandler($ident, LOG_USER);
|
$handler = new SyslogHandler($ident, LOG_USER);
|
||||||
$handler->setFormatter(new JsonFormatter());
|
$handler->setFormatter(new JsonFormatter());
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ class FileRateLimiter implements RateLimiterInterface {
|
|||||||
private $requests;
|
private $requests;
|
||||||
private $window;
|
private $window;
|
||||||
|
|
||||||
public function __construct(string $storageDir = null) {
|
public function __construct(string $storageDir = null, int $requests = 100, int $window = 60) {
|
||||||
$this->dir = $storageDir ?? sys_get_temp_dir() . '/api_rate_limit';
|
$this->dir = $storageDir ?? sys_get_temp_dir() . '/api_rate_limit';
|
||||||
if (!is_dir($this->dir)) {
|
if (!is_dir($this->dir)) {
|
||||||
mkdir($this->dir, 0700, true);
|
mkdir($this->dir, 0700, true);
|
||||||
}
|
}
|
||||||
$this->requests = (int) Config::get('rate_limit_requests', 100);
|
$this->requests = $requests;
|
||||||
$this->window = (int) Config::get('rate_limit_window_seconds', 60);
|
$this->window = $window;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fileForKey(string $key): string {
|
private function fileForKey(string $key): string {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
namespace Services;
|
||||||
|
|
||||||
|
use Core\Config;
|
||||||
|
use Core\HttpClient;
|
||||||
|
use Core\ElixFormsException;
|
||||||
|
|
||||||
|
class ElixFormsAuthenticationClient {
|
||||||
|
private $config;
|
||||||
|
private $httpClient;
|
||||||
|
|
||||||
|
public function __construct(Config $config, HttpClient $httpClient) {
|
||||||
|
$this->config = $config;
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esegue il login verso l'API di elixForms.
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $password
|
||||||
|
* @return string Il token di autenticazione (authToken)
|
||||||
|
* @throws ElixFormsException Se le credenziali sono errate o c'è un errore server
|
||||||
|
*/
|
||||||
|
public function login(string $username, string $password): string {
|
||||||
|
$baseUrl = rtrim($this->config->get('elixforms_api_base_url'), '/');
|
||||||
|
$url = $baseUrl . '/eF/services/api/authentication/login/v1';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'username' => $username,
|
||||||
|
'password' => $password
|
||||||
|
];
|
||||||
|
|
||||||
|
// L'API supporta application/json
|
||||||
|
$response = $this->httpClient->post($url, $data);
|
||||||
|
|
||||||
|
if ($response['status'] !== 200) {
|
||||||
|
throw new ElixFormsException("Errore durante il login elixForms. HTTP Status: " . $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['is_json'] || empty($response['json'])) {
|
||||||
|
throw new ElixFormsException("Risposta non valida dal server elixForms: atteso JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = $response['json'];
|
||||||
|
|
||||||
|
// Estraiamo il token dalla struttura complessa
|
||||||
|
if (isset($json['value']['authToken'])) {
|
||||||
|
return $json['value']['authToken'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ElixFormsException("authToken non trovato nella risposta del login elixForms.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effettua il logout invalidando il token sul server elixForms.
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $token
|
||||||
|
* @return bool True se il logout ha successo
|
||||||
|
* @throws ElixFormsException Se c'è un errore durante il logout
|
||||||
|
*/
|
||||||
|
public function logout(string $username, string $token): bool {
|
||||||
|
$baseUrl = rtrim($this->config->get('elixforms_api_base_url'), '/');
|
||||||
|
$url = $baseUrl . '/eF/services/api/authentication/' . urlencode($username) . '/logout/v1';
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->httpClient->post($url, null, $headers);
|
||||||
|
|
||||||
|
if ($response['status'] !== 200) {
|
||||||
|
throw new ElixFormsException("Errore durante il logout elixForms. HTTP Status: " . $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,22 @@ namespace Services;
|
|||||||
use Core\Config;
|
use Core\Config;
|
||||||
use Core\HttpClient;
|
use Core\HttpClient;
|
||||||
|
|
||||||
class ExternalApiService {
|
class ExternalApiService
|
||||||
public function getStatus() {
|
{
|
||||||
$url = rtrim(Config::get('external_api_base_url'), '/') . '/status';
|
private $config;
|
||||||
$token = Config::secret('external_api_token');
|
private $httpClient;
|
||||||
|
|
||||||
|
public function __construct(Config $config, HttpClient $httpClient)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus()
|
||||||
|
{
|
||||||
|
$url = rtrim($this->config->get('external_api_base_url'), '/') . '/status';
|
||||||
|
$token = $this->config->secret('external_api_token');
|
||||||
$headers = ['Authorization' => 'Bearer ' . $token];
|
$headers = ['Authorization' => 'Bearer ' . $token];
|
||||||
return HttpClient::get($url, $headers);
|
return $this->httpClient->get($url, $headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ final class RateLimiterTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testBlocksWhenExceeded(): void {
|
public function testBlocksWhenExceeded(): void {
|
||||||
$limiter = new FileRateLimiter($this->dir);
|
$requests = 5;
|
||||||
|
$limiter = new FileRateLimiter($this->dir, $requests, 60);
|
||||||
$key = 'test-client-2';
|
$key = 'test-client-2';
|
||||||
$requests = (int) \Core\Config::get('rate_limit_requests', 5);
|
|
||||||
for ($i = 0; $i < $requests; $i++) {
|
for ($i = 0; $i < $requests; $i++) {
|
||||||
$this->assertTrue($limiter->allow($key));
|
$this->assertTrue($limiter->allow($key));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user