refactor
- move elixForms API logic to specific folder - move everything else under Api folder
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace Api\Auth;
|
||||
|
||||
use Core\Config;
|
||||
use Core\Request;
|
||||
|
||||
class ApiTokenAuthenticator
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): void
|
||||
{
|
||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
||||
if (!$authorization) {
|
||||
throw new \Exception('Missing Authorization header');
|
||||
}
|
||||
|
||||
if (!preg_match('/^Bearer\s+(.*)$/i', trim($authorization), $matches)) {
|
||||
throw new \Exception('Invalid Authorization header format');
|
||||
}
|
||||
|
||||
$token = $matches[1];
|
||||
$expected = $this->config->secret('api_access_token');
|
||||
|
||||
if (empty($expected) || !hash_equals((string) $expected, (string) $token)) {
|
||||
throw new \Exception('Invalid API access token');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use ElixForms\ElixFormsClient;
|
||||
|
||||
class ExampleController
|
||||
{
|
||||
private ElixFormsClient $elixFormsClient;
|
||||
|
||||
public function __construct(ElixFormsClient $elixFormsClient)
|
||||
{
|
||||
$this->elixFormsClient = $elixFormsClient;
|
||||
}
|
||||
|
||||
public function test(Request $req, Response $res)
|
||||
{
|
||||
$response = $this->elixFormsClient->request('GET', '/status');
|
||||
|
||||
return $res->json([
|
||||
'external_api_response' => $response['json'] ?? $response['body'],
|
||||
'status' => $response['status']
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use ElixForms\ElixFormsClient;
|
||||
use ElixForms\Exceptions\ElixFormsException;
|
||||
|
||||
class ExternalElixFormsController
|
||||
{
|
||||
private ElixFormsClient $elixFormsClient;
|
||||
|
||||
public function __construct(ElixFormsClient $elixFormsClient)
|
||||
{
|
||||
$this->elixFormsClient = $elixFormsClient;
|
||||
}
|
||||
|
||||
public function status(Request $req, Response $res)
|
||||
{
|
||||
try {
|
||||
$response = $this->elixFormsClient->request('GET', '/status');
|
||||
return $res->json([
|
||||
'status' => $response['status'],
|
||||
'body' => $response['json'] ?? $response['body']
|
||||
]);
|
||||
} catch (ElixFormsException $e) {
|
||||
return $res->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function login(Request $req, Response $res)
|
||||
{
|
||||
$data = $req->body();
|
||||
$username = $data['username'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
try {
|
||||
$token = $this->elixFormsClient->auth()->login($username, $password);
|
||||
return $res->json(['authToken' => $token]);
|
||||
} catch (ElixFormsException $e) {
|
||||
return $res->json(['error' => $e->getMessage()], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
|
||||
class UsersController {
|
||||
private $externalService;
|
||||
|
||||
public function __construct(\Services\ExternalApiService $externalService) {
|
||||
$this->externalService = $externalService;
|
||||
}
|
||||
|
||||
public function index(Request $req, Response $res) {
|
||||
return $res->json([
|
||||
'users' => ['Mario', 'Luigi']
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $req, Response $res) {
|
||||
$data = $req->body();
|
||||
return $res->json([
|
||||
'created' => $data
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
class Config {
|
||||
private $config;
|
||||
private $secrets;
|
||||
|
||||
private function loadConfig() {
|
||||
if ($this->config === null) {
|
||||
$file = __DIR__ . '/../../config/config.php';
|
||||
$this->config = file_exists($file) ? require $file : [];
|
||||
}
|
||||
}
|
||||
|
||||
private function loadSecrets() {
|
||||
if ($this->secrets === null) {
|
||||
$file = __DIR__ . '/../../config/secrets.php';
|
||||
$this->secrets = file_exists($file) ? require $file : [];
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = null) {
|
||||
$this->loadConfig();
|
||||
if (getenv(strtoupper($key)) !== false) {
|
||||
return getenv(strtoupper($key));
|
||||
}
|
||||
return $this->config[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function secret($key, $default = null) {
|
||||
$this->loadSecrets();
|
||||
if (getenv(strtoupper($key)) !== false) {
|
||||
return getenv(strtoupper($key));
|
||||
}
|
||||
return $this->secrets[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
/**
|
||||
* Simple service container / dependency injector.
|
||||
*
|
||||
* - bind($abstract, $concrete): registra una factory, callable o nome classe.
|
||||
* - singleton($abstract, $concrete): come bind ma mantiene l'istanza.
|
||||
* - make($abstract): risolve e restituisce l'istanza.
|
||||
*
|
||||
* Nota: usa reflection per risolvere le dipendenze del costruttore.
|
||||
*/
|
||||
class Container {
|
||||
/** @var array<string,mixed> */
|
||||
private $bindings = [];
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private $instances = [];
|
||||
|
||||
/**
|
||||
* Bind an abstract name (interface or key) to a concrete implementation.
|
||||
* $concrete can be:
|
||||
* - a callable: function(Container $c) { return new Foo(); }
|
||||
* - a string class name: 'App\\Foo'
|
||||
*
|
||||
* @param string $abstract
|
||||
* @param callable|string $concrete
|
||||
* @return void
|
||||
*/
|
||||
public function bind(string $abstract, $concrete): void {
|
||||
$this->bindings[$abstract] = $concrete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind as singleton. The first resolved instance is cached and returned afterwards.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @param callable|string $concrete
|
||||
* @return void
|
||||
*/
|
||||
public function singleton(string $abstract, $concrete): void {
|
||||
$this->bindings[$abstract] = $concrete;
|
||||
// mark as singleton with null placeholder
|
||||
$this->instances[$abstract] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an abstract to an instance.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function make(string $abstract) {
|
||||
// return existing singleton instance if already created
|
||||
if (array_key_exists($abstract, $this->instances) && $this->instances[$abstract] !== null) {
|
||||
return $this->instances[$abstract];
|
||||
}
|
||||
|
||||
if (!isset($this->bindings[$abstract])) {
|
||||
// if no binding, try to instantiate the abstract directly if it's a class
|
||||
if (class_exists($abstract)) {
|
||||
$object = $this->build($abstract);
|
||||
} else {
|
||||
throw new \Exception("No binding found for [{$abstract}]");
|
||||
}
|
||||
} else {
|
||||
$concrete = $this->bindings[$abstract];
|
||||
|
||||
if (is_callable($concrete)) {
|
||||
// factory receives the container
|
||||
$object = $concrete($this);
|
||||
} elseif (is_string($concrete) && class_exists($concrete)) {
|
||||
$object = $this->build($concrete);
|
||||
} else {
|
||||
throw new \Exception("Invalid binding for [{$abstract}]");
|
||||
}
|
||||
}
|
||||
|
||||
// if abstract was registered as singleton, cache the instance
|
||||
if (array_key_exists($abstract, $this->instances)) {
|
||||
$this->instances[$abstract] = $object;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an instance of the given class resolving constructor dependencies.
|
||||
*
|
||||
* @param string $class
|
||||
* @return object
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function build(string $class) {
|
||||
$reflector = new \ReflectionClass($class);
|
||||
|
||||
if (!$reflector->isInstantiable()) {
|
||||
throw new \Exception("Class {$class} is not instantiable");
|
||||
}
|
||||
|
||||
$constructor = $reflector->getConstructor();
|
||||
if (is_null($constructor)) {
|
||||
return new $class();
|
||||
}
|
||||
|
||||
$params = $constructor->getParameters();
|
||||
$dependencies = [];
|
||||
|
||||
foreach ($params as $param) {
|
||||
$type = $param->getType();
|
||||
|
||||
// If parameter has a class/interface type, resolve it from container
|
||||
if ($type && !$type->isBuiltin()) {
|
||||
$depClass = $type->getName();
|
||||
$dependencies[] = $this->make($depClass);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If default value is available, use it
|
||||
if ($param->isDefaultValueAvailable()) {
|
||||
$dependencies[] = $param->getDefaultValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cannot resolve the dependency
|
||||
$name = $param->getName();
|
||||
throw new \Exception("Unresolvable dependency [\${$name}] in class {$class}");
|
||||
}
|
||||
|
||||
return $reflector->newInstanceArgs($dependencies);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ElixFormsException extends Exception {
|
||||
// Custom exception per errori specifici di elixForms (es. login, lookup)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
class HttpClient {
|
||||
private $client;
|
||||
|
||||
private function client() {
|
||||
if ($this->client === null) {
|
||||
$this->client = new Client([
|
||||
'timeout' => 10.0,
|
||||
'http_errors' => false
|
||||
]);
|
||||
}
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function get(string $url, array $headers = []) {
|
||||
return $this->request('GET', $url, null, $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 = $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' => $status,
|
||||
'headers' => $resp->getHeaders(),
|
||||
'body' => $bodyRaw,
|
||||
'is_json' => $isJson,
|
||||
'json' => $isJson ? json_decode($bodyRaw, true) : null
|
||||
];
|
||||
|
||||
} catch (RequestException $e) {
|
||||
return [
|
||||
'status' => 500,
|
||||
'headers' => [],
|
||||
'body' => null,
|
||||
'is_json' => false,
|
||||
'json' => null,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\SyslogHandler;
|
||||
use Monolog\Processor\UidProcessor;
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
|
||||
class LoggerFactory {
|
||||
public static function create(string $name = null): Logger {
|
||||
$ident = $name ?? 'api';
|
||||
$logger = new Logger($ident);
|
||||
$handler = new SyslogHandler($ident, LOG_USER);
|
||||
$handler->setFormatter(new JsonFormatter());
|
||||
$logger->pushHandler($handler);
|
||||
$logger->pushProcessor(new UidProcessor());
|
||||
return $logger;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace Core\RateLimiter;
|
||||
|
||||
use Core\Config;
|
||||
|
||||
class FileRateLimiter implements RateLimiterInterface {
|
||||
private $dir;
|
||||
private $requests;
|
||||
private $window;
|
||||
|
||||
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 = $requests;
|
||||
$this->window = $window;
|
||||
}
|
||||
|
||||
private function fileForKey(string $key): string {
|
||||
return $this->dir . '/rl_' . md5($key) . '.json';
|
||||
}
|
||||
|
||||
public function allow(string $key): bool {
|
||||
$file = $this->fileForKey($key);
|
||||
$now = time();
|
||||
|
||||
$data = ['tokens' => $this->requests, 'last' => $now];
|
||||
if (file_exists($file)) {
|
||||
$fp = fopen($file, 'c+');
|
||||
if (!$fp) return true;
|
||||
flock($fp, LOCK_EX);
|
||||
$contents = stream_get_contents($fp);
|
||||
rewind($fp);
|
||||
$data = $contents ? json_decode($contents, true) : $data;
|
||||
|
||||
// refill tokens
|
||||
$elapsed = $now - ($data['last'] ?? $now);
|
||||
$rate = $this->requests / $this->window;
|
||||
$refill = floor($elapsed * $rate);
|
||||
$data['tokens'] = min($this->requests, ($data['tokens'] ?? $this->requests) + $refill);
|
||||
$data['last'] = $now;
|
||||
|
||||
if ($data['tokens'] > 0) {
|
||||
$data['tokens']--;
|
||||
ftruncate($fp, 0);
|
||||
fwrite($fp, json_encode($data));
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
return true;
|
||||
} else {
|
||||
ftruncate($fp, 0);
|
||||
fwrite($fp, json_encode($data));
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$fp = fopen($file, 'w');
|
||||
if (!$fp) return true;
|
||||
flock($fp, LOCK_EX);
|
||||
$data = ['tokens' => $this->requests - 1, 'last' => $now];
|
||||
fwrite($fp, json_encode($data));
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public function getRetryAfter(string $key): int {
|
||||
$file = $this->fileForKey($key);
|
||||
if (!file_exists($file)) return 0;
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
$tokens = $data['tokens'] ?? 0;
|
||||
if ($tokens > 0) return 0;
|
||||
$last = $data['last'] ?? time();
|
||||
$elapsed = time() - $last;
|
||||
$remaining = max(0, $this->window - $elapsed);
|
||||
return $remaining;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace Core\RateLimiter;
|
||||
|
||||
class InMemoryRateLimiter implements RateLimiterInterface {
|
||||
private $requests;
|
||||
private $window;
|
||||
private $state = [];
|
||||
|
||||
public function __construct(int $requests = 100, int $window = 60) {
|
||||
$this->requests = $requests;
|
||||
$this->window = $window;
|
||||
}
|
||||
|
||||
public function allow(string $key): bool {
|
||||
$now = time();
|
||||
if (!isset($this->state[$key])) {
|
||||
$this->state[$key] = ['tokens' => $this->requests - 1, 'last' => $now];
|
||||
return true;
|
||||
}
|
||||
$data = $this->state[$key];
|
||||
$elapsed = $now - $data['last'];
|
||||
$rate = $this->requests / $this->window;
|
||||
$refill = floor($elapsed * $rate);
|
||||
$data['tokens'] = min($this->requests, $data['tokens'] + $refill);
|
||||
$data['last'] = $now;
|
||||
if ($data['tokens'] > 0) {
|
||||
$data['tokens']--;
|
||||
$this->state[$key] = $data;
|
||||
return true;
|
||||
}
|
||||
$this->state[$key] = $data;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRetryAfter(string $key): int {
|
||||
if (!isset($this->state[$key])) return 0;
|
||||
$data = $this->state[$key];
|
||||
if ($data['tokens'] > 0) return 0;
|
||||
$elapsed = time() - $data['last'];
|
||||
return max(0, $this->window - $elapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Core\RateLimiter;
|
||||
|
||||
interface RateLimiterInterface {
|
||||
public function allow(string $key): bool;
|
||||
public function getRetryAfter(string $key): int;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
class Request {
|
||||
public function method() {
|
||||
return $_SERVER['REQUEST_METHOD'];
|
||||
}
|
||||
|
||||
public function path() {
|
||||
return strtok($_SERVER['REQUEST_URI'], '?');
|
||||
}
|
||||
|
||||
public function body() {
|
||||
return json_decode(file_get_contents('php://input'), true);
|
||||
}
|
||||
|
||||
public function query() {
|
||||
return $_GET;
|
||||
}
|
||||
|
||||
public function header(string $name): ?string
|
||||
{
|
||||
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
|
||||
return $_SERVER[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
class Response {
|
||||
public function json($data, $status = 200) {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
namespace Core;
|
||||
|
||||
class Router {
|
||||
private $routes = [];
|
||||
|
||||
public function __construct(\Core\Container $container) {
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function register($method, $path, $handler) {
|
||||
$this->routes[$method][$path] = $handler;
|
||||
}
|
||||
|
||||
public function get($path, $handler) { $this->register('GET', $path, $handler); }
|
||||
public function post($path, $handler) { $this->register('POST', $path, $handler); }
|
||||
|
||||
public function dispatch(Request $req, Response $res) {
|
||||
$method = $req->method();
|
||||
$path = $req->path();
|
||||
|
||||
// extract version prefix /v1/...
|
||||
if (preg_match('#^/v([0-9]+)(/.*)?$#', $path, $m)) {
|
||||
$version = $m[1];
|
||||
$pathWithoutVersion = $m[2] ?? '/';
|
||||
} else {
|
||||
$version = '1';
|
||||
$pathWithoutVersion = $path;
|
||||
}
|
||||
|
||||
// try exact route with versioned namespace
|
||||
$routeKey = $pathWithoutVersion;
|
||||
if (!isset($this->routes[$method][$routeKey])) {
|
||||
return $res->json(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$handler = $this->routes[$method][$routeKey];
|
||||
// handler can be 'UsersController@index' or 'Controllers\\UsersController@index'
|
||||
if (strpos($handler, '@') !== false) {
|
||||
list($class, $function) = explode('@', $handler);
|
||||
// if class not namespaced, prefix with Controllers\V{n}\
|
||||
if (strpos($class, '\\') === false) {
|
||||
$class = "Controllers\\V{$version}\\" . $class;
|
||||
}
|
||||
} else {
|
||||
return $res->json(['error' => 'Invalid handler'], 500);
|
||||
}
|
||||
|
||||
if (!class_exists($class)) {
|
||||
return $res->json(['error' => 'Controller not found'], 500);
|
||||
}
|
||||
|
||||
// snippet inside Router::dispatch
|
||||
$controller = $this->container->make($class);
|
||||
return $controller->$function($req, $res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
namespace Helpers;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Json
|
||||
{
|
||||
/**
|
||||
* Invia una risposta JSON coerente.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param int $status
|
||||
* @param bool $pretty
|
||||
* @param LoggerInterface|null $logger opzionale per loggare errori di encoding
|
||||
* @return void
|
||||
*/
|
||||
public static function send($data, int $status = 200, bool $pretty = false, ?LoggerInterface $logger = null): void
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
$options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
if ($pretty) {
|
||||
$options |= JSON_PRETTY_PRINT;
|
||||
}
|
||||
|
||||
try {
|
||||
$json = json_encode($data, $options | JSON_THROW_ON_ERROR);
|
||||
echo $json;
|
||||
} catch (\JsonException $e) {
|
||||
if ($logger) {
|
||||
$logger->error('JSON encode error', ['error' => $e->getMessage()]);
|
||||
}
|
||||
// fallback minimale: non esporre dettagli sensibili
|
||||
if (!headers_sent()) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
echo json_encode(['error' => 'Internal Server Error']);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia un errore JSON standardizzato.
|
||||
*
|
||||
* @param string|array $message
|
||||
* @param int $status
|
||||
* @param LoggerInterface|null $logger
|
||||
* @return void
|
||||
*/
|
||||
public static function sendError($message = 'Bad Request', int $status = 400, ?LoggerInterface $logger = null): void
|
||||
{
|
||||
$payload = [
|
||||
'error' => is_array($message) ? $message : ['message' => $message]
|
||||
];
|
||||
|
||||
if ($logger) {
|
||||
$logger->warning('API error response', ['status' => $status, 'payload' => $payload]);
|
||||
}
|
||||
|
||||
self::send($payload, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legge e decodifica il body JSON della richiesta.
|
||||
*
|
||||
* @param bool $assoc
|
||||
* @param int $maxBytes limite in byte per proteggere da payload troppo grandi
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function readJsonBody(bool $assoc = true, int $maxBytes = 1048576)
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
|
||||
if ($raw === false || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($raw) > $maxBytes) {
|
||||
throw new \RuntimeException('Payload too large', 413);
|
||||
}
|
||||
|
||||
try {
|
||||
return json_decode($raw, $assoc, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \InvalidArgumentException('Invalid JSON payload', 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Risposta per errori di validazione con formato coerente.
|
||||
*
|
||||
* @param array $errors mappa campo => messaggi
|
||||
* @param int $status
|
||||
* @return void
|
||||
*/
|
||||
public static function validationError(array $errors, int $status = 422): void
|
||||
{
|
||||
$payload = [
|
||||
'error' => 'validation_failed',
|
||||
'details' => $errors
|
||||
];
|
||||
self::send($payload, $status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace Services;
|
||||
|
||||
use Core\Config;
|
||||
use Core\HttpClient;
|
||||
|
||||
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('elixforms_api_base_url'), '/') . '/status';
|
||||
$token = $this->config->secret('elixforms_api_token');
|
||||
$headers = ['Authorization' => 'Bearer ' . $token];
|
||||
return $this->httpClient->get($url, $headers);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user