- add documentation for EF API calling implementation
- greater adherence to standards
- better DI logic
This commit is contained in:
2026-07-13 17:09:36 +02:00
parent 250ee892dc
commit 1766057cb9
13 changed files with 235 additions and 57 deletions
+21
View File
@@ -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
View File
@@ -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.
+16
View File
@@ -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
+15 -5
View File
@@ -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
View File
@@ -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;
}
}
+8
View File
@@ -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
View File
@@ -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()
];
}
}
}
+1 -1
View File
@@ -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());
+3 -3
View File
@@ -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;
}
}
+16 -5
View File
@@ -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);
}
}
+3 -2
View File
@@ -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));
}