Creating Dynamic Api SDKs in Laravel

Post Image

Published 4 months ago
By Ethan Brace

I often find myself creating new api connections in my Laravel applications and I'm tired of building the same thing over and over again. In this post, I'll show you how I'm working with Toggl's API using php magic. 🪄

First off, lets create a new class for accessing the API. It will need to extend the PendingRequest class.

class ApiService extends \Illuminate\Http\Client\PendingRequest
{
    protected $baseUrl = 'https://api.helloworld.com'
}

Building API sdks in this way allows great flexibility. While it might not look like much right now, we have full control over our new ApiService class, so lets see what we can do with it.

A simple request:

$response = ApiService::get();

Authentication

Most APIs require some sort of auth. This is very simple to add to our ApiService. Just make sure you've added the appropriate configuration and .env values, then call one of the auth methods on the Http client. In this case, we're using 'basic auth', which just means we're using a username and a password.

public function __construct()
{
		parent::__construct();

		$this->withBasicAuth(
				username: config('services.toggl.token'),
				password: 'api_token'
		);
}

Make it dynamic

At this point, we are not very far from the base Http client. We can make a call without authenticating each time, but that's it. Let's make it dead simple to make complex calls.

Something like this would be nice:

$prefs = ApiService::users($user_id)->preferences()->get()->json();

In order to do this, we'll need to make sure our class knows what we want to do each time we make a fluent call to a method (e.g. users() or preferences() ). We could either define those manually, or we could use some PHP magic.

public function __call($method, $parameters)
{
		$request_name = str($method)->camel()->singular()->ucfirst()->append("Request")
				->prepend('Braceyourself\TogglForLaravel\Requests\\')
				->value();

		if (class_exists($request_name)) {
				$request = new $request_name(...$parameters);

				// todo
				foreach ((new \ReflectionClass($this))->getProperties() as $property) {
						if ($value = $property->getValue($this) && !$property->getValue($request)) {
								$request->{$property->getName()} = $value;
						}
				}

				return $request;
		}

		return $this->appendPath($method, \Arr::first($parameters));
}

There are two things happening here. First, there's a check for the existence of a Custom Request Class, then if not, we call the appendPath method. Think of this like the Eloquent builder in Laravel. Each call to ->where() adds to the query being built, then when you're ready to get your data, you call ->get() and the query is executed.

Building an API request in this fashion works just as well.

Here's an example:

Toggl::reports()
		->summary()
		->post("time_entries.{$format}", $this->getTogglData())

The above code corresponds to this endpoint: https://api.track.toggl.com/reports/api/v3/workspace/{workspace_id}/summary/time_entries.pdf

The reports() method sets the base path to /reports/api/v3/workspace/{workspace_id}

In response, I get a PDF output based on the data we get from $this->getTogglData() which is stored in my database at reports.data

Clean up

One of the things I dislike about Laravel's Http client is that it requires a url in the action methods. Lets fix that...

public function get(string $url = '', $query = null)
{
		return parent::get($url, $this->data($query ?? [], true)->getData());
}

public function post(string $url = '', $data = [])
{
		return parent::post($url, $this->data($data, true)->getData());
}

public function put(string $url = '', $data = [])
{
		return parent::put($url, $this->data($data, true)->getData());
}

public function delete(string $url = '', $data = [])
{
		return parent::delete($url, $this->data($data, true)->getData());
}

public function patch(string $url = '', $data = [])
{
		return parent::patch($url, $this->data($data, true)->getData());
}
		

Now, we'll be able to just call ->get() without worrying a care in the world. 

You may notice that I'm wrapping the data in a call to the ->data() method. This method can also be used in public calls when using the API request. When we call ->data([...]) that given parameter (array) is added to the request. If we want to also add data in the http action methods, The above overrides will merge that data into the rest before sending it along to the parent Http client.

Using this on other APIs

I plan on packaging up the base functionality on the client I've created to use with Toggl's api. Keep your eye on my github account!

© ethanbrace.com | All rights reserved.