×
×

Dependency Injection

If you are starting to learn about Drupal 8, you must have come across a term called "Dependency Injection". If you are wondering what it means, then this is the post you should read. Through examples, you will learn why dependency injection is useful for decoupling the code as well as unit testing effectively.

In the post on PHP Inheritance, we created HondaAccord and ChevyRam classes, both of which extend the Vehicle class. Vehicle has getWarrantyLeft(), drive() and getMilesDriven() methods. getWarrantyLeft() method is being overridden by ChevyRam while HondaAccord has a trunk and implements putInTrunk() and takeFromTrunk() methods. Vehicle also has color, warranty and milesDriven properties.

Let's also assume that there is a Person class and each person owns a vehicle. We are initializing this vehicle in the constructor of the Person class. Person has methods travel() and getDistanceTraveled(). travel() method uses Vehicle's drive() method and getDistanceTraveled() method uses the Vehicle's getMilesDriven() method. Here is how Vehicle.php and Person.php files look:

<?php

/**
 * Vehicle class.
 */
class Vehicle {

  // Color of the vehicle.
  public $color;

  // Miles driven.
  private $milesDriven;

  // Warranty available.
  public $warranty;

  /**
   * Default constructor.
   *
   * @param string $color
   *  Color of the vehicle.
   * @param int $milesDriven
   *  Number of miles driven already.
   * @param int $warrantyProvided
   *   Number of miles of warranty provided with the vehicle.
   */
  function __construct($color, $milesDriven = 0, $warranty = 100000) {
    $this->color = $color;
    $this->milesDriven = $milesDriven;
    $this->warranty = $warranty;
  }

  /**
   * Returns miles of warranty left on the vehicle.
   *
   * @return int
   *   Miles of warrnty left on the vehicle.
   */
  public function getWarrantyLeft() {
    if ($this->warranty - $this->getMilesDriven() > 0) {
      return ($this->warranty - $this->getMilesDriven());
    }

    return 0;
  }

  /**
   * Drive the vehicle. This will add to miles driven.
   *
   * @param int $miles
   *   Number of miles driven in the current trip.
   */
  public function drive($miles) {
    if ($miles > 0) {
      $this->milesDriven += $miles;
    }
  }

  /**
   * Returns the number of miles driven in total.
   *
   * @param int
   *   Total number of miles driven.
   */
  public function getMilesDriven() {
    return $this->milesDriven;
  }
}

/**
 * HondaAccord class.
 */
class HondaAccord extends Vehicle {

  // Stuff in the trunk.
  public $stuff;

  /**
   * Put stuff in trunk.
   *
   * @param mixed $stuff
   *   Stuff to be put in the trunk.
   */
  public function putInTrunk($stuff) {
    $this->stuff = $stuff;
  }

  /**
   * Take stuff out from the trunk.
   *
   * @return mixed
   *   Stuff returned from the trunk.
   */
  public function TakeFromTrunk() {
    $stuff = $this->stuff;
    unset($this->stuff);
    return $stuff;
  }
}

/**
 * ChevyRam class.
 */
class ChevyRam extends Vehicle {

  /**
   * Returns miles of warranty left on the vehicle.
   *
   * @return int
   *   Miles of warrnty left on the vehicle.
   */
  public function getWarrantyLeft() {
    if ($this->warranty - $this->getMilesDriven() > 0) {
      return (2 * ($this->warranty - $this->getMilesDriven()));
    }

    return 0;
  }

}

?>
<?php

require_once 'Vehicle.php';

/**
 * Person class.
 */
class Person {

  // Vehicle
  public $vehicle;

  /**
   * Default constructor.
   */
  function __construct() {
    $this->vehicle = new HondaAccord('red');
  }

  /**
   * Let the person travel in his vehicle.
   *
   * param int $miles
   *   Number of miles that the person travels.
   */
  public function travel($miles) {
    $this->vehicle->drive($miles);
  }

  /**
   * Returns the number of miles that the person has traveled.
   *
   * @return int
   *   Number of miles that the person has traveled.
   */
  public function getDistanceTraveled() {
    return $this->vehicle->getMilesDriven();
  }
}

$you = new Person();
$you->travel(2000);
echo 'Distance traveled: ' . $you->getDistanceTraveled() . "\n";

?>

Following is the output when executing Person.php file:

$ php Person.php
Distance traveled: 2000

You will notice that in the constructor of Person class, we are initializing the vehicle he owns to be a red Honda Accord. So this code works fine as long as we expect all the people to own a red Honda Accord. But obviously this is not the case in real life. In Inheritance in PHP post, we had a case where I owned a red Chevy Ram truck while you owned a white Honda Accord. So how do we initialize two people, you and me, such that we both own different vehicles? The easiest way is to remove the initialization of the vehicle from the Person class. Instead initialize it outside and pass the initialized Vehicle class to the Person class in the constructor. This is how the file Person.php will look after this change:

