tomb1n0 / generic-api-client
A package to help speed up development when consuming APIs
Installs: 9 938
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 2
Open Issues: 0
Requires
- php: ^8.1
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.0
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^1.0|^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.5
- mockery/mockery: ^1.5
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^7.0|^8.0|^9.0
Suggests
- guzzlehttp/guzzle: A psr/http-client implementation that can be used with this package for a PSR-18 client
- guzzlehttp/psr7: A psr/http-factory implementation that can be used with this package for PSR-17 factories
This package is auto-updated.
Last update: 2024-12-15 14:46:12 UTC
README
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, PSR-17 and 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
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
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:
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:
$response = $api->json('GET', 'https://dummyjson.com/products'); if ($response->successful()) { $products = $response->json('products'); }
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
$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
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
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 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
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
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
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
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
composer test
Credits
License
The MIT License (MIT). Please see License File for more information.