Integrate third-party services within Laravel
Tags: web, php, laravel
Published: 2022-11-09 20:50:11

---
title: Integrate third-party services within Laravel
published: true
description: Implement any external API using Service Containers.
tags: web, php, laravel
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/znfctlq33g69pxf8eoo6.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-11-05 11:26 +0000
---

Hello everyone and welcome to this new article on Laravel.

Today I want to show you an interesting way to deal with any third-party services or external API you need to integrate in your Laravel projects.

- [Use case: get the user IP info](#use-case-get-the-user-ip-info)
- [Creating the contract](#creating-the-contract)
- [Implement IpInfo as a Geolocator](#implement-ipinfo-as-a-geolocator)
- [Tell Laravel what is our current Geolocator](#tell-laravel-what-is-our-current-geolocator)
- [Use the Geolocator on your controller](#use-the-geolocator-on-your-controller)
- [Swapping the Geolocator](#swapping-the-geolocator)
- [Conclusion](#conclusion)

## Use case: get the user IP info

We will start from a freshly installed Laravel project which allows user to register and authenticate to their dashboard.

On my case I will say I used [Laravel Breeze](https://laravel.com/docs/9.x/starter-kits) to implement such feature, but it can be anything else you want (including your custom authentication logic).

The need is, everytime one user has authenticated, we want to know from where and store this information.

After a while, you found [IPInfo](https://ipinfo.io/) to have all the requirement you need and a generous free plan.

## Creating the contract

Laravel provides a pattern to easily "swap" between different services. For the moment it sounds opaque, so let's follow along to find out how it will help us in the long term.

We will create an interface that will describe all the feature we seek in IPInfo. 

I generally try to find a verb that defines my service, so let's call this interface a "Geolocator".

Create a new "app/Contracts" folder, and create a new "Geolocator.php" file in it.

```php
namespace App\Contracts;

use App\DataTransferObjects\Geolocation;

interface Geolocator
{
  public function locate(string $ip): Geolocation;
}
```

Let's break it down:

- We define a "locate" method in the "Geolocator" interface
- This method must take a string representing the IP we want to scan
- This method must return a "Geolocation" object

The reason we force the method to return our own representation of the geolocation of the IP is for any future implementation to stick with this constraint so we do not have to change anything in our code when we swap with a new implementation.

And that is the force of this pattern: easy service swapping with a minimum of code change.

For information, here is what the "Geolocation" data transfer object look like in "app/DataTransferObjects/Geolocation.php".

```php
namespace App\DataTransferObjects;

class Geolocation
{
  private $ip;
  private $city;
  private $country;
  private $timezone;
  private $internetProvider;

  public function __construct($ip, $city, $country, $timezone, $internetProvider)
  {
    $this->ip = $ip;
    $this->city = $city;
    $this->country = $country;
    $this->timezone = $timezone;
    $this->internetProvider = $internetProvider;
  }

  public function ip()
  {
    return $this->ip;
  }

  public function city()
  {
    return $this->city;
  }

  public function country()
  {
    return $this->country;
  }

  public function timezone()
  {
    return $this->timezone;
  }

  public function internetProvider()
  {
    return $this->internetProvider;
  }
}
```

## Implement IpInfo as a Geolocator

Let us create a new implementation of Geolocator. 

First, let us install the package ([IPInfo Github page](https://github.com/ipinfo/php)):

```bash
composer require ipinfo/ipinfo
```

Next, we will create a config file to store our secure token (available once you create an account on the official website). Let us create a file "config/ipinfo.php" with this content.

```php
return [
  "access_token" => env("IPINFO_ACCESS_TOKEN"),
];
```

And let us add the dot env variable on our file ".env".

```env
...

IPINFO_ACCESS_TOKEN=your-secure-token-here
```

I like to store implementations on a "Services" folder. Create a folder "app/Services/Geolocator", and add a new "IpInfo.php" file within it:

```php
namespace App\Services\Geolocator;

use App\Contracts\Geolocator;
use App\DataTransferObjects\Geolocation;
use ipinfo\ipinfo\IPinfo as BaseIpInfo;

class IpInfo implements Geolocator
{
  public function locate(string $ip): Geolocation
  {
    $ipInfo = new BaseIpInfo(config("ipinfo.access_token"));

    $details = $ipInfo->getDetails($ip);

    return new Geolocation($ip, $details->city, $details->country, $details->timezone, $details->org);
  }
}
```

## Tell Laravel what is our current Geolocator

Let us wrap up and instruct Laravel to "bind" our Geolocator to our current implementation. This is done on the "AppServiceProvider" file available by default on "app/Providers/AppServiceProvider.php".

```php
namespace App\Providers;

use App\Contracts\Geolocator;
use App\Services\Geolocator\Ipinfo;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
  public function register()
  {
    $this->app->bind(Geolocator::class, IpInfo::class);
  }

  public function boot()
  {
    // ...
  }
}
```

## Use the Geolocator on your controller

Let us now find the user geolocation right after logged on the app.

Using Laravel Breeze, this is done on the "AuthenticatedSessionController" controller, so let us head into this file and intercept the moment just before redirecting to the dashboard.

```php
namespace App\Http\Controllers\Auth;

use App\Contracts\Geolocator;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{
    public function create(Request $request)
    {
        // ...
    }

    public function store(LoginRequest $request, Geolocator $geolocator)
    {
        $request->authenticate();
        $request->session()->regenerate();

        $ip = $request->ip();
 
        // Here is where we get the geolocation       
        $geolocation = $geolocator->locate($ip);
        $contry = $geolocation->country();

        // Save the country on a separate table linked to the user for example...

        return redirect()->intended(RouteServiceProvider::HOME);
    }

    public function destroy(Request $request)
    {
        // ...
    }
}
```

If you notice, I did not add "IpInfo" class on the method parameter, but the contract. And yet, Laravel was able to give me an instance of the IpInfo class.

This is the power of Laravel: the ability to inject dependencies and resolve them on controller parameters (more on this on the conclusion).

## Swapping the geolocator

Let us imagine some months have passed, and your app is a big success!

You want to find a cheaper geolocation service, and you found [IP API](https://ipapi.co/) to suit your needs.

Thanks to our system, swapping them will just require you to do 2 things:

- To create an implementation of IP API as a "Geolocator"
- To ask Laravel to load IP API as your current Geolocator

Let us create the "IpApi.php" file under "app/Services/Geolocator" folder.

```php
namespace App\Services\Geolocator;

use App\Contracts\Geolocator;
use App\DTOs\Geolocation;

class IpApi implements Geolocator
{
  public function locate(string $ip): Geolocation
  {
    $response = file_get_contents("https://ipapi.co/$ip/json/");
    $location = json_decode($response, true);

    return new Geolocation($ip, $location["city"], $location["country"], $location["timezone"], $location["org"]);
  }
}
```

Let us change our current implementation in  "app/Providers/AppServiceProviders.php".

```diff
  namespace App\Providers;

  use App\Contracts\Geolocator;
- use App\Services\Geolocator\IpInfo;
+ use App\Services\Geolocator\IpApi;
  use Illuminate\Support\ServiceProvider;

  class AppServiceProvider extends ServiceProvider
  {
    public function register()
    {
-     $this->app->bind(Geolocator::class, IpInfo::class);
+     $this->app->bind(Geolocator::class, IpApi::class);
    }

    public function boot()
    {
      // ...
    }
  }
```

Your controller code does not need to be touched at this point, which is the reason why this pattern is so powerful.

## Conclusion

Let us sum up what we have done.

1. We defined a new concept: Geolocator
2. We have 2 available Geolocator: IpInfo and IpApi
3. At this point we currently use IpApi

All the added value resides in the ability to flexibly swap from one implementation to another, without having to do heavy refactors on our controller (or anywhere else the Geolocator is needed).

If the concept of contracts and binding (or service container) is still fuzzy for you, I highly recommand [this video from Laracast](https://laracasts.com/series/laravel-8-from-scratch/episodes/61) (it is actually when things clicked for me, and I hope this will help you too).

### Limit

One limit with the current implementation is it is not "scalable": if your app is really becoming a success, you are forcing your user to wait a few seconds before we get the response of our Geolocator, and a few milliseconds to save this info in database before actually redirecting the user to its dashboard.

In this case the preferable way would be to use Queue jobs. If you want to know more about it, I've covered it in this article

{% embed https://dev.to/anwar_nairi/laravel-commands-and-queue-jobs-when-to-use-them-h45 %}

### Tests

Not only this pattern helps having a future-proof code base, but it actually also helps with testing!

Since our Geolocator is a bound (e.g. resolvable) dependency, we can ask Laravel to mock it within our tests.

Here is how to test that our ip is well saved in our "geolocations" table.

```php
namespace Tests\Feature\Http\Controllers;

use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;

class AuthenticatedSessionControllerTest extends TestCase
{
  use WithFaker;

  public function testCountryIsStoredWhenUserAuthenticates()
  {
    $ip = $this->faker->ip();
    $country = $this->faker->country();
    $city = $this->faker->city();
    $timezone = $this->faker->timezone();
    $internetProvider = $this->faker->name();
    $email = $this->faker->email();
    $password = $this->faker->password();
    
    $this->mock(Geolocator::class)
      ->shouldReceive("locate")
      ->andReturn(new Geolocation($ip, $city, $country, $timezone, $internetProvider));

    $user = User::factory()
      ->create(["email" => $email, "password" => Hash::make($password)]);

    $this->assertDatabaseCount("geolocations", 0);

    $this->post(route("login"), [
        "email" => $email,
        "password" => $password,
      ])
      ->assertValid()
      ->assertRedirect(route("dashboard"));
    
    $this->assertDatabaseHas("geolocations", [
      "user_id" => $user->id,
      "country" => $country,
    ]);
  }
}
```

### Bonus

By the way, here is a most succint way to write our Geolocation data transfer object using all the latest PHP 8.1 goodies:

```php
namespace App\DataTransferObjects;

class Geolocation
{
  public function __construct(
    public readonly string $ip,
    public readonly string $city,
    public readonly string $country,
    public readonly string $timezone,
    public readonly string $internetProvider,
  ) {}
}
```

And when PHP 8.2 will be out, it will become even elegant.

```php
namespace App\DataTransferObjects;

readonly class Geolocation
{
  public function __construct(
    public string $ip,
    public string $city,
    public string $country,
    public string $timezone,
    public string $internetProvider,
  ) {}
}
```

Cool huh?

### Before leaving

That is all I have for today, I hope you will leave with some new ideas when implementing your next external service!

Happy third-party implementation 🛰️