If you're a programmer, you've likely heard the term "contracts" thrown around in discussions about software development. But what exactly are contracts, and how do they relate to programming?
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 HoneywellApi differ from those of HaydonApi, but this is completely fine. Why? Because we cannot make all API providers and SDKs 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();
}
}
In order to utilize one of these providers, we need to create a controller along with a route that points to it. 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 tell Laravel that when the application looks for an implementation of Dvr, we'll return the Honeywell implementation.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Honeywell::class
);
}
public function boot(): void
{
//
}
}
Now we'll go back to the page we just visited and refresh it.
Play Honeywell DVR
Pretty cool, huh? Now let's visit the pause route (http://example.dev/api/dvr/pause):
Pause Honeywell DVR
Now let's say your manager comes in and states we're going to switch from Honeywell to Haydon. You'll only need to update the binding in AppServiceProvider:
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Haydon::class
);
Visit the play route and you get:
Play Haydon DVR
And the pause route:
Pause Haydon DVR
Let's now get even more advanced. Imagine the manager comes back and says "We actually want to 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 {}
Both extend the DvrController we originally created. Let's 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/honeywell', [\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 Honeywell play endpoint (http://example.test/api/dvr/play/honeywell) we run into a slight problem:
Play Haydon DVR
Laravel currently knows that when we ask for Dvr we return the Haydon implementation — so both controllers get the same one. We need to give the service container more context:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
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);
}
public function boot(): void
{
//
}
}
Now when we visit the endpoints for either Honeywell or Haydon we get the correct data returned. Winner.
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 that 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!
Source code is available on GitHub. Follow me on Twitter and YouTube.
Take the ideas in here from concept to working software in one focused day.
Book a build day →