SDKs, The Laravel Way
Each call to the builder will modify the underlying pending request. Every modification, another link in the chain, that will be sent, finally, with get().
The joy I get when using a beautifully-crafted API cannot be understated. One that is approachable for beginners, and can pack a punch for experienced engineers. Beautiful, simple, flexible. An elusive combination that all engineers strive for, but some never find.
It's an investment
Wiping the dust off an old feature weeks, months, or even years later, I want to reduce the amount of head scratching until I understand what it is I'm looking at. Having used the Laravel framework for years, I find its core APIs easy to come back to. So, designing things in such a way that feels native to Laravel is an investment into my future self.
An Eloquent Example
How do you fetch stored data in Laravel? Eloquent. Much of the time (not always), an SDK is just an abstraction for fetching data from an API. Sometimes, it's just a simple wrapper around making HTTP requests. But what's the difference between fetching data from a database and fetching it from an API?
If we were using Eloquent models talking to the application's database. Fetching data would look something like this:
// filtering
Ticket::where('status', 'open')->get();
Ticket::open()->get();
// selecting fields
Ticket::select(['id', 'summary'])->get();
Ticket::get(['id', 'summary']);
// sorting
Ticket::orderBy('created_at', 'desc')->get();
Ticket::latest()->get();
// eager loading
Ticket::with(['board', 'notes'])->get();
// all
Ticket::get();
We've got a Model
, some methods that return a Builder
object for further method chaining, and some methods that execute the query and return a Collection
of results.
Why couldn't we, instead of building a query to be executed, build an HTTP Request to be sent?
With this comparison in mind, modeling our SDK to mimic Eloquent feels... right. It feels like swimming with the current. Feels, natural. Like breathing.
Getting tickets
The most basic call, requiring no filtering, no sorting, should look like this.
$tickets = Ticket::get();
Unless an error has occurred, we should get a Collection
of tickets.
The Ticket
Model
The get()
method looks pretty intense, but it's only getting some connections settings, building the request, wrapping the response in a Collection
. We're going to simplify this later.
Connection Settings
To make our base request work we'll need to add some values to our services config.
Allowing for static method calls
Notice the Ticket's get()
method wasn't static
? Of course you did. 😉
This is copied straight from Laravel's Model. And that's all you need. From the Ticket
, you can now call any method on the Ticket
.
Ticket::get();
Ticket::url();
// instead of
(new Ticket)->get();
(new Ticket)->url();
Filtering, Sorting, Limiting
In Eloquent, these concerns are all handled by a Builder
object. This object is responsible for providing a fluent interface for updating its own properties and returning the instance to allow for method chaining.
As you can see, this object acts as the glue between our HTTP PendingRequest
and our Ticket
model.
Accessing the builder from the model
One of the many brilliant things Eloquent does is provide easy access to the Builder
object from the Model
. By forwarding any calls to methods that do not exist on the model to a new builder.
$tickets = Ticket::get();
// instead of
$tickets = (new RequestBuilder(new Ticket))->get();
Modifying the request
For the other building methods like where
, whereIn
, whereNull
, whereBetween
, orderBy
, limit
, select
, and so on. Each call to the RequestBuilder
will modify the underlying PendingRequest
. Every modification, another link in the chain, that will will be sent, finally, with get()
.
Read the documentation for both the API you are making requests to and Laravel's HTTP Client. Get real intimate with those. Heck, go source-diving. I've never regretting it.
Preparing for more Models
We can move most of the Ticket methods to a base Model
object so that other models can extend.
HTTP Macro
To make the construction of the RequestBuilder
less verbose, we will create an HTTP macro.
Request scopes
I really like query scopes in Eloquent. Such a small thing. Makes the developer experience so much nicer though.
class Ticket extends Model
{
// url()
public function open(): RequestBuilder
{
return $this->where('status', 'open');
}
}
Ticket::open()->get();
// instead of
Ticket::where('status', 'open')->get();
If there ever comes a new model that needs the open()
scope we could wrap it up in a HasStatus
trait.
Other considerations
For the sake of brevity, I've chosen to omit certian topics like defining relationships. I would tackle those considerations that same way. How does Eloquent handle this? Even if the answer is "it doesn't", it's still a great place to start.
What do you think?
I really like this way of structuring SDKs and have used this methodology successfully for years. But I'd like to know what you think. Let me know.