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:
1namespace App\Contracts;2 3interface Dvr4{5 public function play();6 7 public function pause();8}
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.
app/Services/HoneywellApi.php
1namespace App\Services;23class HoneywellApi4{5 public function pressPlay()6 {7 return 'Play Honeywell DVR';8 }910 public function pressPause()11 {12 return 'Pause Honeywell DVR';13 }14}
app/Services/HaydonApi.php
1namespace App\Services;23class HaydonApi4{5 public function play()6 {7 return 'Play Haydon DVR';8 }910 public function pause()11 {12 return 'Pause Haydon DVR';13 }14}
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.
app/Implementations/Honeywell.php
1namespace App\Implementations;23use App\Contracts\Dvr;4use App\Services\HoneywellApi;56class Honeywell implements Dvr7{8 public function __construct(protected HoneywellApi $api) {}910 public function play()11 {12 return $this->api->pressPlay();13 }1415 public function pause()16 {17 return $this->api->pressPause();18 }19}
app/Implementations/Haydon.php
1namespace App\Implementations;23use App\Contracts\Dvr;4use App\Services\HaydonApi;56class Haydon implements Dvr7{8 public function __construct(protected HaydonApi $api){}910 public function play()11 {12 return $this->api->play();13 }1415 public function pause()16 {17 return $this->api->pause();18 }19}
In 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)
app/Http/Controllers/DvrController.php
1namespace App\Http\Controllers;23use App\Contracts\Dvr;45class DvrController extends Controller6{7 public function __construct(protected Dvr $dvr){}89 public function play()10 {11 return $this->dvr->play();12 }1314 public function pause()15 {16 return $this->dvr->pause();17 }18}
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.
routes/api.php
1use Illuminate\Support\Facades\Route;23Route::get(‘dvr/play’, [\App\Http\Controllers\DvrController::class, ‘play’]);45Route::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:
1Illuminate\Contracts\Container\BindingResolutionException23Target [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.
app/Providers/AppServiceProvider.php
1namespace App\Providers;23use Illuminate\Support\ServiceProvider;45class AppServiceProvider extends ServiceProvider6{7 /**8 * Register any application services.9 */10 public function register(): void11 {12 $this->app->bind(13 \App\Contracts\Dvr::class,14 \App\Implementations\Honeywell::class15 );16 }1718 /**19 * Bootstrap any application services.20 */21 public function boot(): void22 {23 //24 }25}
No we’ll go back to the page we just visited and we’ll refresh the page.
1Play Honeywell DVR
Pretty cool, huh? Now let’s visit the pause route (http://example.dev/api/dvr/pause
) which returns:
1Pause 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:
app/Providers/AppServiceProvider.php
1namespace App\Providers;23use Illuminate\Support\ServiceProvider;45class AppServiceProvider extends ServiceProvider6{7 /**8 * Register any application services.9 */10 public function register(): void11 {12 $this->app->bind(13 \App\Contracts\Dvr::class,14 \App\Implementations\Haydon::class15 );16 }1718 /**19 * Bootstrap any application services.20 */21 public function boot(): void22 {23 //24 }25}
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:
1Play Haydon DVR
Now the pause route (http://example.dev/api/dvr/pause
) which returns:
1Pause 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.
app/Http/Controllers/HaydonController.php
1namespace App\Http\Controllers;23class HaydonController extends DvrController {}
app/Http/Controllers/HoneywellController
1namespace App\Http\Controllers;23class 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:
routes/api.php
1use Illuminate\Support\Facades\Route;23Route::get('dvr/play', [\App\Http\Controllers\DvrController::class, 'play']);4Route::get('dvr/pause', [\App\Http\Controllers\DvrController::class, 'pause']);56Route::get('dvr/play/honeywell', [\App\Http\Controllers\HoneywellController::class, 'play']);7Route::get('dvr/pause/honewell', [\App\Http\Controllers\HoneywellController::class, 'pause']);89Route::get('dvr/play/haydon', [\App\Http\Controllers\HaydonController::class, 'play']);10Route::get('dvr/pause/haydon', [\App\Http\Controllers\HaydonController::class, 'pause']);
When we visit the play Honeywell endpoint (http://laravelcontractsandimplementations.test/api/dvr/play/honeywell
) we run into a slight problem:
1Play 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:
app/Providers/AppServiceProvider.php
1namespace App\Providers;23use Illuminate\Support\ServiceProvider;45class AppServiceProvider extends ServiceProvider6{7 /**8 * Register any application services.9 */10 public function register(): void11 {12 $this->app->bind(13 \App\Contracts\Dvr::class,14 \App\Implementations\Haydon::class15 );1617 $this->app18 ->when(\App\Http\Controllers\HoneywellController::class)19 ->needs(\App\Contracts\Dvr::class)20 ->give(\App\Implementations\Honeywell::class);2122 $this->app23 ->when(\App\Http\Controllers\HaydonController::class)24 ->needs(\App\Contracts\Dvr::class)25 ->give(\App\Implementations\Haydon::class);26 }2728 /**29 * Bootstrap any application services.30 */31 public function boot(): void32 {33 //34 }35}
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!