christianjbrown / api-client
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.
Requires
- php: ^8.5
- ext-dom: *
- ext-libxml: *
- ext-simplexml: *
- guzzlehttp/guzzle: ^7.15
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- symfony/dependency-injection: ^8.0
Requires (Dev)
- christianjbrown/code-quality-scripts: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-01 13:35:38 UTC
README
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.