<?php

require_once 'Vehicle.php';

/**
 * Person class.
 */
class Person {

  // Vehicle
  public $vehicle;

  /**
   * Default constructor.
   *
   * @param Vehicle
   *   Vehciel object.
   */
  function __construct($vehicle) {
    $this->vehicle = $vehicle;
  }

  /**
   * Let the person travel in his vehicle.
   *
   * param int $miles
   *   Number of miles that the person travels.
   */
  public function travel($miles) {
    $this->vehicle->drive($miles);
  }

  /**
   * Returns the number of miles that the person has traveled.
   *
   * @return int
   *   Number of miles that the person has traveled.
   */
  public function getDistanceTraveled() {
    return $this->vehicle->getMilesDriven();
  }
}

$hondaAccord = new HondaAccord('white');
$you = new Person($hondaAccord);
$you->travel(2000);
echo 'Distance traveled by you: ' . $you->getDistanceTraveled() . "\n";

$chevyRam = new ChevyRam('red');
$me = new Person($chevyRam);
$me->travel(5000);
echo 'Distance traveled by me: ' . $me->getDistanceTraveled() . "\n";

?>

Following is the output when executing Person.php file:

$ php Person.php
Distance traveled by you: 2000
Distance travaled by me: 5000

What we did above is that we initialized the Vehicle object outside the Person class and passed the Vehicle object to the Person class when initializing a person. Earlier since Vehicle object was initialized within the Person class, Person class had a hard dependency on the Vehicle object. Now that we are passing an initialized Vehicle object to the Person class, the dependency has been decoupled to an extent. We can initialize any type of Vehicle and pass it to the Person being initialized. The Person class doesn't really care what type of Vehicle object is being passed as long as it implements drive() and getMilesDriven() methods. This is dependency injection. Earlier Person class was dependent on the Vehicle class but now we are initializing the Vehicle object elsewhere and injecting it into the Person object. If you understood what we did here, you understand what dependency injection is. Now let's understand the benefits of dependency injection.

Benefits

Decoupled code, which leads to more flexible architecture

The reason we used dependency injection in the above example is to increase the flexibility. Before using dependency injection, every person was initialized to own a red Honda Accord. After using dependency injection, every person will get initialized to own any vehicle that you initialize, and not necessary a red Honda Accord. In the example above, $me was initialized with a red Chevy Ram and $you were initialized with a white Honda Accord. Now let's take it one step further and instead of defining the argument in the constructor to be an object of the type Vehicle, define it to be an object that implements VehicleInterface. Since a person is using only two methods, drive() and getMilesDriven(), of the Vehicle object, we will define these methods to be in the interface and let the person be initialized with any object that implements this interface. An example of such an object could be a bullock cart. It has drive() and getMilesDriven() methods but doesn't have any other method, such as getWarrantyLeft(). Here's how VehicleInterface.php, BullockCart.php and Person.php will look in such a case:

<?php

/**
 * VehicleInterface interface.
 */
interface VehicleInterface {

  /**
   * Drive the vehicle.
   *
   * @param int $miles
   *   Miles driven.
   */
  public function drive($miles);

  /**
   * Returns the total number of miles driven.
   *
   * @return int
   *   Total miles driven.
   */
  public function getMilesDriven();
}

?>
<?php

require_once 'VehicleInterface.php';

/**
 * BullockCart class.
 */
class BullockCart implements VehicleInterface {

  // Miles driven.
  private $miles;

  /**
   * Default constructor.
   */
  function __construct() {
    $this->miles = 0;
  }

  /**
   * Drive the bullock cart.
   *
   * @param int $miles
   *   Miles driven.
   */
  public function drive($miles) {
    $this->miles += $miles;
  }

  /**
   * Returns the total miles driven.
   *
   * @return int
   *   Total miles driven.
   */
  public function getMilesDriven() {
    return $this->miles;
  }
}

?>

<?php

require_once 'Vehicle.php';
require_once 'BullockCart.php';

/**
 * Person class.
 */
class Person {

  // VehicleInterface
  public $vehicle;

  /**
   * Default constructor.
   *
   * @param VehicleInterface
   *   Object implementing VehicleInterface.
   */
  function __construct(VehicleInterface $vehicle) {
    $this->vehicle = $vehicle;
  }

  /**
   * Let the person travel in his vehicle.
   *
   * param int $miles
   *   Number of miles that the person travels.
   */
  public function travel($miles) {
    $this->vehicle->drive($miles);
  }

  /**
   * Returns the number of miles that the person has traveled.
   *
   * @return int
   *   Number of miles that the person has traveled.
   */
  public function getDistanceTraveled() {
    return $this->vehicle->getMilesDriven();
  }
}

