Binding API Connection Interfaces To Implementations in The Laravel Container
Arie Visser • February 1, 2020
laravel phpThe Laravel service container is called "a powerful tool for managing class dependencies and performing dependency injection".
In this post, we will look at a possible use case of the service container when communicating with multiple HTTP APIs.
Many features in Laravel make use of the service container. A very common example is dependency injection. Imagine a controller that needs a service to interact with GitHub projects:
<?php
use App\Services\Api\Git\GitHubProjectApi;
final class GitProjectController
{
/** @var \App\Services\Api\Git\GitHubProjectApi */
private $gitHubProjectApi;
public function __construct(GitHubProjectApi $gitHubProjectApi)
{
$this->gitHubProjectApi = $gitHubProjectApi;
}
}
The GitHub project api service is injected into this controller by adding it as an argument to the constructor. The Laravel service container will automatically resolve this service, since it does not depend on an interface.
However, when a service communicates with an external system, your services may depend on interfaces. This will give you some nice possibilities in combination with the Laravel service container.
Imagine you would only need to show GitHub projects in some cases, and projects from GitLab in other cases, depending on the settings of the user or some other factor.
It would be very cumbersome to inject, or instantiate, either the GitHubProjectApi
service or the GitLabProjectApi
service in the controller, depending on the context.
This is where binding interfaces to implementations comes in view to make thing easier.
First, we will create the GitProjectApi
interface where the implementation will depend on:
<?php
namespace App\Contracts\Services\Api\Git;
interface GitProjectApi
{
public function index(): array;
}
To keep things simple, each service that depends on the GitProjectApi
interface, only has to implement the index()
method that will show the projects from either GitHub or GitLab.
Instead of injecting, or instantiating the services themselves, we will inject the interface in the controller.
<?php
namespace App\Http\Controllers;
use App\Contracts\Services\Api\Git\GitProjectApi;
use Illuminate\Http\JsonResponse;
final class GitProjectController
{
/** @var \App\Contracts\Services\Api\Git\GitProjectApi */
private $gitProjectApi;
public function __construct(GitProjectApi $gitProjectApi)
{
$this->gitProjectApi = $gitProjectApi;
}
public function index(): JsonResponse
{
return response()->json($this->gitProjectApi->index());
}
}
In our project, there are two implementations of this interface, the GitHubProjectApi
service and the GitLabProjectApi
service:
<?php
namespace App\Services\Api\GitHub;
use App\Contracts\Services\Api\Git\GitProjectApi;
use App\Contracts\Support\HttpClientInterface;
use App\DataTransferObjects\GitProjectDataCollection;
use App\DataTransferObjects\GitProjectData;
final class GitHubProjectApi implements GitProjectApi
{
/** @var \App\Contracts\Support\HttpClientInterface */
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function index(): array
{
$projects = $this->client->get("https://api.github.com/repositories");
return GitProjectDataCollection::create($projects, function ($project) {
return GitProjectData::fromGitHubProject($project);
})->items();
}
}
<?php
namespace App\Services\Api\GitLab;
use App\Contracts\Services\Api\Git\GitProjectApi;
use App\Contracts\Support\HttpClientInterface;
use App\DataTransferObjects\GitProjectDataCollection;
use App\DataTransferObjects\GitProjectData;
final class GitLabProjectApi implements GitProjectApi
{
/** @var \App\Contracts\Support\HttpClientInterface */
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function index(): array
{
$projects = $this->client->get("https://gitlab.com/api/v4/projects");
return GitProjectDataCollection::create($projects, function ($project) {
return GitProjectData::fromGitLabProject($project);
})->items();
}
}
The GitHubProjectApi
implementation makes a call to GitHub's API to fetch projects, and the GitLabProjectApi
implementation makes the call to GitLab's API.
Both implementations will convert the data to a generic collection of projects by using data transfer objects.
Now we only have to specify which implementation of the interface will be resolved in the service container, in order that we can use it in our controller and show either projects from GitHub or GitLab. This can be done in the register method of a service provider:
public function register()
{
$this->app->bind(
'App\Contracts\Services\Api\Git\GitProjectApi',
'App\Services\Api\GitHub\GitHubProjectApi'
);
}
We tell the Laravel service container to bind the GitProjectApi
interface to the GitHubProjectApi
implementation.
The index
method that is called in the GitProjectController
will now return projects from GitHub.
When we need to swap the implementation to GitLab projects, we can bind the interface to the GitLabProjectApi
service:
public function register()
{
$this->app->bind(
'App\Contracts\Services\Api\Git\GitProjectApi',
'App\Services\Api\GitHub\GitLabProjectApi'
);
}
The index()
method that is called in the controller, will now return projects from GitLab. Of course, it is possible to specify to which implementation the interface wil be bound based on a user setting, the class where it is resolved, or some other factor.
Some good examples on how to configure this can be found in the documentation.
The full code of this example project can be found in this repository.