Guzzle, PSR-7 and PSR-18

Guzzle 23,452 (https://github.com/guzzle/guzzle 23,452 ) wraps cURL. composer require guzzlehttp/guzzle installed 8.2.0 (6 September 2026). The 7.x line is still patched (7.15.5).

Guzzle: a base URI, a 4xx exception, and the PSR-18 interfacePHP
<?php
require 'vendor/autoload.php';
use GuzzleHttp\{Client, Exception\ClientException, Psr7\HttpFactory};
$api = new Client(['base_uri' => 'http://127.0.0.1:8218', 'timeout' => 2]);
echo json_decode($api->get('/products/2')->getBody())->title, "\n";
try {
  $api->post('/orders', ['json' => ['product_id' => 2, 'qty' => 3]]);     // no token
} catch (ClientException $e) {
  echo $e::class, ' ', $e->getResponse()->getStatusCode(), "\n";
}
$request = (new HttpFactory())->createRequest('GET', '/products/9');    // PSR-17 factory
echo $api->sendRequest($request)->getStatusCode(), "\n";                // PSR-18: no throw
Output
SQL Queries That Scale
GuzzleHttp\Exception\ClientException 401
404

The json option encodes the body and sets the header. Guzzle's methods throw on 4xx and 5xx, while PSR-18's sendRequest() throws only on network errors. getAsync() returns a promise, and Utils::unwrap() ran two /slow calls in 0.80 s. PSR-7 defines messages and PSR-18 the client. Code that depends on Psr\Http\Client\ClientInterface can switch to Symfony 6,093 HttpClient or a mock.