$hondaAccord = new HondaAccord('white');
$you = new Person($hondaAccord);
$you->travel(2000);
echo 'Distance traveled by you: ' . $you->getDistanceTraveled() . "\n";

$bullockCart = new BullockCart();
$me = new Person($bullockCart);
$me->travel(5000);
echo 'Distance traveled by me in a bullock cart: ' . $me->getDistanceTraveled() . "\n";

?>

As seen in the code above, Person object is no longer dependent on the Vehicle object. In fact, we can even initialize it using the BullockCart object, which is not related to the Vehicle object in any way, as long as BullockCart object implements VehicleInterface, which in turn requires only two methods: drive() and getMilesDriven(). We have truly made Person and Vehicle classes decoupled and increased the flexibility of the code.

Easier Unit Testing

The second advantage of dependency injection is the ability to unit test the classes Vehicle and Person independent of each other. Consider the initial code when Person was initializing the Vehicle object in its constructor. This is how a PHPUnit test for Person will look:

<?php

/**
 * PersonTest class.
 */
class PersonTest extends \PHPUnit_Framework_TestCase {

  /**
   * Make sure that distance traveled is correct.
   */
  public function testDistanceTraveled() {
    $person = new Person();
    $person->travel(1000);
    $this->assertEquals(1000, $person->getDistanceTraveled(), 'Distance traveled does not match.');
  }
}

?>

The above test will pass. Let's assume that in future, there is a mistake in the implementation of getMilesDriven() in the Vehicle class. Suppose getMilesDriven() function in Vehicle class starts returning 0 irrespective of the miles actually driven. Now the above test will fail since getDistanceTraveled() method, which in turn calls Vehicle's getMilesDriven() method, returns 0 and does not match 1000. This is undesirable. Why should a test testing Person class fail if there is a mistake in Vehicle class? Agreed that in the above code, it's very easy to debug what's going on but when the project grows, just figuring the root cause of a failing test can become time-consuming. What we want is that a unit test testing the Person class should fail if and only if there is a problem in the Person class and not anywhere else. This is where dependency injection is useful. Now that we are creating an object implementing VehicleInterface and passing it in the Person's constructor, we can use mocks in PHPUnit to isolate the tests for Person class from any other class. Here is how the PHPUnit test for Person class will look after implementing dependency injection using VehicleInterface:

<?php

/**
 * PersonTest class.
 */
class PersonTest extends \PHPUnit_Framework_TestCase {

  /**
   * Make sure that distance traveled is correct.
   */
  public function testDistanceTraveled() {
    // Create a stub for VehicleInterface.
    $stub = $this->getMockBuilder('VehicleInterface')->getMock();

    // Configure the stub to return 1000 whenever getMilesDriven() method is called.
    $stub->method('getMilesDriven')->willReturn(1000);

    $person = new Person($stub);
    $person->travel(1000);
    $this->assertEquals(1000, $this->getDistanceTraveled(), 'Distance traveled does not match.');
  }
}

?>

The test code above is slightly more complicated that the one earlier. First we are creating a mock for VehicleInterface and instructing the mock to return 1000 whenever its method getMilesDriven() is called. Next we pass this mock to the Person object during initialization. As a result, In the assertEquals() statement, $this->getDistanceTraveled() will invoke getMilesDriven() method of the mock and will return 1000 as programmed. You will notice that even in the unit test, we have eliminated the dependency on any external class. Even if the implementation of getMilesDriven() method in the Vehicle or BullockCart class has bugs, the above test will pass since we are using a mock that gets passed to the Person object. So in future, if the above test fails, then we know for sure that there is a problem in the Person class and nowhere else. It becomes much easier to debug.

In this post, you learned about dependency injection and its benefits. In the next post, you'll learn about dependency injection container. In the meantime, please comment below if you can think of any other advantages of using dependency injection. If you found some part of this post difficult to understand, please let me know in a comment below so that I can make it easier to understand.

Services: 
Drupal Development

Sign up for our weekly newsletter


Comments

  • by weseze (not verified)
  • Wed, 09/02/2015 - 03:55

I'm not getting the very last example in the dependency injection, right before the unit tests. You mention "As seen in the code above, Person object is no longer dependent on the Vehicle object. " But the example code clearly shows "function __construct(Vehicle $vehicle) {". Is there a typo?

neerav.mehta's picture
  • by neerav.mehta
  • Sat, 09/05/2015 - 14:27

Yes, it's a typo and has been fixed. Thanks for bringing it up!

  • by just-passin-thru (not verified)
  • Thu, 09/24/2015 - 08:41

This is one of, if not the, best tutorial I've seen on dependency injection. Such a simple concept that is hampered by an obtuse name and usually even more obtuse explanations, lol.

well done!

neerav.mehta's picture
  • by neerav.mehta
  • Fri, 09/25/2015 - 21:40

Thanks! :) I am glad you liked it.

Add new comment