PHPackages                             tomb1n0/generic-api-client - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. tomb1n0/generic-api-client

ActiveLibrary[API Development](/categories/api)

tomb1n0/generic-api-client
==========================

A package to help speed up development when consuming APIs

0.10(7mo ago)218.6k↓15.4%2[1 issues](https://github.com/tomb1n0/generic-api-client/issues)[1 PRs](https://github.com/tomb1n0/generic-api-client/pulls)MITPHPPHP ^8.1CI passing

Since Jan 14Pushed 3mo ago1 watchersCompare

[ Source](https://github.com/tomb1n0/generic-api-client)[ Packagist](https://packagist.org/packages/tomb1n0/generic-api-client)[ Docs](https://github.com/tomb1n0/guzzle-mock-handler)[ RSS](/packages/tomb1n0-generic-api-client/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (9)Versions (21)Used By (0)

Generic API Client
==================

[](#generic-api-client)

[![Latest Version on Packagist](https://camo.githubusercontent.com/40edaaf4499b409b7f2828beec6ec040200b51261085b93c3a33e1bb313b082a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f746f6d62316e302f67656e657269632d6170692d636c69656e742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/tomb1n0/generic-api-client)[![Total Downloads](https://camo.githubusercontent.com/3ebfe0bd246d1aefa1a80dab3a3e3f50b7d44b6d4fc93d8ba8d3164eaf7c8a40/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f746f6d62316e302f67656e657269632d6170692d636c69656e742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/tomb1n0/generic-api-client)

When developing integrations for my PHP applications, i've often found myself carrying very similar but slightly different boiler-plate code around with me.

Often, each implementation will need:

- Some way of handling auth.
    - Often is as simple as adding an extra header or property to the body of the request.
- Pagination handling
    - Often boils down to checking for the presence of a header or some property in the body of the response, before fetching the next page with an added header/query parameter.
- Response Mocking
    - Providing confidence that our integration is working as we expected, and error conditions are properly handled.

There's also the question of what HTTP client to use, with the introduction of [PSR-7](https://www.php-fig.org/psr/psr-7/), [PSR-17](https://www.php-fig.org/psr/psr-17) and [PSR-18](https://www.php-fig.org/psr/psr-18) we are able to depend on HTTP clients and factories that implement these interfaces rather than relying on any one client.

My goal with this package is to provide a wrapper around these PSR interfaces that makes it simpler to write API integrations.

Installation
------------

[](#installation)

Please note that this package does not require a HTTP client out of the box - but rather it depends on the virtual packages `psr/http-client-implementation` and `psr/http-factory-implementation`. This allows the package to be client-agnostic.

If you're unsure on this, i would recommend requiring `guzzlehttp/guzzle` alongside this package as it provides implementations for the above virtual packages.

```
composer require tomb1n0/generic-api-client guzzlehttp/guzzle
```

If your project already require a HTTP-client that has implementations for the above standards, you can omit the guzzle dependency.

Usage
-----

[](#usage)

Please note that the examples below assume the use of Guzzle for the PSR-18 Client etc. Feel free to swap these out with your own.

#### Client Instantiation:

[](#client-instantiation)

```
use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Client as GuzzleHttpClient;

$api = new Client(
    new GuzzleHttpClient(), // PSR-18 Client that sends the PSR-7 Request
    new HttpFactory(), // A PSR-17 Request Factory used to create PSR-7 Requests
    new HttpFactory(), // A PSR-17 Response Factory used to create PSR-7 Responses
    new HttpFactory(), // A PSR-17 Stream Factory used to create the bodies of our PSR-7 requests
    new HttpFactory(), // a PSR-17 URI Factory used to create URIs.
);
```

#### Making a JSON request:

[](#making-a-json-request)

```
$response = $api->json('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}
```

#### Making a Form (x-www-form-urlencoded) request:

[](#making-a-form-x-www-form-urlencoded-request)

```
$response = $api->form('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}
```

#### Making a request using a PSR-7 request directly

[](#making-a-request-using-a-psr-7-request-directly)

```
$requestFactory = new GuzzleHttp\Psr7\HttpFactory();
$request = $requestFactory->createRequest('GET', 'https://example.com');

$response = $api->send($request);

if ($response->successful()) {
    // Do something with the response.
}
```

### Configuration

[](#configuration)

#### Base Url:

[](#base-url)

```
$client = $existingClient->withBaseUrl('https://dummyjson.com');

$response = $client->json('GET', '/products'); // Will make a request to https://dummyjson.com/products.
```

Note that if you try to perform a request to a fully-formed URL that is different to the Base URL, the Base URL is ignored.

#### Pagination

[](#pagination)

You can create a pagination handler by creating a class that implements the `PaginationHandlerContract` interface provided by this package.

```
// Create a class that implements the PaginationHandlerContract
class PaginationHandler implements PaginationHandlerContract
{
    public function hasNextPage(Response $response): bool
    {
        return $response->toPsr7Response()->hasHeader('next-page');
    }

    public function getNextPage(Response $response): RequestInterface
    {
        $originalRequest = $response->toPsr7Request();
        $psr7Response = $response->toPsr7Response();

        return $originalRequest->withHeader('page', $psr7Response->getHeaderLine('next-page'));
    }
}
$handler = new PaginationHandler();
$client = $existingClient->withPaginationHandler($handler);

$response = $client->json('GET', 'https://dummyjson.com/products');

// HasNextPage will defer to the Pagination Handler to determine if the Response has a next page
if ($response->hasNextPage()) {
    $nextPage = $response->getNextPage();
}

// For convenience, a helper is provided to fetch all pages in a loop:
$response->forEachPage(function (Response $response) {
    // Do something with this pages response
});
```

#### Middleware

[](#middleware)

Middleware can be created by creating a class that implements the `MiddlewareContract` interface.

```
class AuthenticationMiddleware implements MiddlewareContract
{
    public function __construct(protected string $accessToken)
    {
    }

    public function handle(RequestInterface $request, callable $next): ResponseInterface
    {
        // Mutate the request
        $request = $request->withHeader('Authorization', 'Bearer ' . $this->accessToken);

        // Call the next middleware in the chain, ultimately fetching the Response.
        $response = $next($request);

        // Can also mutate the Response here if desired.
        $response = $response->withHeader('X-My-Header', 'Foo');

        // Return the Response
        return $response;
    }
}

// Multiple middleware can be provided
$client = $existingClient->withMiddleware([
    new AuthenticationMiddleware('my-access-token');
]);

// The request will be sent through our middleware in the order given.
$response = $client->json('GET', 'https://dummyjson.com/products');
```

Note that it is possible for a middleware to mutate the request before it is sent, or the response after it is received.

### Testing the API

[](#testing-the-api)

#### Stubbing Responses

[](#stubbing-responses)

It is possible to stub responses for testing purposes:

```
// It is important to call fake first, as this returns a new client with a Fake PSR-18 client underneath.
$client = $existingClient->fake()->stubResponse(
    'https://dummyjson.com/products',
    [
        'products' => [['id' => 1], ['id' => 2]],
    ],
    200,
    ['X-Custom-Header' => 'Foo'],
);

$response = $client->json('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}
```

### Preventing Stray Requests

[](#preventing-stray-requests)

By default the library will return a `200 OK` for any non-matched responses when faked. If you prefer, you can prevent stray requests:

```
$client = $existingClient->fake()->preventStrayRequests();

try {
    // Make a request which has not been stubbed
    $response = $client->json('GET', 'https://dummyjson.com/products');
} catch (NoMatchingStubbedResponseException $e) {
    // a NoMatchingStubbedResponseException exception will be thrown.
}
```

#### Asserting Requests

[](#asserting-requests)

Maybe you want to assert the correct payload is sent to an API to create a user:

```
$client = $existingClient->fake()->stubResponse('https://dummyjson.com/users', null, 200);

// This would likely be in some Service object method your test is calling.
$response = $client->json('POST', 'https://dummyjson.com/users', ['name' => 'Tom']);

// Assert we sent a request with the correct payload
$client->assertSent(function (RequestInterface $request) {
    $contents = $request->getBody()->getContents();
    $expected = ['name' => 'Tom'];

    return $contents === $expected
});
```

#### Asserting Requests With Custom Request Matching

[](#asserting-requests-with-custom-request-matching)

Maybe you want to stub a response using some other information in the request

Do the same as above but use the `stubResponseWithCustomMatcher` method providing a custom implementation of the matcher contract. For example you could use the included `UrlMatcher` to check the method type

```
class UrlMatcher implements FakeResponseMatcherContract
{
    public function __construct(private string $url, private ?string $method = 'GET')
    {
    }

    public function match(RequestInterface $request): bool
    {
        $requestUrl = (string) $request->getUri();
        $requestMethod = $request->getMethod();

        return $this->url === $requestUrl && $this->method === $requestMethod;
    }
}
```

```
$client = $existingClient->fake();
$client->stubResponseWithCustomMatcher(new UrlMatcher('https://dummyjson.com/users', 'GET'), null, 200);
$client->stubResponseWithCustomMatcher(new UrlMatcher('https://dummyjson.com/users', 'POST'), null, 500);
```

Running the Tests
-----------------

[](#running-the-tests)

```
composer test
```

Credits
-------

[](#credits)

- [Tom Harper](https://github.com/tomb1n0)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance62

Regular maintenance activity

Popularity30

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 83.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~70 days

Recently: every ~181 days

Total

15

Last Release

237d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0a38ae99b48bbe724ca5d9589e6a55b48511f5300822aab97fd05f5601e0a97a?d=identicon)[tomb1n0](/maintainers/tomb1n0)

---

Top Contributors

[![tomb1n0](https://avatars.githubusercontent.com/u/11994391?v=4)](https://github.com/tomb1n0 "tomb1n0 (35 commits)")[![ollwells](https://avatars.githubusercontent.com/u/61095076?v=4)](https://github.com/ollwells "ollwells (4 commits)")[![AliceKLWilliams](https://avatars.githubusercontent.com/u/25745335?v=4)](https://github.com/AliceKLWilliams "AliceKLWilliams (3 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tomb1n0-generic-api-client/health.svg)

```
[![Health](https://phpackages.com/badges/tomb1n0-generic-api-client/health.svg)](https://phpackages.com/packages/tomb1n0-generic-api-client)
```

###  Alternatives

[openai-php/client

OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API

5.8k22.6M232](/packages/openai-php-client)[getbrevo/brevo-php

Official Brevo provided RESTFul API V3 php library

963.1M35](/packages/getbrevo-brevo-php)[swisnl/json-api-client

A PHP package for mapping remote JSON:API resources to Eloquent like models and collections.

211473.2k12](/packages/swisnl-json-api-client)[opensearch-project/opensearch-php

PHP Client for OpenSearch

15224.3M65](/packages/opensearch-project-opensearch-php)[phpro/http-tools

HTTP tools for developing more consistent HTTP implementations.

28137.8k](/packages/phpro-http-tools)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

35636.1k2](/packages/telnyx-telnyx-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
