Introduction
In this post, we will tackle about "Inversion of Control" principle. We will learn when to use it and how we can use it with Laravel using Contextual Binding in the Service Container. Although this topic assumes that you have the basic knowledge with Laravel and a general OOP concepts.
At the time of writing this, the current version for Laravel is on version 8.
What is Inversion of Control?
If you know what is Dependency Injection then it's basically just a reverse of it. For Dependency Injection the code typically depends directly on the class. With Inversion of Control (IoC) we invert it, the code does not depend on the class directly but only to the interface and we bind it in the service container. So when we Inject dependency to a certain class or Controller we call the Interface and not the class.
When to use Inversion of Control?
One must be aware and has to fully understand the scenario or what problem or certain feature to be implemented. There are a lot of problems and are a lot of ways to address it. But to choose the proper method of addressing the problem is a good way to approach it. Things does not have to be complex and should just be simple as possible.
Other than that, say for a given scenario, your client wants to support multiple payment providers for the project that you are building. At your first glance you might think that you'll have to create a lot of classes for it and have different implementation details on each one of them, but the problem arises that you might have to inject a lot these in your Controllers or that you might have to put up some conditional logics just to use the correct payment provider.
That can work but it may not the ideal implementation. Then that's the time we will make use of abstraction using Inversion of Control, we can then only inject a single dependency to a Controller or whatever class it requires and that leaves us lesser code to write. It keeps it simple, and that also means it'll be easier to maintain it in the long term.
Creating a PaymentInterface
Just for a quick, simple, and straight-forward example let's have a PaymentInterface
that requires 1 method to implement whichever a class implements this interface or abstract.
So let's just create a directory under app
directory of a fresh Laravel project, and call this directory as Interfaces
and have a file created named as PaymentInterface.php
and for its content, we have this
<?php
namespace App\Interfaces;
interface PaymentInterface
{
/**
* @param float $amount
* @return mixed
*/
public function pay(float $amount): string;
}
We only require classes to implement pay
method that takes an argument $amount
type hinted with float
and it returns a string
Creating payment services that implements PaymentInterface
Let's say the client wants to have at least 3 payment providers, let's just call it whatever we want in this case.
- Paypal
- SquarePay
- Stripe
We have at least 3 payment providers but with different implementation details because we might have to setup a few configuration for each of these third party APIs. Typically we want these configuration setup private and should not be exposed publicly, we only expose what is defined in the PaymentInterface
So let's define these services, we'll start off with Paypal
<?php
namespace App\Services;
use App\Interfaces\PaymentInterface;
class PaypalService implements PaymentInterface
{
public function pay(float $amount): string
{
return "From PaypalService $amount";
}
}
The PaypalService
implements the PaymentInterface
and the pay
method as well, and as defined from the abstract class or interface that pay
should return a string
then we'll return it with a type of string
so we basically just know right away what we should be returning.
For the SquarePayService
<?php
namespace App\Services;
use App\Interfaces\PaymentInterface;
class SquarePayService implements PaymentInterface
{
public function pay(float $amount): string
{
return "From SquarePayService $amount";
}
}
For the StripeService
<?php
namespace App\Services;
use App\Interfaces\PaymentInterface;
class StripeService implements PaymentInterface
{
public function pay(float $amount): string
{
return "From StripeService $amount";
}
}
Now we have those defined and implemented the PaymentInterface
we can move on to having to dynamically bind the interface with the corresponding class or payment provider.
Exposing the payment service providers to REST API
Now let's go over and create the controllers for each of these payment service providers that we defined. If you are coding along then open up your terminal and let's create these controllers using the artisan commands.
# Will create a directory called "PaymentProvider" and have
# the controller named as defined "PaypalController" for example
# Paypal
php artisan make:controller PaymentProvider/PaypalController
# Stripe
php artisan make:controller PaymentProvider/StripeController
# SquarePay
php artisan make:controller PaymentProvider/SquarePayController
Then we'll go over each of these controllers and we will inject PaymentInterface
into the constructor and pass it down into a private field.
PaypalController
<?php
namespace App\Http\Controllers\PaymentProvider;
use App\Http\Controllers\Controller;
use App\Contracts\PaymentInterface;
use Illuminate\Http\Request;
class PaypalController extends Controller
{
private $paymentService;
public function __construct(PaymentInterface $paymentService)
{
$this->paymentService = $paymentService;
}
public function index()
{
return response()->json([
'data' => $this->paymentService->pay(250.0),
]);
}
}
StripeController
<?php
namespace App\Http\Controllers\PaymentProvider;
use App\Http\Controllers\Controller;
use App\Contracts\PaymentInterface;
use Illuminate\Http\Request;
class StripeController extends Controller
{
private $paymentService;
public function __construct(PaymentInterface $paymentService)
{
$this->paymentService = $paymentService;
}
public function index()
{
return response()->json([
'data' => $this->paymentService->pay(10.0),
]);
}
}
SquarePayController
<?php
namespace App\Http\Controllers\PaymentProvider;
use App\Http\Controllers\Controller;
use App\Contracts\PaymentInterface;
use Illuminate\Http\Request;
class SquarePayController extends Controller
{
private $paymentService;
public function __construct(PaymentInterface $paymentService)
{
$this->paymentService = $paymentService;
}
public function index()
{
return response()->json([
'data' => $this->paymentService->pay(5.0),
]);
}
}
Once that's done, we can then expose these controllers as an endpoint to the REST API.
We define it in api.php
<?php
use App\Http\Controllers\PaymentProvider\PaypalController;
use App\Http\Controllers\PaymentProvider\SquarePayController;
use App\Http\Controllers\PaymentProvider\StripeController;
use Illuminate\Support\Facades\Route;
Route::get('pay-with-paypal', [PaypalController::class, 'index']);
Route::get('pay-with-stripe', [StripeController::class, 'index']);
Route::get('pay-with-squarepay', [SquarePayController::class, 'index']);
I just defined it with GET
request HTTP method just for simplicity of the tutorial. But for actual payment implementations, prefer to use POST
instead that it contains a payload data of payment information containing the amount, the account ID and any other sensitive information.
So there we defined it, you might be tempted to test it out with your HTTP client to see if it works. But actually it won't work yet as we didn't define it to act that way. So let's proceed to using Contextual Binding implementation.
Contextual Binding
We will be defining these bindings in the AppServiceProvider
or you can create a different service provider that is relevant to its implementation, it can be PaymentServiceProvider
(or any other name as you prefer) and have it registered in AppServiceProvider
. But just for the sake of simplicity for the tutorial I will just bind the interface and their corresponding services directly into the AppServiceProvider
.
<?php
namespace App\Providers;
use App\Http\Controllers\PaymentProvider\PaypalController;
use App\Http\Controllers\PaymentProvider\SquarePayController;
use App\Http\Controllers\PaymentProvider\StripeController;
use App\Interfaces\PaymentInterface;
use App\Services\PaypalService;
use App\Services\SquarePayService;
use App\Services\StripeService;
use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->when(PaypalController::class)
->needs(PaymentInterface::class)
->give(PaypalService::class);
$this->app->when(StripeController::class)
->needs(PaymentInterface::class)
->give(StripeService::class);
$this->app->when(SquarePayController::class)
->needs(PaymentInterface::class)
->give(SquarePayService::class);
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
}
On the register
method of the AppServiceProvider
is where we define the Contextual Binding, as you can see it checks on the Controller
using the when
method then the needs
is referring to the dependency of that particular controller, and the last method chaining is give
which is what we want to bind it to, these are the service classes that we defined PaypalService
, StripeService
, and SquarePayService
.
In other words, if the PaypalController
injects the PaymentInterface
the service container would know that its corresponding binding will result to PaypalService
and the same goes for StripeController
and SquarePayController
.
Now that we defined that in the service container, we can proceed to testing it out manually using our HTTP or just the browser to see if it worked.
Manually Testing in browser
It's just a simple test. So make sure you have an active server running via php artisan serve
and then just put up the endpoints that we defined in api.php
; We have the following:
/api/pay-with-paypal
/api/pay-with-stripe
/api/pay-with-squarepay
Now let's see each of these if it returns the actual implementation from each services that we defined above when creating them.
/api/pay-with-paypal
That is exactly what we wrote in PaypalService
to return a string from the pay
method and we even specified it with "PaypalService" just for us to indicate where the implementation is coming from. So using Contextual Binding works and it solves our problem!
/api/pay-with-stripe
And this is for Stripe.
/api/pay-with-squarepay
And our last payment provider that our client wants. We got it.
Conclusion
Now that we learned how to tackle multiple payment providers using the Inversion of Control (IoC) principle, we learned how to implement it with Laravel's service container using Contextual Binding, and we understand that we'll always go with the best approach to address a problem. Don't use Inversion of Control when it's not really relevant solution at all, no need to add complexity. Only use it when it seems the best solution.
I hope this was useful and that you have learned something new. Thanks for taking the time to read and have a good day!
Top comments (4)
That was great! You cleared all the fog from my brain like a hurricane. It was a real world example with clean, consistent syntax that shows the whole life cycle of a use case. Bravo and thank you!
You are excellent tutor, I greatly appreciate for boosting my career!!
Wow! Thank you and I am glad my tutorials were impactful 🙏
Goode explanation! thank you very much