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();
|
||||
|
||||
$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'
|
||||
$rlDriver = Config::get('rate_limiter_driver', 'file');
|
||||
$rlDriver = $config->get('rate_limiter_driver', 'file');
|
||||
|
||||
if ($rlDriver === 'memory') {
|
||||
$container->singleton(RateLimiterInterface::class, function($c) {
|
||||
$requests = (int) \Core\Config::get('rate_limit_requests', 100);
|
||||
$window = (int) \Core\Config::get('rate_limit_window_seconds', 60);
|
||||
$config = $c->make(Config::class);
|
||||
$requests = (int) $config->get('rate_limit_requests', 100);
|
||||
$window = (int) $config->get('rate_limit_window_seconds', 60);
|
||||
return new InMemoryRateLimiter($requests, $window);
|
||||
});
|
||||
} else {
|
||||
$container->singleton(RateLimiterInterface::class, function($c) {
|
||||
$dir = Config::get('rate_limit_storage_dir', sys_get_temp_dir() . '/api_rate_limit');
|
||||
return new FileRateLimiter($dir);
|
||||
$config = $c->make(Config::class);
|
||||
$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
|
||||
$container->singleton(LoggerInterface::class, function($c) {
|
||||
$config = $c->make(Config::class);
|
||||
// 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
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,22 @@ use Core\RateLimiter\RateLimiterInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
$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);
|
||||
|
||||
$router = new Router($container); // vedi nota: router può ricevere container
|
||||
|
||||
@@ -6,14 +6,24 @@ use Core\Response;
|
||||
use Core\Config;
|
||||
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, [
|
||||
'Authorization' => 'Bearer ' . Config::secret('external_api_token')
|
||||
$url = $this->config->get('external_api_base_url') . '/status';
|
||||
|
||||
$response = $this->httpClient->get($url, [
|
||||
'Authorization' => 'Bearer ' . $this->config->secret('external_api_token')
|
||||
]);
|
||||
|
||||
return $res->json([
|
||||
|
||||
+14
-14
@@ -2,36 +2,36 @@
|
||||
namespace Core;
|
||||
|
||||
class Config {
|
||||
private static $config;
|
||||
private static $secrets;
|
||||
private $config;
|
||||
private $secrets;
|
||||
|
||||
private static function loadConfig() {
|
||||
if (!self::$config) {
|
||||
private function loadConfig() {
|
||||
if ($this->config === null) {
|
||||
$file = __DIR__ . '/../../config/config.php';
|
||||
self::$config = file_exists($file) ? require $file : [];
|
||||
$this->config = file_exists($file) ? require $file : [];
|
||||
}
|
||||
}
|
||||
|
||||
private static function loadSecrets() {
|
||||
if (!self::$secrets) {
|
||||
private function loadSecrets() {
|
||||
if ($this->secrets === null) {
|
||||
$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) {
|
||||
self::loadConfig();
|
||||
public function get($key, $default = null) {
|
||||
$this->loadConfig();
|
||||
if (getenv(strtoupper($key)) !== false) {
|
||||
return getenv(strtoupper($key));
|
||||
}
|
||||
return self::$config[$key] ?? $default;
|
||||
return $this->config[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function secret($key, $default = null) {
|
||||
self::loadSecrets();
|
||||
public function secret($key, $default = null) {
|
||||
$this->loadSecrets();
|
||||
if (getenv(strtoupper($key)) !== false) {
|
||||
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;
|
||||
|
||||
class HttpClient {
|
||||
private static $client;
|
||||
private $client;
|
||||
|
||||
private static function client() {
|
||||
if (!self::$client) {
|
||||
self::$client = new Client([
|
||||
private function client() {
|
||||
if ($this->client === null) {
|
||||
$this->client = new Client([
|
||||
'timeout' => 10.0,
|
||||
'http_errors' => false
|
||||
]);
|
||||
}
|
||||
return self::$client;
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public static function get(string $url, array $headers = []) {
|
||||
try {
|
||||
$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 function get(string $url, array $headers = []) {
|
||||
return $this->request('GET', $url, null, $headers);
|
||||
}
|
||||
|
||||
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 {
|
||||
$options = ['headers' => $headers];
|
||||
if ($data !== null) $options['json'] = $data;
|
||||
$resp = self::client()->request('POST', $url, $options);
|
||||
if ($data !== null) {
|
||||
$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 [
|
||||
'status' => $resp->getStatusCode(),
|
||||
'body' => json_decode($resp->getBody()->getContents(), true)
|
||||
'status' => $status,
|
||||
'headers' => $resp->getHeaders(),
|
||||
'body' => $bodyRaw,
|
||||
'is_json' => $isJson,
|
||||
'json' => $isJson ? json_decode($bodyRaw, true) : null
|
||||
];
|
||||
|
||||
} 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 {
|
||||
public static function create(string $name = null): Logger {
|
||||
$ident = $name ?? Config::get('log_ident', 'api');
|
||||
$ident = $name ?? 'api';
|
||||
$logger = new Logger($ident);
|
||||
$handler = new SyslogHandler($ident, LOG_USER);
|
||||
$handler->setFormatter(new JsonFormatter());
|
||||
|
||||
@@ -8,13 +8,13 @@ class FileRateLimiter implements RateLimiterInterface {
|
||||
private $requests;
|
||||
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';
|
||||
if (!is_dir($this->dir)) {
|
||||
mkdir($this->dir, 0700, true);
|
||||
}
|
||||
$this->requests = (int) Config::get('rate_limit_requests', 100);
|
||||
$this->window = (int) Config::get('rate_limit_window_seconds', 60);
|
||||
$this->requests = $requests;
|
||||
$this->window = $window;
|
||||
}
|
||||
|
||||
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\HttpClient;
|
||||
|
||||
class ExternalApiService {
|
||||
public function getStatus() {
|
||||
$url = rtrim(Config::get('external_api_base_url'), '/') . '/status';
|
||||
$token = Config::secret('external_api_token');
|
||||
class ExternalApiService
|
||||
{
|
||||
private $config;
|
||||
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];
|
||||
return HttpClient::get($url, $headers);
|
||||
return $this->httpClient->get($url, $headers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ final class RateLimiterTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testBlocksWhenExceeded(): void {
|
||||
$limiter = new FileRateLimiter($this->dir);
|
||||
$requests = 5;
|
||||
$limiter = new FileRateLimiter($this->dir, $requests, 60);
|
||||
$key = 'test-client-2';
|
||||
$requests = (int) \Core\Config::get('rate_limit_requests', 5);
|
||||
|
||||
for ($i = 0; $i < $requests; $i++) {
|
||||
$this->assertTrue($limiter->allow($key));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user