4 Commits
21 changed files with 2757 additions and 105 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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"]
+2 -1
View File
@@ -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.log_level=7
xdebug.idekey=VSCODE xdebug.idekey=VSCODE
+2
View File
@@ -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
+1
View File
@@ -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"
+10
View File
@@ -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
+10
View File
@@ -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
+25
View File
@@ -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
View File
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
View File
@@ -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
-3
View File
@@ -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.
+62 -76
View File
@@ -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)
);
// curl_close non è più necessario in PHP 8.0+ ed è deprecato in PHP 8.5+ } catch (RequestException $exception) {
$response = $exception->getResponse();
if ($errno !== 0) { if ($response !== null) {
throw new \RuntimeException("Errore cURL durante la chiamata a {$url}: [{$errno}] {$error}"); throw new \RuntimeException(
"Richiesta fallita con codice di stato HTTP {$response->getStatusCode()}.",
0,
$exception
);
} }
throw new \RuntimeException('Errore di connessione al servizio esterno.', 0, $exception);
} catch (GuzzleException $exception) {
throw new \RuntimeException('Errore durante la chiamata al servizio esterno.', 0, $exception);
}
$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;
+1 -1
View File
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command dump-autoload -Arguments @('--optimize')
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command install -Arguments @('--optimize-autoloader')
+47
View File
@@ -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
}
+77
View File
@@ -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);
}
}