Working with third party services in laravel

Published on by

Working with third party services in laravel image

So a little over two years ago, I wrote a tutorial on how you should work with third-party services in Laravel. To this day, it is the most visited page on my website. However, things have changed over the last two years, and I decided to approach this topic again.

So I have been working with third-party services for so long that I cannot remember when I wasn't. As a Junior Developer, I integrated API into other platforms like Joomla, Magento, and WordPress. Now it mainly integrates into my Laravel applications to extend business logic by leaning on other services.

This tutorial will describe how I typically approach integrating with an API today. If you have read my previous tutorial, keep reading as a few things have changed - for what I consider good reasons.

Let's start with an API. We need an API to integrate with. My original tutorial was integrating with PingPing, an excellent uptime monitoring solution from the Laravel community. However, I want to try a different API this time.

For this tutorial, we will use the Planetscale API. Planetscale is an incredible database service I use to get my read-and-write operations closer to my users in the day job.

What will our integration do? Imagine we have an application that allows us to manage our infrastructure. Our servers run through Laravel Forge, and our database is over on Planetscale. There is no clean way to manage this workflow, so we created our own. For this, we need an integration or two.

Initially, I used to keep my integrations under app/Services; however, as my applications have gotten more extensive and complicated, I have needed to use the Services namespace for internal services, leading to a polluted namespace. I have moved my integrations to app/Http/Integrations. This makes sense and is a trick I picked up from Saloon by Sam Carrè.

Now I could use Saloon for my API integration, but I wanted to explain how I do it without a package. If you need an API integration in 2023, I highly recommend using Saloon. It is beyond amazing!

So, let's start by creating a directory for our integration. You can use the following bash command:

mkdir app/Http/Integrations/Planetscale

Once we have the Planetscale directory, we need to create a way to connect to it. Another naming convention I picked up off of the Saloon library is to look at these base classes as connectors - as their purpose is to allow you to connect to a specific API or third party.

Create a new class called PlanetscaleConnector in the app/Http/Integrations/Planetscale directory, and we can flesh out what this class needs, which will be a lot of fun.

So we must register this class with our container to resolve it or build a facade around it. We could register this to "long" way in a Service Provider - but my latest approach is to have these Connectors register themselves - kind of ...

declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale;
 
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
 
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
 
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: '',
)->timeout(
seconds: 15,
)->withHeaders(
headers: [],
)->asJson()->acceptJson(),
),
);
}
}

So the idea here is that all the information about how this class is registered into the container lives within the class itself. All the service provider needs to do is call the static register method on the class! This has saved me so much time when integrating with many APIs because I don't have to hunt for the provider and find the correct binding, amongst many others. I go to the class in question, which is all in front of me.

You will notice that currently, we have nothing being passed to the token or base url methods in the request. Let's fix that next. You can get these in your Planetscale account.

Create the following records in your .env file.

PLANETSCALE_SERVICE_ID="your-service-id-goes-here"
PLANETSCALE_SERVICE_TOKEN="your-token-goes-here"
PLANETSCALE_URL="https://api.planetscale.com/v1"

Next, these need to be pulled into the application's configuration. These all belong in config/services.php as this is where third-party services are typically configured.

return [
// the rest of your services config
 
'planetscale' => [
'id' => env('PLANETSCALE_SERVICE_ID'),
'token' => env('PLANETSCALE_SERVICE_TOKEN'),
'url' => env('PLANETSCALE_URL'),
],
];

Now we can use these in our PlanetscaleConnector under the register method.

declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale;
 
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
 
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
 
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: config('services.planetscale.url'),
)->timeout(
seconds: 15,
)->withHeaders(
headers: [
'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
],
)->asJson()->acceptJson(),
),
);
}
}

You need to send tokens over to Planetscale in the following format: service-id:service-token, so we cannot use the default withToken method as it doesn't allow us to customize it how we need to.

Now that we have a basic class created, we can start to think about the extent of our integration. We must do this when creating our service token to add the correct permissions. In our application, we want to be able to do the following: List databases. List database regions. List database backups. Create database backup. Delete database backup.

So, we can look at grouping these into two categories: Databases. Backups.

Let's add two new methods to our connector to create what we need:

declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale;
 
use App\Http\Integrations\Planetscale\Resources\BackupResource;
use App\Http\Integrations\Planetscale\Resources\DatabaseResource;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
 
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
 
