Search by

christianjbrown / api-client

christianjbrown

A thin, strongly-typed PHP 8.5+ client for JSON and XML APIs that wraps GuzzleHttp and normalizes its exceptions into a single, framework-agnostic hierarchy.

Package info

github.com/christianjbrown/api-client-php

pkg:composer/christianjbrown/api-client

Statistics

Installs: 420

Dependents: 6

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.1 2026-10-01 11:29 UTC

README

CI Coverage Packagist License PHP

This library provides a simple request client for JSON and XML APIs. It is a wrapper around GuzzleHttp's Client class that decodes responses — JSON to an array, XML to a DOMDocument — and provides common non-Guzzle specific exception classes to make it easier to catch and handle.

✔️ Prerequisites

💡 If you're on MacOS and have Homebrew, PHP and Composer will install with brew install composer.

🏗️ Installation

For your composer-enabled project:

composer require christianjbrown/api-client

💻 Usage

Setup

use ChristianBrown\ApiClient\ApiClientFactory;
use ChristianBrown\ApiClient\ClientOptions;

$apiClient = (new ApiClientFactory(new ClientOptions()))->create();
$jsonApiRequestSender = $apiClient->getJsonApiRequestSender();

// or, for XML use
// $xmlApiRequestSender = $apiClient->getXmlApiRequestSender();

Timeouts

A request gives up after 30 seconds, or after 10 seconds if it cannot connect. To change either, pass your own ClientOptions. Use 0 to wait indefinitely.

use ChristianBrown\ApiClient\ApiClientFactory;
use ChristianBrown\ApiClient\ClientOptions;

$apiClient = (new ApiClientFactory(new ClientOptions(timeout: 60.0, connectTimeout: 5.0)))->create();

A timeout surfaces as a ConnectException, because that is how Guzzle reports it.

POST examples

If you need to POST an API endpoint, use post like -

use ChristianBrown\ApiClient\Exception\ExceptionInterface;

try {
    $data = $jsonApiRequestSender->post('url', ['query-string-1-key' => 'query-string-1-value'], [], ['body-key-1' => 'body-value-1']);
} catch (ExceptionInterface $e) {
    print $e->getMessage();
}

GET example

If you need to GET data from an API endpoint, use get like -

use ChristianBrown\ApiClient\Exception\ExceptionInterface;

try {
    $data = $jsonApiRequestSender->get('url', ['query-string-1-key' => 'query-string-1-value'], []);
} catch (ExceptionInterface $e) {
    print $e->getMessage();
}

PUT, PATCH and DELETE

The remaining verbs follow the same shape. put and patch take a body array and send it as JSON; putForm and patchForm send it as application/x-www-form-urlencoded; delete takes no body.

use ChristianBrown\ApiClient\Exception\ExceptionInterface;

try {
    $data = $jsonApiRequestSender->put('url', [], [], ['body-key-1' => 'body-value-1']);
    $data = $jsonApiRequestSender->patch('url', [], [], ['body-key-1' => 'body-value-1']);
    $data = $jsonApiRequestSender->putForm('url', [], [], ['body-key-1' => 'body-value-1']);
    $data = $jsonApiRequestSender->patchForm('url', [], [], ['body-key-1' => 'body-value-1']);
    $data = $jsonApiRequestSender->delete('url');
} catch (ExceptionInterface $e) {
    print $e->getMessage();
}

An endpoint that answers 204 No Content, or any other empty body, comes back as an empty array.

Multipart uploads

postMultipart, putMultipart and patchMultipart send a multipart/form-data body built from a list of MultipartParts. A part with a filename is sent as a file; without one, it is a plain field. The Content-Type and its boundary are set for you, and replace any Content-Type you pass.

use ChristianBrown\ApiClient\Multipart\MultipartPart;

$data = $jsonApiRequestSender->postMultipart('url', [], [], [
    new MultipartPart('description', 'Photo of the damage'),
    new MultipartPart('file', file_get_contents('photo.png'), 'photo.png', ['Content-Type' => 'image/png']),
]);

⬆️ Upgrading to 3.0

ApiClient no longer builds anything itself. Its constructor takes the three senders, and ApiClientFactory builds the default wiring.

// before
$apiClient = new ApiClient();
$apiClient = new ApiClient(new ApiClientContainerFactory(new ClientOptions(timeout: 60.0)));

// after
$apiClient = (new ApiClientFactory(new ClientOptions()))->create();
$apiClient = (new ApiClientFactory(new ClientOptions(timeout: 60.0)))->create();

ApiRequestSender takes a PSR-17 RequestFactoryInterface and StreamFactoryInterface as its fourth and fifth constructor arguments (Guzzle's HttpFactory implements both). Code that builds it by hand passes them:

new ApiRequestSender($guzzle, $exceptionRedactor, $multipartBodyFactory, new HttpFactory(), new HttpFactory());

ApiRequestSenderInterface and JsonApiRequestSenderInterface are now composed of narrower interfaces: Read, Write, Form and Multipart, with a Json prefix on the JSON ones, for example JsonReadApiRequestSenderInterface. Type-hint the narrow one when a class only needs get and delete, or only the JSON post.

🚨 Error handling

The main value-add of this library is that it catches Guzzle's transport-specific exceptions and re-throws framework-agnostic ones. Every exception this library throws implements ChristianBrown\ApiClient\Exception\ExceptionInterface (which extends Throwable), so a single catch handles all of them:

use ChristianBrown\ApiClient\Exception\ExceptionInterface;

try {
    $data = $jsonApiRequestSender->get('url');
} catch (ExceptionInterface $e) {
    // any failure from this library
    print $e->getMessage();
}

To handle specific failure modes, catch the narrower interfaces (all extend ExceptionInterface):

Interface Thrown when
Exception\Request\ConnectExceptionInterface The request could not reach the host (DNS/connection failure).
Exception\Response\BadResponseExceptionInterface The API returned a non-2xx status. Exposes getRequest(), getResponse(); the exception code is the HTTP status.
Exception\Response\TooManyRedirectsExceptionInterface The request exceeded the redirect limit.
Exception\Parse\ParseJsonExceptionInterface A JSON request body could not be encoded, or a JSON response could not be decoded.
Exception\Parse\ParseXmlExceptionInterface An XML response could not be parsed. Exposes getErrors() (LibXMLError[]).

Request/response exceptions expose the PSR-7 getRequest() (and getResponse() for response errors); parse exceptions expose the failing getMethod(), getUrl(), and getQueryStrings().

📝 Changelog

Notable changes in each release are listed in CHANGELOG.md.

📄 License

Released under the MIT License.