http clients and apis
Guzzle Orientation
Guzzle is a widely used PHP HTTP client. It wraps lower-level transport details behind a consistent API for sending requests, reading responses, handling errors, adding middleware, and testing integrations.
This lesson is an orientation. You should understand the shape of Guzzle code even if the package is not installed in this training project.
Why teams use Guzzle
Compared with raw streams or direct cURL, Guzzle gives you:
- clear request option arrays
- response objects with status, headers, and body
- consistent exceptions
- middleware for logging, retries, and signing
- test helpers such as mock handlers
- PSR-7 request and response support
Request option shape
A Guzzle request usually starts with a method, URL or path, and an options array.
<?php
declare(strict_types=1);
$request = [
'method' => 'GET',
'uri' => '/products',
'options' => [
'base_uri' => 'https://api.example.test',
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
],
];
echo $request['method'] . ' ' . $request['options']['base_uri'] . $request['uri'] . PHP_EOL;
// Prints:
// GET https://api.example.test/products
In real Guzzle code, a client may be created with base_uri, then each request uses a relative URI.
JSON requests
Guzzle has a json option. It JSON-encodes the payload and sets the appropriate content type.
<?php
declare(strict_types=1);
$options = [
'json' => [
'name' => 'Keyboard',
'price' => 99.99,
],
'headers' => [
'Accept' => 'application/json',
],
'timeout' => 5,
];
echo json_encode($options['json'], JSON_THROW_ON_ERROR) . PHP_EOL;
// Prints:
// {"name":"Keyboard","price":99.99}
Do not pass both json and a manually encoded body for the same request unless you have a very specific reason. Choose one request-body strategy.
Query parameters
Guzzle also has a query option for query strings.
<?php
declare(strict_types=1);
$options = [
'query' => [
'search' => 'PHP & APIs',
'page' => 2,
],
];
echo http_build_query($options['query']) . PHP_EOL;
// Prints:
// search=PHP+%26+APIs&page=2
This keeps encoding separate from the path and avoids hand-built URLs.
Responses and exceptions
Guzzle responses expose status code, headers, and body. By default, Guzzle throws exceptions for many 4xx and 5xx responses unless http_errors is disabled.
You need to decide which style your client uses:
- catch exceptions and translate them into application errors
- disable
http_errorsand inspect status codes yourself
<?php
declare(strict_types=1);
function responseOutcome(int $statusCode): string
{
return $statusCode >= 200 && $statusCode < 300
? 'success'
: 'handle status ' . $statusCode;
}
echo responseOutcome(422) . PHP_EOL;
// Prints:
// handle status 422
The important point is consistency. Do not let every caller handle API failures differently.
A small API client wrapper
Teams often wrap Guzzle in a focused client class. The rest of the application calls methods such as findProduct() rather than building HTTP requests everywhere.
<?php
declare(strict_types=1);
final class ProductApiRequest
{
/**
* @return array{method: string, uri: string, options: array<string, mixed>}
*/
public function findProduct(int $id): array
{
return [
'method' => 'GET',
'uri' => '/products/' . $id,
'options' => [
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
],
];
}
}
$request = (new ProductApiRequest())->findProduct(42);
echo $request['method'] . ' ' . $request['uri'] . PHP_EOL;
// Prints:
// GET /products/42
In production, that wrapper would hold a real GuzzleHttp\ClientInterface and return parsed application data.
Middleware and testing
Guzzle middleware can add behaviour around requests, such as logging, retry decisions, request IDs, or authentication signatures.
For tests, Guzzle's mock handler pattern lets code receive prepared responses without making network calls. That is important because unit and integration tests should not depend on a third-party API being online.
What to check in a project
Check client configuration for base_uri, timeouts, and default headers.
Check whether http_errors is enabled or disabled and how failures are handled.
Check that credentials are injected from configuration and not hard-coded.
Check that repeated request logic is wrapped in a focused API client instead of duplicated across controllers.
Check that tests use mocks, fakes, or recorded fixtures rather than real external requests for normal test runs.
What you should be able to do
After this lesson, you should be able to recognise Guzzle request options, understand json and query, reason about response handling, and see why teams wrap Guzzle in small API client classes.
Practice
Task: Shape A Guzzle Request
Write a small PHP script that builds request option arrays in the style you would pass to Guzzle.
Requirements
- Use
declare(strict_types=1);. - Create a function for a
GET /productsrequest with query parameters. - Create a function for a
POST /productsrequest with a JSON payload. - Include
Accept: application/json. - Include a timeout.
- Print the method, URI, and encoded query or body for both requests.
Check Your Work
Run the script and confirm query data and JSON body data are kept in different option keys.
Show solution
This solution prepares arrays that mirror Guzzle request options without requiring Guzzle to be installed.
<?php
declare(strict_types=1);
/**
* @return array{method: string, uri: string, options: array<string, mixed>}
*/
function listProductsRequest(string $search, int $page): array
{
return [
'method' => 'GET',
'uri' => '/products',
'options' => [
'query' => ['search' => $search, 'page' => $page],
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
],
];
}
/**
* @return array{method: string, uri: string, options: array<string, mixed>}
*/
function createProductRequest(string $name, float $price): array
{
return [
'method' => 'POST',
'uri' => '/products',
'options' => [
'json' => ['name' => $name, 'price' => $price],
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
],
];
}
$list = listProductsRequest('PHP & APIs', 2);
$create = createProductRequest('Keyboard', 99.99);
echo $list['method'] . ' ' . $list['uri'] . '?' . http_build_query($list['options']['query']) . PHP_EOL;
echo $create['method'] . ' ' . $create['uri'] . ' ' . json_encode($create['options']['json'], JSON_THROW_ON_ERROR) . PHP_EOL;
// Prints:
// GET /products?search=PHP+%26+APIs&page=2
// POST /products {"name":"Keyboard","price":99.99}
Why This Works
The GET request keeps query parameters in query. The POST request keeps JSON payload data in json. Both include headers and timeout options that a real API client would need.