public function databases(): DatabaseResource
{
return new DatabaseResource(
connector: $this,
);
}
 
public function backups(): BackupResource
{
return new BackupResource(
connector: $this,
);
}
 
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: config('services.planetscale.url'),
)->timeout(
seconds: 15,
)->withHeaders(
headers: [
'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
],
)->asJson()->acceptJson(),
),
);
}
}

As you can see, we created two new methods, databases and backups. These will return new resource classes, passing through the connector. The logic can now be implemented in the resource classes, but we must add another method to our connector later.

<?php
 
declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale\Resources;
 
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
 
final readonly class DatabaseResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
 
public function list()
{
//
}
 
public function regions()
{
//
}
}

This is our DatabaseResource; we have now stubbed out the methods we want to implement. You can do the same thing for the BackupResource. It will look somewhat similar.

So the results can be paginated on the listing of databases. However, I will not deal with this here - I would lean on Saloon for this, as its implementation for paginated results is fantastic. In this example, we aren't going to worry about pagination. Before we fill out the DatabaseResource, we need to add one more method to the PlanetscaleConnector to send the requests nicely. For this, I am using my package called juststeveking/http-helpers, which has an enum for all the typical HTTP methods I use.

public function send(Method $method, string $uri, array $options = []): Response
{
return $this->request->send(
method: $method->value,
url: $uri,
options: $options,
)->throw();
}

Now we can go back to our DatabaseResource and start filling in the logic for the list method.

declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale\Resources;
 
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use Illuminate\Support\Collection;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;
 
final readonly class DatabaseResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
 
public function list(string $organization): Collection
{
try {
$response = $this->connector->send(
method: Method::GET,
uri: "/organizations/{$organization}/databases"
);
} catch (Throwable $exception) {
throw $exception;
}
 
return $response->collect('data');
}
 
public function regions()
{
//
}
}

Our list method accepts the parameter organization to pass through the organization to list databases. We then use this to send a request to a specific URL through the connector. Wrapping this in a try-catch statement allows us to catch potential exceptions from the connectors' send method. Finally, we can return a collection from the method to work with it in our application.

We can go into more detail with this request, as we can start mapping the data from arrays to something more contextually useful using DTOs. I wrote about this here, so I won't repeat the same thing here.

Let's quickly look at the BackupResource to look at more than just a get request.

declare(strict_types=1);
 
namespace App\Http\Integrations\Planetscale\Resources;
 
use App\Http\Integrations\Planetscale\Entities\CreateBackup;
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;
 
final readonly class BackupResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
 
public function create(CreateBackup $entity): array
{
try {
$response = $this->connector->send(
method: Method::POST,
uri: "/organizations/{$entity->organization}/databases/{$entity->database}/branches/{$entity->branch}",
options: $entity->toRequestBody(),
);
} catch (Throwable $exception) {
throw $exception;
}
 
return $response->json('data');
}
}

Our create method accepts an entity class, which I use to pass data through the application where needed. This is useful when the URL needs a set of parameters and we need to send a request body through.

I haven't covered testing here, but I did write a tutorial on how to test JSON:API endpoints using PestPHP here, which will have similar concepts for testing an integration like this.

I can create reliable and extendible integrations with third parties using this approach. It is split into logical parts, so I can handle the amount of logic. Typically I would have more integrations, so some of this logic can be shared and extracted into traits to inherit behavior between integrations.

Steve McDougall photo

Technical writer at Laravel News, Developer Advocate at Treblle. API specialist, veteran PHP/Laravel engineer. YouTube livestreamer.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
A Resize Plugin for Alpine.js image

A Resize Plugin for Alpine.js

Read article
How to Migrate MySQL from DBngin to Laravel Herd image

How to Migrate MySQL from DBngin to Laravel Herd

Read article
Learn to master Query Scopes in Laravel image

Learn to master Query Scopes in Laravel

Read article
How to Redirect Uppercase URLs to Lowercase with Laravel Middleware image

How to Redirect Uppercase URLs to Lowercase with Laravel Middleware

Read article
PHP 8.4 Alpha 1 is now out! image

PHP 8.4 Alpha 1 is now out!

Read article
Generics Added to Eloquent Builder in Laravel 11.15 image

Generics Added to Eloquent Builder in Laravel 11.15

Read article