In this new article on building web applications using PHP Laravel, I'll introduce the dependency injection principle, also known as the Inversion of Control (IoC) container, and explore how PHP Laravel uses it internally and how it offers this concept to application developers to use and make their code clear and decoupled.

Dependency Injection

Dependency injection is a programming technique that allows you to decouple your software components from each other.

When you're building large-scale applications, you'll most often encounter cases when a class requires another service class to function correctly. When you allow the class to create its own dependencies, you're imposing coupling between your classes and making them tightly dependent on each other.

The result of this tight coupling leads to the following consequences:

  • Code that's harder to test
  • Code that's harder to maintain

Here's an example showing how a class instantiates its own dependencies.

class InvoiceController extends Controller
{
    protected PaymentService $paymentService;

    public function __construct()
    {
        $this->paymentService = new PaymentService();
    }
}

That's why you have the need for IoC container/dependency injection to invert the flow of object instantiation. Instead of a class instantiating and managing its own dependencies, the IoC Container instead prepares and injects those dependencies into the classes that need them.

class InvoiceService
{
    public function __construct(
      protected PaymentService $paymentService) { }
}

The classes have the option to either accept a concrete implementation or an interface that gets replaced with the concrete implementation at runtime.

Dependency injection belongs to the SOLID principles (https://en.wikipedia.org/wiki/SOLID) that aim to increase the reusability of your code. It satisfies this goal by decoupling the process of creating an object from its use. Because of this, you can replace dependencies without altering the class that uses them. It also lessens the likelihood that you need to modify a class just because one of its dependent components changed.

There are different types of dependency injection, like setter injection, constructor injection, method injection, and other types. I'll be focusing on constructor injection throughout this article.

A dependency is just another object that your class needs to function, so if you have a model class that fetches data from a database object, you can say that the model class has a dependency on that database object.

The Four Major Roles in Dependency Injection

To implement dependency injection into your code, there are four main roles you need to know about:

  • The service you want to use, such as a payment service or an email service.
  • The client that uses the service. This is the class that you'll inject the service into.
  • An interface that's used by the client and implemented by the service. This is optional. You can inject a concrete class without an interface. But by injecting an interface, you get the chance to swap out concrete implementations at runtime.
  • The injector creates a service instance and injects it into the client. This is usually known as a dependent injection container. Its responsibility is to manage object instantiation and keep track of their dependencies.

The above four roles are mandatory for the successful implementation and use of dependency injection in your application. The fourth role, the injector, is something you don't need to worry about. Typically, almost every back-end framework provides you with an injector, or dependency injection, container.

The injector is the brains behind the concept of dependency injection. For instance, a framework gives you the means to register a dependency. When the framework detects a class that requires a registered dependency, it uses its injector to instantiate the dependency and inject it into the class requiring it.

Now that you have an idea of what dependency injection is, let's see how PHP Laravel implements it.

Dependency injection simply means the dependency is pushed into the class from the outside. This means that you shouldn't instantiate dependencies using the new operator from inside of the class, but instead, take it as a constructor parameter or via a setter.

How Laravel Implements Dependency Injection

The IoC Container sits at the heart of the PHP Laravel framework. Laravel comes with a Service Container that's responsible for managing dependencies in the application and injecting them wherever needed.

The container gives you the opportunity to define bindings to classes and interfaces. At the same time, it has a unique feature known as Zero Configuration Resolution that makes the container resolve dependencies without even registering them. It can do so provided the dependency has no other dependencies or has some dependencies that the container knows how to instantiate already.

Service container is a Laravel service that allows you to tell Laravel how an object or class needs to be constructed and then Laravel can figure it out from there.

Simple Dependency Injection

Let's look at an example to show you how dependency injection works in a Laravel application. Listing 1 Shows the entire source code for this example.

Listing 1: Simple Dependency Injection example

public class PaymentService
{
    public function doPayment ()
    {
        // ...
    }
}

class PaymentController extends Controller
{
    public function __construct (protected PaymentService $paymentService)
    {

    }

    public function payment ()
    {
        // $paymentService
    }
}

First, define a PaymentService that contains a single method named doPayment(). Eventually, you'll place the code responsible for performing the payment after a checkout or purchase.

Next, inside the PaymentController, define a constructor that accepts as an input parameter a PaymentService object. The payment() action method uses the $paymentService object to perform the payment.

When you send a request to the payment() method, Laravel does many tasks behind the scenes. One of those tasks is to instantiate the PaymentController class. While instantiating it, it observes that the constructor requires a dependency.

Laravel uses its Service Container to look up dependencies. For now, you haven't guided Laravel regarding how to instantiate the PaymentService dependency. However, Laravel is smart enough to resolve this dependency and inject it into the PaymentController constructor. The PaymentService has no other dependencies, so it's easy for Laravel to instantiate it and make an object out of it.

Once Laravel instantiates the PaymentService class, it then instantiates a new object out of the PaymentController using its constructor and passing the required dependency.

You've seen how Laravel Dependency Injection works for simple cases where a class has a dependency on another class that has no other dependencies. What will happen when the PaymentService itself has a dependency?

Adding Dependencies to Other Dependencies

In this section, you'll explore what happens when a dependency has a required dependency. How does Laravel perform?

Let's see with another example, shown in Listing 2.

Listing 2: Dependency Injection with dependencies example

public class PaymentService
{
    public function __construct (protected string $secretKey){ }

    public function doPayment ()
    {
        // ...
    }
}

class PaymentController extends Controller
{
    public function __construct(protected PaymentService $paymentService)
    {

    }

    public function payment ()
    {
        // $paymentService
    }
}

The PaymentService class now defines a constructor that accepts a dependency of type string named $secretKey. In this case, Laravel won't be able to instantiate the PaymentService on its own without some help from your side. The reason? Laravel cannot predict or provide the new dependency.

You must provide Laravel with additional instructions on how to instantiate the PaymentService.

Inside the app\Providers\AppServiceProvider.php file, you register a binding to tell Laravel how to instantiate a new object of the PaymentService.

public function register()p
{
    $this->app()->bind(PaymentService::class, function() {
        return new PayentService('123456');
    }
);

The app() method call returns an instance of the Illuminate\Foundation\Application class. This class, in turn, extends the Illuminate\Container\Container class. Hence, the app() method lets you work with the Laravel Service Container directly.

The Container defines the bind() method, which lets you define a new binding inside the Service Container.

The bind() method accepts as a first input parameter the name or type of the dependency you want to define a binding for. The second argument is a PHP Closure (https://www.php.net/manual/en/class.closure.php). The code instantiates and returns a new instance of the PaymentService by providing the correct secret key required by the service.

When it's time for Laravel to inject the PaymentService into the PaymentController, it checks to see whether you've defined a binding for this dependency. If it finds one, it executes and runs the closure to return an instance of this dependency.

In summary, you have the chance to not only use the Service Container to instantiate and inject dependencies, but also instruct it on how to instantiate dependencies.

It gets a bit trickier when you have multiple concrete implementations of the same service. Let's see how you instruct Laravel Service Container to handle this complexity.

A Dependency with Multiple Concrete Implementations

Oftentimes, you need to connect to multiple payment gateways at once. Depending on the user's preference or other criteria in your application, you might need to have multiple concrete implementations of a service and be ready to either use one concrete implementation at a time, or both together depending on some logic.

Listing 3 shows the entire code for this example.

Listing 3: Multiple concrete implementations

interface PaymentGateway
{
    public function doPayment ();
}

classPaypalGateway implements PaymentGateway
{
    public function __construct (protected string $secretKey) { }

    public function doPayment ()
    {

    }
}

classStripeGateway implements PaymentGateway
{
    public function __construct (protected string $secretKey) { }

    public function doPayment ()
    {

    }
}

class PaymentController
{
    public function __construct (
        protected PaymentGateway $paymentGateway) { }

    public function __invoke (Request $request)
    {
        // ...
    }
}

$this->app()->bind(PaymentServiceContract::class, function () {
    if (request()->gateway() === 'stripe') {
        return new StripeGateway('123');
    }

    return new PaypalGateway('123');
});

You start by defining the PaymentGateway interface (https://www.zend.com/blog/what-interface-php). This interface dictates what methods need to exist and be implemented on the different payment gateways available in the application.

Next, define two new payment services: PaypalGateway and StripeGateway. Each service implements the PaymentGateway interface and provides a different concrete implementation for its corresponding payment gateway. The PaypalGateway connects to the Paypal service and the latter connects to the Stripe service.

The PaymentController defines the PaymentGateway interface as a dependency. In this case, the controller is requesting an interface instead of an actual concrete implementation. What happens at run-time is that, based on how you configure the Service Container, Laravel injects a certain concrete implementation into this controller to replace the interface instance.

Finally, you guide the Laravel Service Container to return a concrete instance of the PaymentGateway based on a request parameter named gateway. If it has a value of stripe, then you return the StripeGateway, otherwise, you return the PaypalGateway. This is a simple implementation to illustrate the ID. You may extend it further in the way it fits the needs of your application.

Using interfaces as dependencies lets you swap implementations at run-time with no effort. Also, this way, you don't have to change the entire source in case the PayPal or Stripe service has changed. In the future, you might need to add an additional payment service, and this can be easily done by adding a new implementation of the PaymentGateway interface and registering it with the bind() method of the Service Container in Laravel.

Now that I've covered several use cases of the Laravel Service Container and different ways of providing dependency injection in a Laravel application, let's see how easy it is to test mock dependencies, especially when using them as interfaces.

Testing Code with Dependencies

Dependency injection has a major side effect of writing cleaner and better code. When coding against interfaces, you can swap an implementation while testing and be able to isolate the class under test without testing its dependencies. Assuming that the dependencies are well tested separately, you can simply mock them; that is, provide a dummy implementation for their methods, and hence get rid of one burden and focus on the functionality of the main class.

Laravel offers PHPUnit testing straight out of the box. Recently, a new testing library appeared, called Pest PHP. This library is internally based on PHPUnit. However, it offers a more expressive and easier experience to test and expect/assert testing results.

Mocking an object means replacing or shadowing the actual implementation of a class with another dummy or mocked class that has almost no functionality. The only thing in common between an object and its mock is the blueprint of the methods and functions. A mock object can be used in place of the original object, especially when writing tests. However, you, as a developer, can control the behavior of the mock object by deciding what methods are to be called, what parameters to pass to those methods, and what return values those methods can return.

In summary, mocking objects has the following benefits:

  • A class under test can be isolated from its dependencies.
  • The tests run quickly, especially if you're mocking a class that interacts with the database or I/O.

Irrespective of which testing library you are using; the ultimate result is the same. By adding a mock object, you're isolating the class under test from its dependencies and assuming that the dependencies' method calls are all working as expected. This, of course, assumes that you've written enough test cases to cover the correctness of those dependency objects.

To create a mock object when using Pest PHP, you need to install a composer plug-in. Follow this link to learn how to set up your environment https://pestphp.com/docs/plugins/mock.

Here's how to create and use mock objects in PHPUnit (https://phpunit.de/manual/6.5/en/test-doubles.html).

Let's see an example of how to mock a dependency using the PHPUnit library.

To create a new feature test in Laravel, run the following command:

php artisan make:test PaymentTest

The command creates a new PaymentTest.php file inside the tests\Feature folder. Listing 4 shows the entire test case code.

Listing 4: Testing code with dependencies

<?php

namespace Tests\Feature  ;

// use Illuminate\Foundation\Testing\RefreshDatabase;

use App\Payments\PaymentGateway  ;
use App\Payments\PaypalGateway  ;
use Mockery  ;
use Tests\TestCase  ;

class PaymentTest extends TestCase
{
    public function test_payment_returns_a_successful_response ()
    {
        // Create a mock
        $mock = Mockery::mock(PaypalGateway::class)->makePartial();

        // Set expectations
        $mock->shouldReceive('doPayment')
             ->once()->andReturnNull();

        // Add this mock to the service container
        // to take the service class' place.
        app()->instance(PaymentGateway::class, $mock);

        // Run endpoint
        $this->get('/payment')->assertStatus(200);
    }
}

For this test case, I'll use the example on implementing dependency injection using interfaces.

Using dependency injection simplifies the creation of test doubles (often referred to as “mocks”). If you pass dependencies to classes, it's straightforward to pass in a test double implementation.

It is impossible to generate test doubles for dependencies that are hard-coded.

Start by:

  • Creating a new mock for the PaypalGateway class.
  • Set the expectations for the mock object. For instance, you instruct the mock object that the doPayment() method will be executed once and will return null. This is where you control what to pass to the methods and what to return. You have full control over what's being passed and returned from methods.
  • You replace the binding for the PaymentGateway interface with the mock instance object. This replaces any previous bindings set inside the AppServiceProvider (as shown previously in this article). While running the tests, you want to replace the Service Container mappings to use your mock object.
  • Finally, you send a GET request to the /payment endpoint.

Define this new endpoint inside the routes\web.php file as follows:

Route::get('/payment', PaymentController::class);

The PaymentController should be defined as an invokable controller, as shown in Listing 5.

Listing 5: PaymentController class

class PaymentController extends Controller
{
    public function __construct (
        protected PaymentGateway $paymentGateway)
    {
    }

    public function __invoke (Request $request)
    {
        $this->paymentGateway->doPayment();
    }
}

The PaymentController has a dependency on the PaymentGateway interface. When running the test, the $paymentGateway parameter gets replaced with the PaypalGateway mock object. By using a mock, nothing has changed regarding the way PaymentController calls methods on the PaymentGateway interface. A mock object guarantees that the code continues to work with the same blueprint irrespective of the actual concrete implementation.

Now that you know the benefits of dependency injection and the Laravel Service Container, let's explore what binding options the service container offers.

Service Container Bindings

The Laravel Service Container offers several ways to bind and register dependencies. You've seen so far how to use the app()->bind() method to bind a dependency in the application. The Service Container offers other ways of binding dependencies. The Laravel documentation does a great job of detailing them all (https://laravel.com/docs/9.x/container#binding). However, I'd like to shed light on three major binding techniques that you might find yourself using most of the time.

Manual Binding

You've already seen how manual binding happens using the bind() method. Every time Laravel requests a dependency that is registered with the bind() method, it asks the Service Container to create and return a new instance of the registered dependency. In some cases, this might be inefficient, especially when you're creating expensive resource classes.

Here's an example of using the bind() method:

$this->app()->bind(PaymentService::class, function() {
    return new PaymentService('123456');
});

For instance, inside the Closure function, you can retrieve values from the application configuration. If a dependency requires another dependency, you can even request a dependency from the Service Container while inside the Closure function.

Let's see how singleton binding is different from manual binding.

Singleton Binding

A singleton binding makes sure, when a dependency is registered as a singleton, to have one and only one instance per a request/response lifecycle. In contrast to the manual binding, every request to retrieve a dependency from the Service Container results in the creation of a new instance of that dependency. With singleton binding, a single instance exists. This is more useful when you have services or classes that are too expensive to keep creating them when they're requested.

Here's an example of using the singleton() method:

$this->app()->singleton(PaymentService::class, function() {
    return new PaymentService('123456');
});

Every time there's a need for an instance of the PaymentService, the same exact instance object will be returned during a request/response lifecycle.

Instance Binding

Instance binding is like singleton binding, except that you create a new instance of a dependency and instruct the Laravel Service Container to always return this instance.

Here's an example of using the instance() method:

$paymentService = new PaymentService('123456');
$this->app()->instance(PaymentService::class, $paymentService);

The main difference between instance binding and the other two forms of binding is that you always create a new instance and add it to the Service Container. In the case of manual or singleton binding, only when the application requests an instance of a dependency does Closure function execute and return an instance. Think of it as an early binding versus late binding thing.

As I mentioned, there are other ways of extending the Service Container and registering dependencies. You can read all the details at the Laravel documentation website.

Resolving Service Container Dependencies

The Service Container offers several ways to instantiate dependency instances. I'll list a few of these ways that I generally use:

app(PaymentService::class);

You can simply use the app() method to resolve and instantiate a dependency.

app()->make(PaymentService::class);

Another method for instantiating objects is to make use of the make() method defined on the app() object.

resolve(PaymentService::class);

The resolve() function is a helper function that instantiates an object based on the mapping name passed to it. In this case, you're requesting from the Service Container to instantiate an instance of the PaymentService class.

Finally, dependencies can also be instantiated once injected into the classes inside their constructors. You use this type of resolution most of the time while developing with Laravel.

Conclusion

PHP Laravel is a rich PHP framework with numerous topics to discuss and learn. In this series, I'm trying to cover as many Laravel features and concepts as possible to help you build better applications using this framework. In the coming articles, I'll continue to explore more of Laravel's features one step at a time.

Happy Laraveling!