> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Client HTTP e test nei pacchetti

> Progettare client API con la Factory HTTP condivisa di Laravel 13, isolando lo stato a ogni richiesta, registrando i servizi e verificandoli con Http::fake().

Quando un pacchetto deve usare un'API esterna, occorre conciliare due esigenze: **ereditare la configurazione del client HTTP gestito da Laravel** e **non trascinare la configurazione da una chiamata all'altra**. Creare una Factory propria o conservare a lungo una richiesta non ancora inviata sono cause tipiche per cui i fake definiti dall'applicazione che usa il pacchetto non vengono applicati.

Questa pagina esamina l'implementazione di Laravel Framework `v13.39.0`. Per le operazioni di base consulta [Client HTTP](https://kawax.biz/it/http-client.md), per la preparazione dell'ambiente di test consulta [Testare i pacchetti](https://kawax.biz/it/advanced/package-testing.md).

## Condividere la Factory e creare ogni volta una PendingRequest

Il client HTTP di Laravel è composto da oggetti con responsabilità diverse.

| Oggetto | Ruolo | Uso nel pacchetto |
| - | - | - |
| `Illuminate\Http\Client\Factory` | Gestisce fake, opzioni comuni, middleware comuni e così via | Riceverla dal container di Laravel |
| `Illuminate\Http\Client\PendingRequest` | Contiene la configurazione della richiesta da inviare, come destinazione, header e timeout | Crearla a ogni chiamata di un metodo dell'API |
| `Illuminate\Http\Client\Response` | Ispeziona lo status e il corpo | Trattarla come risultato della comunicazione e, se necessario, convertirla in un'eccezione o in un valore di ritorno |

`FoundationServiceProvider` registra la Factory come singleton, e anche il facade `Http` la risolve dal container usando lo stesso nome di classe. Per questo, in una normale applicazione Laravel, **anche le comunicazioni effettuate tramite la Factory ricevuta dal container possono essere verificate con `Http::fake()`**. È possibile anche usare direttamente il facade, ma in questa pagina iniettiamo la Factory per rendere esplicite le dipendenze.

I normali metodi di configurazione della richiesta chiamati sulla Factory, come `baseUrl()`, vengono inoltrati da `__call()` a una nuova PendingRequest. `createPendingRequest()` passa le callback dei fake e il blocco delle stray request in vigore al momento della creazione, mentre `newPendingRequest()` applica i middleware comuni e le opzioni comuni.

```mermaid theme={null}
flowchart LR
    A["Container di Laravel"] --> B["Factory condivisa"]
    C["Risposte definite con Http::fake()"] --> B
    B --> D["Nuova PendingRequest<br>a ogni chiamata API"]
    D --> E["Configurazione specifica della chiamata"]
    E --> F["Invio e ispezione della risposta"]
```

> [!WARNING]
>
> Se all'interno del pacchetto crei `new Factory()` o il client di un'altra libreria HTTP, i fake della Factory condivisa e `Http::preventStrayRequests()` non vengono ereditati. Queste funzionalità non bloccano le comunicazioni dell'intero processo. Nei client pensati per Laravel usa la Factory condivisa; se fornisci un client indipendente, prepara un metodo di test dedicato a quel client.

## Raccogliere le operazioni dell'API in un piccolo client

Prendiamo come esempio il pacchetto `Acme\Catalog`, che recupera prodotti da un'API di catalogo fittizia. `src/CatalogClient.php` conserva solo la Factory e l'URL di base, e la richiesta viene creata durante l'esecuzione di `product()`.

```php theme={null}
<?php

namespace Acme\Catalog;

use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\PendingRequest;
use UnexpectedValueException;

final class CatalogClient
{
    public function __construct(
        private readonly Factory $http,
        private readonly string $baseUrl,
    ) {
    }

    public function product(int $id, ?string $language = null): array
    {
        $request = $this->request();

        if ($language !== null) {
            $request->withHeaders(['Accept-Language' => $language]);
        }

        $data = $request->get('/products/'.$id)
            ->throw()
            ->json(flags: JSON_THROW_ON_ERROR);

        if (! is_array($data)) {
            throw new UnexpectedValueException('Catalog response must be an array.');
        }

        return $data;
    }

    private function request(): PendingRequest
    {
        return $this->http->baseUrl($this->baseUrl)
            ->acceptJson()
            ->connectTimeout(3)
            ->timeout(10);
    }
}
```

Con questo design, anche se sullo stesso client chiami `product(42)` dopo `product(42, 'ja')`, l'header `Accept-Language` aggiunto nella chiamata precedente non rimane. La configurazione comune che l'applicazione ha impostato sulla Factory continua invece a essere applicata a ogni richiesta.

* Applica la configurazione specifica del pacchetto alla PendingRequest appena creata. Evita di impostare `Http::globalOptions()` durante l'inizializzazione del pacchetto, perché influenzerebbe anche comunicazioni non correlate.
* Non inviare nulla nel costruttore. Progetta il pacchetto in modo che la sola risoluzione del servizio, l'avvio di Artisan o la pubblicazione dei file di configurazione non si colleghino all'API esterna.
* `throw()` trasforma le risposte 4xx e 5xx in `RequestException`. Distinguile dalle `ConnectionException` dovute a errori di connessione o timeout, e non sostituire un fallimento con un valore di successo come un array vuoto.
* `json(flags: JSON_THROW_ON_ERROR)` trasforma un JSON non valido in `JsonException`. L'esempio verifica soltanto che il risultato sia un array, quindi con un'API reale verifica anche i campi obbligatori e i tipi di dato.

> [!TIP]
>
> Se ti serve solo condividere URL di base o header, anche le macro HTTP della documentazione ufficiale sono un'opzione. Se invece il pacchetto fornisce le operazioni dell'API, il controllo dei valori di ritorno e il contratto delle eccezioni, separarli in una classe dedicata come questa rende la manutenzione più semplice. Per il funzionamento delle macro consulta [Il trait Macroable](https://kawax.biz/it/advanced/macroable.md).

## Registrare il client nel service provider

Prepara la configurazione iniziale in `config/catalog.php` del pacchetto. Non salvarci client HTTP o closure, ma solo valori che possono essere memorizzati nella cache della configurazione.

```php theme={null}
<?php

return [
    'base_url' => 'https://catalog.example.test/api',
];
```

`src/CatalogServiceProvider.php` unisce la configurazione e passa la Factory condivisa al momento della risoluzione del client. Il binding della Factory stessa non viene sovrascritto.

```php theme={null}
<?php

namespace Acme\Catalog;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\ServiceProvider;

class CatalogServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/catalog.php', 'acme-catalog');

        $this->app->bind(CatalogClient::class, function (Application $app): CatalogClient {
            return new CatalogClient(
                $app->make(Factory::class),
                $app['config']->get('acme-catalog.base_url'),
            );
        });
    }

    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../config/catalog.php' => config_path('acme-catalog.php'),
        ], 'acme-catalog-config');
    }
}
```

Il provider viene caricato, ad esempio, tramite la [package discovery](https://kawax.biz/it/advanced/package-discovery.md). Chi usa il pacchetto pubblica la configurazione e modifica l'URL di base in `config/acme-catalog.php` impostando l'API reale. L'host `catalog.example.test` dell'esempio è fittizio e serve per la spiegazione e per i fake.

```shell theme={null}
php artisan vendor:publish --tag=acme-catalog-config
```

Nell'applicazione il client si risolve tramite constructor injection o dal container. Solo con questa chiamata viene inviata la richiesta per recuperare il prodotto.

```php theme={null}
use Acme\Catalog\CatalogClient;

$product = app(CatalogClient::class)->product(42, 'ja');
```

La registrazione usa `bind()`, ma se l'oggetto che conserva il client è un singleton, quel riferimento non viene ricreato ogni volta. L'importante è non salvare nelle proprietà la lingua, le credenziali o gli oggetti richiesta specifici di una chiamata. Per i dettagli consulta [Progettare il ciclo di vita dei servizi di un pacchetto](https://kawax.biz/it/advanced/package-service-lifetimes.md).

La sovrascrittura della configurazione e l'aggiornamento della cache durante il deploy sono trattati in [Unione e cache della configurazione dei pacchetti](https://kawax.biz/it/advanced/package-config-merging.md). Gestisci l'URL di base come configurazione affidabile dell'applicazione e non permettere che la destinazione venga scelta liberamente dall'input della richiesta.

## Verificare il client reale con i fake

Se fai il mock del client stesso, non puoi verificare se usa la Factory condivisa né se costruisce correttamente URL e header. Nei test del pacchetto, **risolvi dal container il vero `CatalogClient` e rendi fake solo le risposte HTTP**.

L'esempio seguente viene eseguito nell'[ambiente di test di Orchestra Testbench](https://kawax.biz/it/advanced/package-testing.md). `preventStrayRequests()` viene attivato nel `setUp()` di ogni test. Nei test che comunicano, definendo risposte solo per gli URL previsti e senza un fallback `'*'`, puoi rilevare anche errori negli URL.

```php theme={null}
<?php

namespace Acme\Catalog\Tests;

use Acme\Catalog\CatalogClient;
use Acme\Catalog\CatalogServiceProvider;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\StrayRequestException;
use Illuminate\Support\Facades\Http;
use Orchestra\Testbench\TestCase;

class CatalogClientTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [CatalogServiceProvider::class];
    }

    protected function setUp(): void
    {
        parent::setUp();

        Http::preventStrayRequests();
    }

    public function test_resolving_the_client_does_not_send_a_request(): void
    {
        Http::fake();

        $this->assertInstanceOf(CatalogClient::class, $this->app->make(CatalogClient::class));

        Http::assertNothingSent();
    }

    public function test_product_request_uses_the_shared_factory(): void
    {
        $client = $this->app->make(CatalogClient::class);

        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response([
                'id' => 42,
                'name' => 'Notebook',
            ]),
        ]);

        $this->assertSame(['id' => 42, 'name' => 'Notebook'], $client->product(42, 'ja'));

        Http::assertSent(static fn (Request $request): bool =>
            $request->method() === 'GET'
            && $request->url() === 'https://catalog.example.test/api/products/42'
            && $request->hasHeader('Accept', 'application/json')
            && $request->hasHeader('Accept-Language', 'ja')
        );
        Http::assertSentCount(1);
    }

    public function test_language_is_not_carried_over_to_the_next_request(): void
    {
        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response(['id' => 42]),
        ]);

        $client = $this->app->make(CatalogClient::class);
        $client->product(42, 'ja');
        $client->product(42);

        Http::assertSentInOrder([
            static fn (Request $request): bool => $request->hasHeader('Accept-Language', 'ja'),
            static fn (Request $request): bool => ! $request->hasHeader('Accept-Language'),
        ]);
    }

    public function test_server_errors_are_not_returned_as_product_data(): void
    {
        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response(['error' => 'unavailable'], 503),
        ]);

        $this->expectException(RequestException::class);

        $this->app->make(CatalogClient::class)->product(42);
    }

    public function test_a_request_without_a_matching_fake_is_rejected(): void
    {
        Http::fake([]);

        $this->expectException(StrayRequestException::class);

        $this->app->make(CatalogClient::class)->product(42);
    }
}
```

Nel secondo test, **il fake viene configurato dopo aver risolto il client**. Poiché il client conserva la Factory e crea la PendingRequest al momento della chiamata effettiva, il fake viene applicato anche con questo ordine. Se modifichi il design per creare e conservare la PendingRequest nel costruttore, il test rileva che i fake aggiunti dopo la creazione non vengono applicati.

`assertSent()` verifica che esista almeno una richiesta corrispondente. Per non lasciarti sfuggire invii aggiuntivi involontari, combinalo con `assertSentCount()`. `assertSentInOrder()` verifica numero e ordine delle richieste, quindi è utile anche per controllare l'isolamento dello stato quando si usa lo stesso client più volte di seguito.

La guardia viene configurata dopo `parent::setUp()`. Questo test da solo non può verificare le comunicazioni durante l'avvio del provider, quindi mantieni un design in cui registrazione e avvio non comunicano.

> [!NOTE]
>
> Con i fake puoi verificare la richiesta costruita dal client e il modo in cui gestisce le risposte preparate. Non garantiscono l'autenticazione del servizio reale, la connessione TLS, i tempi di timeout effettivi né le modifiche alle specifiche dell'API esterna. Esegui separatamente le verifiche che richiedono connessioni esterne, in un ambiente di test preparato esplicitamente.

## Verifiche da aggiungere per la manutenzione a lungo termine

| Modifica o caso anomalo | Cosa verificare |
| - | - |
| Sovrascrittura dell'URL di base | La configurazione dell'applicazione viene applicata e la richiesta viene inviata al percorso previsto |
| Errore di connessione | Con `Http::failedConnection()` la `ConnectionException` arriva al codice che usa il pacchetto |
| JSON non valido o tipo inatteso | Il JSON non valido produce `JsonException`, un valore che non è un array produce la `UnexpectedValueException` dell'esempio |
| Aggiunta dei retry | Verifica numero di tentativi, status interessati e contenuto inviato, e per le API con effetti collaterali considera le esecuzioni duplicate |
| Aggiunta di autenticazione o lingua | Anche usando lo stesso client più volte, i valori della chiamata precedente non vengono trascinati |
| Aggiornamento di Laravel | Nelle versioni supportate continua a verificare il percorso di risoluzione della Factory, l'applicazione dei fake e il contratto delle eccezioni |

Separando le responsabilità della Factory condivisa da quelle di ciascuna PendingRequest, chi usa il pacchetto può adottare così come sono i normali metodi di test di Laravel. Prima di costruire un meccanismo di fake proprio, prepara un'implementazione che passi dalla Factory condivisa e i test che lo verificano.

## Fonti primarie consultate

* [Documentazione ufficiale di Laravel: client HTTP](https://github.com/laravel/docs/blob/13.x/http-client.md)
* [Documentazione ufficiale di Laravel: sviluppo di pacchetti](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Framework v13.39.0: fake, createPendingRequest e \_\_call della Factory](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/Factory.php)
* [Framework v13.39.0: configurazione e gestione degli stub di PendingRequest](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/PendingRequest.php)
* [Framework v13.39.0: throw e decodifica JSON di Response](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/Response.php)
* [Framework v13.39.0: destinazione di risoluzione e fake del facade Http](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Facades/Http.php)
* [Framework v13.39.0: registrazione singleton in FoundationServiceProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php)
* [Test ufficiali di Laravel: HttpClientTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Http/HttpClientTest.php)


## Related topics

- [HTTP Client](https://kawax.biz/it/http-client.md)
- [Argomenti avanzati](https://kawax.biz/it/advanced/index.md)
- [Guida allo sviluppo di app con l'API del motore - VOICEVOX for Laravel](https://kawax.biz/it/packages/laravel-voicevox/app-guide.md)
- [Tag del container e aggregazione di plugin nei pacchetti](https://kawax.biz/it/advanced/package-container-tags.md)
- [Test HTTP](https://kawax.biz/it/http-tests.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.