API Wrapper Library

Edit: Updated Jan 11, 2021 for version 2.x
.
The Problem With Guzzle
For making HTTP requests to remote APIs, most PHP developers turn to Guzzle. Considering its ubiquity, Guzzle likely requires no introduction, but for the uninitiated, it is a client library for sending and processing raw HTTP requests with nearly unlimited configuration options. For all its ubiquity, however, Guzzle lacks an elegant syntax, and its use often leads to an overabundance of boilerplate. Furthermore, the responsibility of organizing calls into reusable components falls entirely on the developer. At first this point would seem ideal, with the full control to develop a system that meets your needs, but in truth, it’s an unnecessary burden akin to developing an entire web application without a framework.
Consider this example for making a request to the JSONPlaceholder API, a free to use fake REST API for testing basic requests:
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://jsonplaceholder.typicode.com'
]);$response = $client->request('POST', 'posts', [
'json' => [
'title' => 'API Wrapper Library',
'body' => 'For making HTTP Requests to remote APIs...',
'completed' => false
]
]);$body = $response->getBody();$data = json_decode($body, true);print_r($data);// Array
// (
// ['title'] => API Wrapper Library
// ['body'] => For making HTTP Requests to remote APIs...
// ['completed'] => false
// ['id'] => 101
// )
For a single request, that isn’t too bad, but now imagine writing this code for any number of endpoints that represent a full-fledged API. Of course, you can reuse the Client
object once it's instantiated, and clever developers may even write a helper function for sending a request and returning the response as an array or object. Beyond that, any additional code to further improve the experience of using these remote calls starts to fall into the category of "unnecessarily burdensome" as I described before.
Client Libraries
To resolve this problem, many companies with popular APIs publish client packages that wrap all of the low-level request logic for you. This allows you to focus on using the API, rather than the logic of communicating with it in the first place. Beyond providing wrapper functions to each individual endpoint, some libraries even provide model classes which not only represent the incoming data as an object, but can also handle transacting that data to and from the API, further abstracting the low-level request layer.
Compared to the previous Guzzle example, consider this call to the Stripe API:
\Stripe\Stripe::setApiKey('my_key');$product = \Stripe\Product::create([
'name' => 'Cup of Coffee',
'description' => 'It keeps you going'
]);print_r($product);// Stripe\Product Object
// (
// [id] => prod_xxxxx
// [name] => Cup of Coffee
// [description => It keeps you going
// ...
// )
And when you consider that most frameworks will allow you to put one-time configuration options such as API Keys in a global configuration file, the inline code for executing the request is even shorter.
However, not every remote API provides a client package, and many developers simply fall back to using raw Guzzle requests, or worse, writing their own wrappers. In the modern development scene, there’s absolutely no excuse for burdening yourself with the design of an API wrapper schema every single time you need to query a remote system. Much like application frameworks which abstract boilerplate code for processing incoming requests, there should be a reusable abstraction for processing outgoing requests, thus allowing you to focus on the logic of your application.
API Wrapper Library
The core components for this library were primarily inspired by the Stripe client package for PHP, and by the Router and Eloquent Model components of the Laravel framework. (Despite the inspiration, please note that this package is usable in any PHP system, not just Laravel.) With a focus on being generic, adaptable, and reusable, this library allows you to easily define your configuration for each endpoint in a remote API, then quickly transition to using those endpoints with little to no boilerplate or hassle.
The design of this library is broken into two core components: the Request layer, and the Resource layer.
The Request layer acts much like the Laravel Router but in reverse. That is, you define all of your Endpoints using a single configuration system, and those Endpoints ultimately map to the underlying logic of executing the request with Guzzle. One could develop an entire API client with this feature alone, as it provides all of the logic necessary to abstract low-level requests into more friendly reusable components.
The Resource layer, in turn, acts much like the Laravel Model layer, or the models provided by the Stripe PHP client. That is, each Resource class represents a particular data model from the remote API, and contains the logic for calling the Endpoints to query and modify that data.
To get started, install the API Wrapper Library with composer:
composer require helium/api-wrapper
Declaring Routes
Like the Laravel Router, you can technically use this component to declare routes from anywhere, but the logic is best contained in a single configuration file.
If you’re using Laravel, I prefer to create a new Provider
class and execute this code in the boot
method. If you want to lean even more heavily into the Laravel design pattern, you could even create a file in the routes
folder and pull it into your provider with ApiWrapper::load('path/to/routes/api-wrapper.php)
.
If you’re not using Laravel, you should execute this configuration during your app’s setup phase, while all of the appropriate dependencies are being constructed, but before any real application logic gets executed.
Regardless, here is the basic configuration for declaring a set of remote Endpoints. Please note that we are querying the JSONPlaceholder API for “posts”, which is not to be confused with the HTTP POST verb.
use Helium\ApiWrapper\Api\Route;Route::group('https://jsonplaceholder.typicode.com',
[],
function() {
Route::get('posts.all', 'posts');
Route::get('posts.get', 'posts/{id}');
Route::post('posts.create', 'posts');
Route::patch('posts.update', 'posts/{id}');
Route::delete('posts.delete', 'posts/{id}');
}
);
With the first line, we declare a Route group, which applies the same configuration settings to all of the Routes declared within. For this simple example, the only shared option is the base URL.
Within the group, we declare each of our endpoints using the required HTTP verb, a reusable name to call this Endpoint later, and the URL path.
You can, of course, declare Routes outside of the context of a group, but in this case, you must provide the full endpoint URL, rather than just the path stub. Likewise, you may also nest Route groups, allowing you to selectively apply additional configuration options to certain endpoints.
Route Processors
Similar to the concept of Middleware, Processors are classes which intercept Requests to a certain Route before and after they are executed. Using Processors is the preferred way of applying certain repeating configuration rules, such as Authorization headers, to your Requests.
Take this example:
use Helium\ApiWrapper\Api\Processor;
use Helium\ApiWrapper\Api\Request;
use Helium\ApiWrapper\Api\Response;class AuthorizationProcessor extends Processor
{
public static function handle(Request $request,
callable $next): Response
{
//Pre-execution
$request->headers(['Authorization' => 'your_auth_token'] //Execute
$response = $next($request); //Post-execution return $response;
}
}
Like in Laravel Middleware, the same Processor class can be used to process requests both before and after execution. Pre-execution logic may be declared before the call to $next
, and post-execution logic may be declared after the call to $next
. In either case, your Processor's handle
function should return the Response object returned by the call to $next
.
You can attach Processors to your Routes in a few different ways:
Route::group('https://jsonplaceholder.typicode.com',
GroupProcessor1::class,
function() {
Route::get('posts.all', 'posts')
->processor(Processor1::class);
Route::get('posts.get', 'posts/{id}')
->processor([Processor1::class, Processor2::class]);
Route::group(processors: [GroupProcessor2::class],
callback: function() {
Route::post('posts.create', 'posts');
}
);
}
);
To apply a Processor to multiple requests, you can attach them to your Route groups. The processors
argument of the Route::group
function accepts either a single Processor class name or an array of Processor class names. With PHP 8, you may also use named arguments when calling the Route::group
function.
To apply a Processor to a single Route endpoint, you can call the processor
method on the declared route. Like Route::group
, the processor
method accepts either a single class name, or an array of class names.
With this, it should be obvious that a Route will receive all of the Processors attached to it directly, as well as all of the Processors attached to its parent Groups. In this example, the posts.all
route will receive the GroupProcessor1
and Processor1
Processors; the posts.get
route will receive GroupProcessor1
, Processor1
, and Processor2
; and the posts.create
route will receive GroupProcessor1
and GroupProcessor2
.
Requests and Responses
Once you have declared your Routes and all of the appropriate configuration options, you can begin to actually send and process Requests.
$response = Request::route('posts.update')
->pathParams(['id' => 1])
->json([
'title' => 'API Wrapper Library',
'body' => 'For making HTTP Requests to remote APIs...',
])->send();print_r($response->json());// Array
// (
// [userId] => 1
// [id] => 1
// [title] => API Wrapper Library
// [body] => For making HTTP Requests to remote APIs...
// )
Use the pathParams
function to set the value of any keys that were declared in your routes with inline curly braces {}
.
Use the headers
function to append headers to your request. This function accepts an associative array of header names and values.
For Post, Put, and Patch operations, use one of the body
, json
, formParams
, or multipart
functions to form your request body, depending on your needs.
Finally, for any additional configuration options to pass directly to Guzzle, use the options
function.
To customize the underlying Guzzle Client, you may pass an instance of GuzzleHttp\\Client
as a second parameter to the Request::route
function. This is most often used when debugging or mocking Guzzle for tests.
$client = new Client([
'handler' => HandlerStack::create(new MockHandler([
new Response(status: 200, body: [
'userId' => 1,
'id' => 1,
'title' => 'API Wrapper Library',
'body' => 'For making HTTP Requests to remote APIs...'
])
]))
]);$response = Request::route('posts.get', $client)
->pathParams(['id' => 1])
->send();
The send
function executes your request, and returns a Response
object.
Use the getContents
function to get the Response body as a string.
Use the json
function to return the decoded Response body JSON. The default return value is an associative array, but to get your results as stdClass instead you can pass false
into the function.
As the Response
class implements Psr\Http\MessageResponseInterface
, you can also call any function that you're accustomed to calling on the Response objects returned directly from Guzzle.
Resources
To further abstract the Request layer into usable data objects with basic ORM-like behavior, you can create Resource classes. For APIs that don’t fit or adhere to REST standards, it may not make sense to use Resource classes, but for APIs that deal in relational object data, Resources provide a fair bit of useful behavior.
The Resource class derives a lot of inspiration from Eloquent Models, although it is much more slimmed down. Internally, it maintains an array of attributes, or key/value pairs to represent the object’s data. It also includes the inherent ability to cast raw data to alternate primitive or class types, such as casting date strings and timestamps to Carbon
instances, or casting nested data arrays to other Resource classes. Finally, when creating your Resource classes, you can attach various custom or pre-built Operations, or functions that execute Requests directly. This library provides implementations for all of the basic CRUD operations, including All, Create, Delete, Get, and Update, but you may implement your own custom Operations as necessary to meet the needs of the remote API you're working with.
use Helium\ApiWrapper\Resource\Contracts\All as AllContract;
use Helium\ApiWrapper\Resource\Contracts\Create as CreateContract;
use Helium\ApiWrapper\Resource\Contracts\Delete as DeleteContract;
use Helium\ApiWrapper\Resource\Contracts\Get as GetContract;
use Helium\ApiWrapper\Resource\Contracts\Update as UpdateContract;
use Helium\ApiWrapper\Resource\Operations\All;
use Helium\ApiWrapper\Resource\Operations\Create;
use Helium\ApiWrapper\Resource\Operations\Delete;
use Helium\ApiWrapper\Resource\Operations\Get;
use Helium\ApiWrapper\Resource\Operations\Update;
use Helium\ApiWrapper\Resource\Resource;/**
* @property int $id
* @property int $userId
* @property string $title
* @property string $body
*/
class Post extends Resource implements AllContract, CreateContract, DeleteContract, GetContract, UpdateContract
{
use All, Create, Delete, Get, Update;
}
To start making Requests with your Resource class, simply call the Operation methods:
$post = Post::get(1); //Provided by Get Operation Traitprint_r($post->toArray());// Array
// (
// [userId] => 1
// [id] => 1
// [title] => sunt aut facere repellat...
// [body] => quia et suscipit...
// )$post->title = 'API Wrapper Library';
$post->body = 'For making HTTP Requests to remote APIs...';$post->saveChanges(); //Provided by Update Operation Traitprint_r($post->toArray());// Array
// (
// [userId] => 1
// [id] => 1
// [title] => API Wrapper Library
// [body] => For making HTTP Requests to remote APIs...
// )
Under the hood, the Operation implementations simply map to Request calls, and provide for default behavior that should work with most basic REST APIs. For more complex calls that don’t work out of the box, you can either override the default implementations, or create your own custom operations.
By default, each Operation assumes a certain format for the route name consisting of the pluralized classname followed by the operation name. For example, the Post::all
operation assumes you configured a route named posts.all
. To customize the Request Routes used by each of the default Operation implementations, simply set the corresponding property on your Resource class.
class Post extends Resource implements AllContract, CreateContract, DeleteContract, GetContract, UpdateContract
{
use All, Create, Delete, Get, Update; protected string $allRoute = 'customAllRoute';
protected string $createRoute = 'customCreateRoute';
protected string $deleteRoute = 'customDeleteRoute';
protected string $getRoute = 'customGetRoute';
protected string $updateRoute = 'customUpdateRoute';
}
Future Work
In future versions of this package, I’d like to continue expanding the functionality for both the Route and Resource layers.
For Routes, I plan to included named Processors, inspired by named Middleware in Laravel, which will allow you to attach Processors to your Routes using a simplified name string rather than the full classname. Similarly, I’d like to include Processor Groups, like Laravel Middleware Groups, to further simplify the process of defining your API route configuration.
For Resources, I would draw additional inspiration from Laravel’s Eloquent Models. I plan to add additional default Operation implementations, such as a Search to query objects based on certain query parameters, to round out the classes and minimize the amount of custom code required to tap into most basic APIs. Additionally, a strong relationship system for easily querying data in relational systems would be a welcome addition.
Like with most things, I won’t see the places where the experience could be improved and simplified until I get a chance to use it in the wild, but feedback and suggestions are always welcome from those who take an interest in my work here.
Conclusion
In short, the API Wrapper library provides a method for abstracting and reusing the logic to execute basic HTTP requests. My hope with this system is to help expedite the process of developing remote API clients so that we can all get back to work on the logic of our own applications. By neatly packaging your configuration options away in separate files and classes, you’re free to simply use the client where it’s needed without writing a ton of boilerplate or messy helper functions.
For more specific information on the use of each specific component, check out the project source on BitBucket, or start tinkering in your own projects by installing our Composer package!