DEV Community

Cover image for Integrate third-party services within Laravel
Anwar
Anwar

Posted on

Integrate third-party services within Laravel

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

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 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 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.

namespace App\Contracts;

use App\DataTransferObjects\Geolocation;

interface Geolocator
{
  public function locate(string $ip): Geolocation;
}
Enter fullscreen mode Exit fullscreen mode

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".

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Implement IpInfo as a Geolocator

Let us create a new implementation of Geolocator.

First, let us install the package (IPInfo Github page):

composer require ipinfo/ipinfo
Enter fullscreen mode Exit fullscreen mode

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.

return [
  "access_token" => env("IPINFO_ACCESS_TOKEN"),
];
Enter fullscreen mode Exit fullscreen mode

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

...

IPINFO_ACCESS_TOKEN=your-secure-token-here
Enter fullscreen mode Exit fullscreen mode

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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".

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()
  {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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)
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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 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.

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"]);
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  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()
    {
      // ...
    }
  }
Enter fullscreen mode Exit fullscreen mode

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 (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

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.

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,
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

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

namespace App\DataTransferObjects;

readonly class Geolocation
{
  public function __construct(
    public string $ip,
    public string $city,
    public string $country,
    public string $timezone,
    public string $internetProvider,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

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 🛰️

Top comments (1)

Collapse
 
dhruvjoshi9 profile image
Dhruv Joshi

That is a helpful blog! Thanks!