Contracts are an advanced programming topic that is common to many programming languages. In technical terms, contracts are called “interfaces,” but for the purposes of this article, we will use the term “contracts” to make things simpler.
A contract is like a business agreement in that it is an agreement between parties. Contracts usually have terms, which are the conditions of the agreement that both parties agree to follow. When we take this and translate it into code, we have something like this:
<?php
namespace App\Contracts;
interface Dvr
{
public function play();
public function pause();
}
In this contract, we are agreeing to do business with the DVR and the terms (methods) of the contract which we must abide to play()
and pause()
the DVR. As you can imagine, there are multiple companies that provide DVR services. Here in the United States, two of the largest DVR providers are Honeywell and Haydon.
Let’s now create an API service for both Honeywell and Haydon.
<?php
namespace App\Services;
class HoneywellApi
{
public function pressPlay()
{
return 'Play Honeywell DVR';
}
public function pressPause()
{
return 'Pause Honeywell DVR';
}
}
<?php
namespace App\Services;
class HaydonApi
{
public function play()
{
return 'Play Haydon DVR';
}
public function pause()
{
return 'Pause Haydon DVR';
}
}
Something you’ve likely noticed is that the method names used by the HoneywellApi
differ from that of HaydonApi
, but this is completely fine. Why? Because we cannot make all API providers and SDKs to be designed the same. We’ll now create an implementation to represent each API while also abiding by the contract.
<?php
namespace App\Implementations;
use App\Contracts\Dvr;
use App\Services\HoneywellApi;
class Honeywell implements Dvr
{
public function __construct(protected HoneywellApi $api){}
public function play()
{
return $this->api->pressPlay();
}
public function pause()
{
return $this->api->pressPause();
}
}
<?php
namespace App\Implementations;
use App\Contracts\Dvr;
use App\Services\HaydonApi;
class Haydon implements Dvr
{
public function __construct(protected HaydonApi $api){}
public function play()
{
return $this->api->play();
}
public function pause()
{
return $this->api->pause();
}
}
n order to utilize one of these providers, we need to create a controller along with a route that points to the controller. As you’re about to see, we will inject the /interface/ into the constructor. This is a critical piece as it is what allows us to swap between API providers should we ever need to. (Later, we’ll tap into Laravel’s service container to bring it all together)
<?php
namespace App\Http\Controllers;
use App\Contracts\Dvr;
class DvrController extends Controller
{
public function __construct(protected Dvr $dvr){}
public function play()
{
return $this->dvr->play();
}
public function pause()
{
return $this->dvr->pause();
}
}
The controller now only cares about playing or pausing the DVR, it doesn’t care (nor should it) about who will be providing the underlying service.
Moving along we’ll create a route for each controller method in our api.php
routes file.
<?php
use Illuminate\Support\Facades\Route;
Route::get(‘dvr/play’, [\App\Http\Controllers\DvrController::class, ‘play’]);
Route::get(‘dvr/pause’,[\App\Http\Controllers\DvrController::class, ‘pause’]);
When we fire up the browser, and go to our play route (http://example.dev/api/dvr/play
) we run into the following:
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Contracts\Dvr] is not instantiable while building [App\Http\Controllers\DvrController].
What is happening is that Laravel sees the Dvr contract, so it is attempting to resolve it to an implementation, but cannot locate one, which produces the error.
Now let’s have a little fun. We will go into our AppServiceProvider
and we will tell Laravel that wen the the application looks for an implementation of Dvr
that we’ll return it the Honeywell
implementation.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Honeywell::class
);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
No we’ll go back to the page we just visited and we’ll refresh the page.
Play Honeywell DVR
Pretty cool, huh? Now let’s visit the pause route (http://example.dev/api/dvr/pause
) which returns:
Pause Honeywell DVR
Now let’s say your manager comes in and states we’re going to switch from Honeywell to Haydon, you’ll now just need to update the AppServiceProvider
to:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Haydon::class
);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
We’ll now go back and visit the play route (Now let’s visit the play route (http://example.dev/api/dvr/play
) which returns:
Play Haydon DVR
Now the pause route (http://example.dev/api/dvr/pause
) which returns:
Pause Haydon DVR
Let’s now get even more advanced. Imagine the Manager comes back in and says “So we’ve talked about it, we actually want be able to use both Honeywell and Haydon. Can you make that happen?” This is where you say “I’ve got you covered, boss!”
Given we’ll now be offering multiple providers let’s take a DRY approach to our controllers.
<?php
namespace App\Http\Controllers;
class HaydonController extends DvrController {}
<?php
namespace App\Http\Controllers;
class HoneywellController extends DvrController {}
As you’ll see, these both extends the DvrController we originally created.
Let’s now update our api.php
routes file to correspond to them:
<?php
use Illuminate\Support\Facades\Route;
Route::get('dvr/play', [\App\Http\Controllers\DvrController::class, 'play']);
Route::get('dvr/pause', [\App\Http\Controllers\DvrController::class, 'pause']);
Route::get('dvr/play/honeywell', [\App\Http\Controllers\HoneywellController::class, 'play']);
Route::get('dvr/pause/honewell', [\App\Http\Controllers\HoneywellController::class, 'pause']);
Route::get('dvr/play/haydon', [\App\Http\Controllers\HaydonController::class, 'play']);
Route::get('dvr/pause/haydon', [\App\Http\Controllers\HaydonController::class, 'pause']);
When we visit the play Honeywell endpoint (http://example.test/api/dvr/play/honeywell
) we run into a slight problem:
Play Haydon DVR
Currently, Laravel knows that when we ask for Dvr
we return the Haydon
implementation.
So how to we solve this? We’ll have to now provide the AppServiceProvider
with more context:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Haydon::class
);
$this->app
->when(\App\Http\Controllers\HoneywellController::class)
->needs(\App\Contracts\Dvr::class)
->give(\App\Implementations\Honeywell::class);
$this->app
->when(\App\Http\Controllers\HaydonController::class)
->needs(\App\Contracts\Dvr::class)
->give(\App\Implementations\Haydon::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
Now when we visit the endpoints for either Honeywell or Haydon we now get the proper data returned. #winner
In conclusion, contracts and implementations are powerful tools in Laravel that allow you to define a standard interface and write code that can adapt to different implementations. By using contracts, you can build more modular, scalable, and maintainable code, which can be easily updated or switched out with different implementations if needed. I hope this article has been informative and helpful in your Laravel journey, and I encourage you to continue exploring and experimenting with this fascinating framework!