Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2289f6e5be | ||
|
|
7a015f2b70 | ||
|
|
1cf674a31c | ||
|
|
01b22f9c2d |
@@ -32,7 +32,7 @@ namespace ElixForms\Common;
|
|||||||
|
|
||||||
Le pagine importano con:
|
Le pagine importano con:
|
||||||
```php
|
```php
|
||||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
use ElixForms\Common\ElixFormsComponent;
|
use ElixForms\Common\ElixFormsComponent;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ QueryParamHelper::getDecodedTextFromQuery('COL0002'); // ?string (testo URL-deco
|
|||||||
|
|
||||||
### `HttpClient.php` (Utility Richieste HTTP REST)
|
### `HttpClient.php` (Utility Richieste HTTP REST)
|
||||||
|
|
||||||
Classe orientata agli oggetti per effettuare chiamate HTTP server-to-server sicure verso Web Service esterni (es. endpoint contratti):
|
Adapter applicativo basato su Guzzle per effettuare chiamate HTTP server-to-server sicure verso Web Service esterni (es. endpoint contratti). Accetta un `GuzzleHttp\\ClientInterface` opzionale nel costruttore per consentire mocking e test senza traffico di rete:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use ElixForms\Common\HttpClient;
|
use ElixForms\Common\HttpClient;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ Questa struttura è l'equivalente PHP del monorepo React in `react/`.
|
|||||||
|
|
||||||
```
|
```
|
||||||
elixforms-custom-pages/
|
elixforms-custom-pages/
|
||||||
|
├── composer.json ← Configurazione Composer e autoload PSR-4
|
||||||
|
├── vendor/ ← Autoloader generato (non versionato)
|
||||||
├── react/ ← Custom pages React (esistente)
|
├── react/ ← Custom pages React (esistente)
|
||||||
└── php/ ← Custom pages PHP
|
└── php/ ← Custom pages PHP
|
||||||
├── AGENTS.md ← Regole dell'agente PHP
|
├── AGENTS.md ← Regole dell'agente PHP
|
||||||
@@ -35,7 +37,7 @@ elixforms-custom-pages/
|
|||||||
|
|
||||||
Ogni pagina (es. `scelta-carriera/`) è un file PHP autonomo che:
|
Ogni pagina (es. `scelta-carriera/`) è un file PHP autonomo che:
|
||||||
|
|
||||||
1. **Include la libreria `common/`** tramite `require_once`
|
1. **Carica l'autoloader Composer** tramite `vendor/autoload.php`
|
||||||
2. **Definisce una classe** che estende `ElixFormsComponent`
|
2. **Definisce una classe** che estende `ElixFormsComponent`
|
||||||
3. **Sovrascrive `createCustomFormFields()`** per i campi specifici
|
3. **Sovrascrive `createCustomFormFields()`** per i campi specifici
|
||||||
4. **Istanzia e renderizza** la pagina con `echo $page->render()`
|
4. **Istanzia e renderizza** la pagina con `echo $page->render()`
|
||||||
@@ -50,13 +52,10 @@ Il progetto usa **namespace PSR-4**:
|
|||||||
namespace ElixForms\Common; // Per le classi in common/
|
namespace ElixForms\Common; // Per le classi in common/
|
||||||
```
|
```
|
||||||
|
|
||||||
L'autoloading è gestito manualmente con `require_once` (non Composer):
|
L'autoloading PSR-4 è gestito da Composer:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
// ElixFormsComponent.php include internamente:
|
|
||||||
// - QueryParamHelper.php
|
|
||||||
// - ElixFormsElement.php
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requisiti PHP
|
## Requisiti PHP
|
||||||
@@ -108,7 +107,7 @@ Identica alla versione React:
|
|||||||
| **Linguaggio** | TypeScript/JSX | PHP 8.0+ |
|
| **Linguaggio** | TypeScript/JSX | PHP 8.0+ |
|
||||||
| **Rendering** | Client-side | Server-side |
|
| **Rendering** | Client-side | Server-side |
|
||||||
| **Build tool** | Vite (single bundle JS) | Nessuno (file serviti direttamente) |
|
| **Build tool** | Vite (single bundle JS) | Nessuno (file serviti direttamente) |
|
||||||
| **Dipendenze** | npm (React, Vite, plugin) | Nessuna (PHP built-in) |
|
| **Dipendenze** | npm (React, Vite, plugin) | Composer (autoload PSR-4) |
|
||||||
| **CSS loading** | `@import url()` + CSS injection plugin | `<link>` tags |
|
| **CSS loading** | `@import url()` + CSS injection plugin | `<link>` tags |
|
||||||
| **State management** | React state (`this.setState`) | Stateless (`$_GET`) |
|
| **State management** | React state (`this.setState`) | Stateless (`$_GET`) |
|
||||||
| **Deploy** | Upload del bundle `.js` | Upload dei file `.php` |
|
| **Deploy** | Upload del bundle `.js` | Upload dei file `.php` |
|
||||||
@@ -116,9 +115,10 @@ Identica alla versione React:
|
|||||||
|
|
||||||
## Deploy su Apache
|
## Deploy su Apache
|
||||||
|
|
||||||
1. Caricare la cartella `php/` sul server Apache
|
1. Eseguire `composer install --no-dev --optimize-autoloader` dalla root del repository
|
||||||
2. Configurazione Apache minima:
|
2. Caricare sul server le cartelle `php/` e `vendor/`, mantenendole allo stesso livello
|
||||||
|
3. Configurazione Apache minima:
|
||||||
- `mod_php` o `php-fpm` abilitato
|
- `mod_php` o `php-fpm` abilitato
|
||||||
- `DirectoryIndex index.php` (default Apache)
|
- `DirectoryIndex index.php` (default Apache)
|
||||||
3. La pagina è accessibile via URL: `https://server/php/nome-pagina/`
|
4. La pagina è accessibile via URL: `https://server/php/nome-pagina/`
|
||||||
4. elixForms reindirizza l'utente alla pagina passando i parametri via query string
|
5. elixForms reindirizza l'utente alla pagina passando i parametri via query string
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ FROM php:8.5.8-cli
|
|||||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8000 9023
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["php", "-S", "0.0.0.0:8000"]
|
CMD ["php", "-S", "0.0.0.0:8000"]
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ xdebug.mode=develop,debug
|
|||||||
xdebug.start_with_request=yes
|
xdebug.start_with_request=yes
|
||||||
xdebug.client_host=host.docker.internal
|
xdebug.client_host=host.docker.internal
|
||||||
xdebug.client_port=9023
|
xdebug.client_port=9023
|
||||||
xdebug.log_level=0
|
xdebug.log=/tmp/xdebug.log
|
||||||
xdebug.idekey=VSCODE
|
xdebug.log_level=7
|
||||||
|
xdebug.idekey=VSCODE
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules
|
node_modules
|
||||||
|
vendor
|
||||||
|
|
||||||
# Build generated files
|
# Build generated files
|
||||||
dist
|
dist
|
||||||
@@ -27,6 +28,7 @@ temp
|
|||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
||||||
# OSX
|
# OSX
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -54,4 +56,4 @@ obj
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
# Configuration files
|
# Configuration files
|
||||||
**/config.json
|
**/config.json
|
||||||
|
|||||||
Vendored
+1
@@ -54,6 +54,7 @@
|
|||||||
"name": "WWW - Listen for Xdebug",
|
"name": "WWW - Listen for Xdebug",
|
||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
"hostname": "0.0.0.0",
|
||||||
"port": 9023,
|
"port": 9023,
|
||||||
"pathMappings": {
|
"pathMappings": {
|
||||||
"/app": "${workspaceFolder}/php"
|
"/app": "${workspaceFolder}/php"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
SETLOCAL EnableDelayedExpansion
|
||||||
|
echo %cmdcmdline% | findstr /i /c:"%~nx0" >NUL && set iscommandline=1
|
||||||
|
echo %PSModulePath% | findstr /i /c:"%USERPROFILE%" >NUL && set ispowershell=1
|
||||||
|
cd /D "%~dp0"
|
||||||
|
|
||||||
|
@pwsh -NoProfile -ExecutionPolicy Bypass -Command .\scripts\composer-dump-autoload.ps1
|
||||||
|
|
||||||
|
IF DEFINED iscommandline IF NOT DEFINED ispowershell pause
|
||||||
|
ENDLOCAL
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
SETLOCAL EnableDelayedExpansion
|
||||||
|
echo %cmdcmdline% | findstr /i /c:"%~nx0" >NUL && set iscommandline=1
|
||||||
|
echo %PSModulePath% | findstr /i /c:"%USERPROFILE%" >NUL && set ispowershell=1
|
||||||
|
cd /D "%~dp0"
|
||||||
|
|
||||||
|
@pwsh -NoProfile -ExecutionPolicy Bypass -Command .\scripts\composer-install.ps1
|
||||||
|
|
||||||
|
IF DEFINED iscommandline IF NOT DEFINED ispowershell pause
|
||||||
|
ENDLOCAL
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "elixforms-web-pages/pages",
|
||||||
|
"description": "Custom web pages PHP per elixForms",
|
||||||
|
"license": "proprietary",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ElixForms\\Common\\": "php/common/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2491
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,16 @@ services:
|
|||||||
elixforms-www:
|
elixforms-www:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ../.docker/php/Dockerfile
|
dockerfile: .docker/php/Dockerfile
|
||||||
container_name: elixforms-www
|
container_name: elixforms-www
|
||||||
working_dir: /app
|
working_dir: /app/php
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
ports:
|
ports:
|
||||||
- "8080:8000"
|
- "8080:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- ../.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
|
- ./.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
|
||||||
#environment:
|
#environment:
|
||||||
# XDEBUG_MODE: debug
|
# XDEBUG_MODE: debug
|
||||||
# XDEBUG_CONFIG: client_host=host.docker.internal client_port=9003 start_with_request=yes
|
# XDEBUG_CONFIG: client_host=host.docker.internal client_port=9003 start_with_request=yes
|
||||||
+5
-3
@@ -14,7 +14,8 @@ In caso di conflitti di ID (ad esempio con input `hidden`), applicare il suffiss
|
|||||||
|
|
||||||
2. **Namespace PSR-4**:
|
2. **Namespace PSR-4**:
|
||||||
- Tutte le classi nella libreria comune usano il namespace `ElixForms\Common;`.
|
- Tutte le classi nella libreria comune usano il namespace `ElixForms\Common;`.
|
||||||
- Il caricamento è manuale tramite `require_once __DIR__ . '/../common/ElixFormsComponent.php';`.
|
- L'autoload PSR-4 è gestito da Composer tramite `vendor/autoload.php`.
|
||||||
|
- Gli entry point includono `require_once __DIR__ . '/../../vendor/autoload.php';`; le classi comuni non devono includersi manualmente tra loro.
|
||||||
|
|
||||||
3. **Rendering Server-Side**:
|
3. **Rendering Server-Side**:
|
||||||
- I file PHP generano direttamente l'HTML server-side usando il design system Bootstrap Italia di UniPR.
|
- I file PHP generano direttamente l'HTML server-side usando il design system Bootstrap Italia di UniPR.
|
||||||
@@ -24,12 +25,13 @@ In caso di conflitti di ID (ad esempio con input `hidden`), applicare il suffiss
|
|||||||
- I checkbox inviano il proprio stato serializzato come stringa separata da virgole tramite un input hidden (gestito client-side da uno script JS automatico).
|
- I checkbox inviano il proprio stato serializzato come stringa separata da virgole tramite un input hidden (gestito client-side da uno script JS automatico).
|
||||||
|
|
||||||
## Deploy e Server Locale
|
## Deploy e Server Locale
|
||||||
- **Deploy**: Le cartelle vengono caricate direttamente su web server Apache.
|
- **Dipendenze**: dalla root del repository eseguire `composer install --optimize-autoloader` oppure `composer-install.bat`.
|
||||||
|
- **Deploy**: Eseguire `composer install --no-dev --optimize-autoloader` e caricare sul server sia `php/` sia `vendor/`, mantenendole allo stesso livello.
|
||||||
- **Server locale**:
|
- **Server locale**:
|
||||||
- **PHP CLI**: Avviare il server dalla cartella `php/` tramite `php -S localhost:8080` (le pagine saranno raggiungibili su `http://localhost:8080/<nome-pagina>/`).
|
- **PHP CLI**: Avviare il server dalla cartella `php/` tramite `php -S localhost:8080` (le pagine saranno raggiungibili su `http://localhost:8080/<nome-pagina>/`).
|
||||||
- **Docker**: Dalla root del repository, eseguire:
|
- **Docker**: Dalla root del repository, eseguire:
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -p 8080:8080 -v "${PWD}/php:/app" -w /app php:8.2-cli php -S 0.0.0.0:8080
|
docker run --rm -p 8080:8080 -v "${PWD}:/app" -w /app/php php:8.2-cli php -S 0.0.0.0:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skill PHP Correlate
|
## Skill PHP Correlate
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ElixForms\Common;
|
namespace ElixForms\Common;
|
||||||
|
|
||||||
require_once __DIR__ . '/QueryParamHelper.php';
|
|
||||||
require_once __DIR__ . '/ElixFormsElement.php';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classe astratta base per le custom page PHP di elixForms.
|
* Classe astratta base per le custom page PHP di elixForms.
|
||||||
* Equivalente PHP di ElixFormsComponentAbstract.tsx nel progetto React.
|
* Equivalente PHP di ElixFormsComponentAbstract.tsx nel progetto React.
|
||||||
|
|||||||
+61
-75
@@ -4,24 +4,32 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ElixForms\Common;
|
namespace ElixForms\Common;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use GuzzleHttp\RequestOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client HTTP robusto per effettuare chiamate REST esterne.
|
* Client HTTP robusto per effettuare chiamate REST esterne.
|
||||||
* Utilizza l'estensione cURL di PHP ed implementa best practices per
|
* Adapter applicativo basato su Guzzle per la gestione uniforme di
|
||||||
* la gestione degli header, dell'autenticazione e degli errori.
|
* header, autenticazione, timeout ed errori delle chiamate esterne.
|
||||||
*/
|
*/
|
||||||
class HttpClient
|
class HttpClient
|
||||||
{
|
{
|
||||||
private array $defaultHeaders = [];
|
private ClientInterface $client;
|
||||||
|
private array $defaultHeaders;
|
||||||
private ?string $username = null;
|
private ?string $username = null;
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
private int $timeout = 10;
|
private int $timeout = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $defaultHeaders Header predefiniti per ogni richiesta
|
* @param array<string, string> $defaultHeaders Header predefiniti per ogni richiesta
|
||||||
*/
|
*/
|
||||||
public function __construct(array $defaultHeaders = [])
|
public function __construct(array $defaultHeaders = [], ?ClientInterface $client = null)
|
||||||
{
|
{
|
||||||
$this->defaultHeaders = $defaultHeaders;
|
$this->defaultHeaders = $defaultHeaders;
|
||||||
|
$this->client = $client ?? new Client();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +47,10 @@ class HttpClient
|
|||||||
*/
|
*/
|
||||||
public function setTimeout(int $seconds): self
|
public function setTimeout(int $seconds): self
|
||||||
{
|
{
|
||||||
|
if ($seconds <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Il timeout deve essere maggiore di zero.');
|
||||||
|
}
|
||||||
|
|
||||||
$this->timeout = $seconds;
|
$this->timeout = $seconds;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -47,19 +59,16 @@ class HttpClient
|
|||||||
* Esegue una richiesta HTTP GET.
|
* Esegue una richiesta HTTP GET.
|
||||||
*
|
*
|
||||||
* @param string $url URL della richiesta
|
* @param string $url URL della richiesta
|
||||||
* @param array $queryParams Parametri query aggiuntivi
|
* @param array<string, mixed> $queryParams Parametri query aggiuntivi
|
||||||
* @param array $headers Header specifici per questa richiesta
|
* @param array<string, string> $headers Header specifici per questa richiesta
|
||||||
* @return string Risposta in formato testuale
|
* @return string Risposta in formato testuale
|
||||||
* @throws \RuntimeException In caso di errore curl o codice di stato non 2xx
|
* @throws \RuntimeException In caso di errore HTTP o di trasporto
|
||||||
*/
|
*/
|
||||||
public function get(string $url, array $queryParams = [], array $headers = []): string
|
public function get(string $url, array $queryParams = [], array $headers = []): string
|
||||||
{
|
{
|
||||||
if (!empty($queryParams)) {
|
$options = $queryParams === [] ? [] : [RequestOptions::QUERY => $queryParams];
|
||||||
$separator = (strpos($url, '?') === false) ? '?' : '&';
|
|
||||||
$url .= $separator . http_build_query($queryParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->request($url, 'GET', null, $headers);
|
return $this->request($url, 'GET', $headers, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,87 +76,64 @@ class HttpClient
|
|||||||
*
|
*
|
||||||
* @param string $url URL della richiesta
|
* @param string $url URL della richiesta
|
||||||
* @param mixed $data Dati da inviare nel body (array, stringa o JSON)
|
* @param mixed $data Dati da inviare nel body (array, stringa o JSON)
|
||||||
* @param array $headers Header specifici per questa richiesta
|
* @param array<string, string> $headers Header specifici per questa richiesta
|
||||||
* @return string Risposta in formato testuale
|
* @return string Risposta in formato testuale
|
||||||
* @throws \RuntimeException In caso di errore curl o codice di stato non 2xx
|
* @throws \RuntimeException In caso di errore HTTP o di trasporto
|
||||||
*/
|
*/
|
||||||
public function post(string $url, $data, array $headers = []): string
|
public function post(string $url, $data, array $headers = []): string
|
||||||
{
|
{
|
||||||
return $this->request($url, 'POST', $data, $headers);
|
$options = is_array($data)
|
||||||
|
? [RequestOptions::FORM_PARAMS => $data]
|
||||||
|
: [RequestOptions::BODY => $data];
|
||||||
|
|
||||||
|
return $this->request($url, 'POST', $headers, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metodo interno per eseguire la richiesta tramite cURL.
|
* @param array<string, string> $headers
|
||||||
|
* @param array<string, mixed> $requestOptions
|
||||||
*/
|
*/
|
||||||
private function request(string $url, string $method, $data = null, array $headers = []): string
|
private function request(string $url, string $method, array $headers, array $requestOptions = []): string
|
||||||
{
|
{
|
||||||
$ch = curl_init();
|
$options = [
|
||||||
if ($ch === false) {
|
RequestOptions::HEADERS => array_merge($this->defaultHeaders, $headers),
|
||||||
throw new \RuntimeException('Impossibile inizializzare cURL.');
|
RequestOptions::TIMEOUT => $this->timeout,
|
||||||
}
|
RequestOptions::CONNECT_TIMEOUT => $this->timeout,
|
||||||
|
RequestOptions::ALLOW_REDIRECTS => ['max' => 3],
|
||||||
|
RequestOptions::VERIFY => true,
|
||||||
|
RequestOptions::HTTP_ERRORS => true,
|
||||||
|
];
|
||||||
|
|
||||||
// Configura le opzioni cURL di base
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
|
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
|
|
||||||
|
|
||||||
// Disabilita la verifica SSL in ambiente di sviluppo locale se necessario,
|
|
||||||
// ma di default è attiva per sicurezza.
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
|
||||||
|
|
||||||
// Gestione metodo HTTP
|
|
||||||
$method = strtoupper($method);
|
|
||||||
if ($method === 'POST') {
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
if ($data !== null) {
|
|
||||||
if (is_array($data)) {
|
|
||||||
$postData = http_build_query($data);
|
|
||||||
} else {
|
|
||||||
$postData = $data;
|
|
||||||
}
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
|
||||||
}
|
|
||||||
} elseif ($method !== 'GET') {
|
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Costruisci gli header
|
|
||||||
$mergedHeaders = array_merge($this->defaultHeaders, $headers);
|
|
||||||
$formattedHeaders = [];
|
|
||||||
foreach ($mergedHeaders as $name => $value) {
|
|
||||||
$formattedHeaders[] = "{$name}: {$value}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($formattedHeaders)) {
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Autenticazione Basic
|
|
||||||
if ($this->username !== null && $this->password !== null) {
|
if ($this->username !== null && $this->password !== null) {
|
||||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
$options[RequestOptions::AUTH] = [$this->username, $this->password];
|
||||||
curl_setopt($ch, CURLOPT_USERPWD, "{$this->username}:{$this->password}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Esegui la richiesta
|
try {
|
||||||
$response = curl_exec($ch);
|
$response = $this->client->request(
|
||||||
$error = curl_error($ch);
|
strtoupper($method),
|
||||||
$errno = curl_errno($ch);
|
$url,
|
||||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
array_replace($options, $requestOptions)
|
||||||
|
);
|
||||||
|
} catch (RequestException $exception) {
|
||||||
|
$response = $exception->getResponse();
|
||||||
|
if ($response !== null) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
"Richiesta fallita con codice di stato HTTP {$response->getStatusCode()}.",
|
||||||
|
0,
|
||||||
|
$exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// curl_close non è più necessario in PHP 8.0+ ed è deprecato in PHP 8.5+
|
throw new \RuntimeException('Errore di connessione al servizio esterno.', 0, $exception);
|
||||||
|
} catch (GuzzleException $exception) {
|
||||||
if ($errno !== 0) {
|
throw new \RuntimeException('Errore durante la chiamata al servizio esterno.', 0, $exception);
|
||||||
throw new \RuntimeException("Errore cURL durante la chiamata a {$url}: [{$errno}] {$error}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
if ($statusCode < 200 || $statusCode >= 300) {
|
if ($statusCode < 200 || $statusCode >= 300) {
|
||||||
throw new \RuntimeException("Richiesta fallita con codice di stato HTTP {$statusCode}.");
|
throw new \RuntimeException("Richiesta fallita con codice di stato HTTP {$statusCode}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string)$response;
|
return (string)$response->getBody();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../common/QueryParamHelper.php';
|
|
||||||
|
|
||||||
use ElixForms\Common\ElixFormsComponent;
|
use ElixForms\Common\ElixFormsComponent;
|
||||||
use ElixForms\Common\QueryParamHelper;
|
use ElixForms\Common\QueryParamHelper;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
require_once __DIR__ . '/../common/HttpClient.php';
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
use ElixForms\Common\HttpClient;
|
use ElixForms\Common\HttpClient;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
use ElixForms\Common\ElixFormsComponent;
|
use ElixForms\Common\ElixFormsComponent;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command dump-autoload -Arguments @('--optimize')
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command install -Arguments @('--optimize-autoloader')
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$Command,
|
||||||
|
[string[]]$Arguments = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$root = (Resolve-Path (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..')).Path
|
||||||
|
|
||||||
|
$useDockerComposer = $false
|
||||||
|
$dockerRoot = $null
|
||||||
|
|
||||||
|
if (-not (Get-Command composer -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Host "Composer non trovato localmente. Verifico se Docker è disponibile..."
|
||||||
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Composer non trovato e Docker mancante! Installa Composer o Docker prima di continuare."
|
||||||
|
}
|
||||||
|
|
||||||
|
$dockerRoot = $root -replace '\\', '/'
|
||||||
|
if ($dockerRoot -match '^([A-Za-z]):') {
|
||||||
|
$dockerRoot = "/$($Matches[1].ToLower())$($dockerRoot.Substring(2))"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Docker trovato. Verifico Composer tramite container..."
|
||||||
|
docker run --rm -v "$($dockerRoot):/app" -w /app composer:2 composer --version 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker presente, ma Composer non riesce ad avviarsi tramite container. Installa Composer localmente o verifica Docker."
|
||||||
|
}
|
||||||
|
|
||||||
|
$useDockerComposer = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $root
|
||||||
|
try {
|
||||||
|
if ($useDockerComposer) {
|
||||||
|
docker run --rm -v "$($dockerRoot):/app" -w /app composer:2 composer $Command @Arguments
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
composer $Command @Arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Composer ha restituito il codice di uscita $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use ElixForms\Common\HttpClient;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Handler\MockHandler;
|
||||||
|
use GuzzleHttp\HandlerStack;
|
||||||
|
use GuzzleHttp\Middleware;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class HttpClientTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetUsesQueryHeadersAuthenticationAndTimeout(): void
|
||||||
|
{
|
||||||
|
$history = [];
|
||||||
|
$stack = HandlerStack::create(new MockHandler([new Response(200, [], '{"ok":true}')]));
|
||||||
|
$stack->push(Middleware::history($history));
|
||||||
|
|
||||||
|
$client = new HttpClient(
|
||||||
|
['Accept' => 'application/json', 'X-Default' => 'default'],
|
||||||
|
new Client(['handler' => $stack])
|
||||||
|
);
|
||||||
|
$client->setBasicAuth('user', 'secret')->setTimeout(5);
|
||||||
|
|
||||||
|
$result = $client->get(
|
||||||
|
'https://example.test/resource',
|
||||||
|
['term' => 'Mario Rossi'],
|
||||||
|
['X-Default' => 'override']
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame('{"ok":true}', $result);
|
||||||
|
self::assertCount(1, $history);
|
||||||
|
self::assertSame('GET', $history[0]['request']->getMethod());
|
||||||
|
self::assertSame('term=Mario%20Rossi', $history[0]['request']->getUri()->getQuery());
|
||||||
|
self::assertSame('application/json', $history[0]['request']->getHeaderLine('Accept'));
|
||||||
|
self::assertSame('override', $history[0]['request']->getHeaderLine('X-Default'));
|
||||||
|
self::assertSame(['user', 'secret'], $history[0]['options']['auth']);
|
||||||
|
self::assertSame(5, $history[0]['options']['timeout']);
|
||||||
|
self::assertSame(5, $history[0]['options']['connect_timeout']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostEncodesArrayAsFormData(): void
|
||||||
|
{
|
||||||
|
$history = [];
|
||||||
|
$stack = HandlerStack::create(new MockHandler([new Response(200, [], 'saved')]));
|
||||||
|
$stack->push(Middleware::history($history));
|
||||||
|
$client = new HttpClient([], new Client(['handler' => $stack]));
|
||||||
|
|
||||||
|
self::assertSame('saved', $client->post('https://example.test/resource', ['name' => 'Mario Rossi']));
|
||||||
|
self::assertSame('POST', $history[0]['request']->getMethod());
|
||||||
|
self::assertSame('name=Mario+Rossi', (string)$history[0]['request']->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpErrorsAreExposedWithoutResponseBody(): void
|
||||||
|
{
|
||||||
|
$client = new HttpClient([], new Client([
|
||||||
|
'handler' => HandlerStack::create(new MockHandler([
|
||||||
|
new Response(503, [], 'sensitive upstream response'),
|
||||||
|
])),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Richiesta fallita con codice di stato HTTP 503.');
|
||||||
|
|
||||||
|
$client->get('https://example.test/resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTimeoutMustBePositive(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
(new HttpClient())->setTimeout(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user