DEV Community

loading...

Laravel Inversion of Control Implementation using Contextual Binding

Carlo Miguel Dy
Software Engineer
・7 min read

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;
} 
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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()
    {
        //
    }
} 
Enter fullscreen mode Exit fullscreen mode

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

image

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

image

And this is for Stripe.

/api/pay-with-squarepay

image

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!

Full Source Code

Discussion (0)