Yes, another blog about Dependency Injection and how to put A as a dependency into B. It's so easy with the help of frameworks and DI containers to build a class structure and let them be automagically instantiated in your projects, so why creating another one? Because dependency doesn't end after the injection.
<?php
function square_of_2() : int
{
return 2 * 2;
}
A simple function that calculates the square of 2 and is easy to copy and paste in case we need to get the square value of other numbers.<?php
function square_of_2() : int
{
return 2 * 2;
}
function square_of_6() : int
{
return 6 * 6;
}
function square_of_8() : int
{
return 8 * 8;
}
As you can see, this is pretty repetitive and when you think about it, there are an infinitive amount of numbers you may need to calculate the same way. Would you always create a new function for that?<?php
function square(int $number) : int
{
return $number * $number;
}
By injecting the number as an argument, we don't need to create multiple functions for every calculation. Copy and paste is a source of bugs and duplicated code anyway.date()
, have a dependency to the PHP configuration or a library, so is that an injected dependency too? At least it's one you can't inject yourself, so we cannot speak about "injection" here, but I digress. Let's go to classes and objects.<?php
class Config
{
private $reader;
public function __construct(ReaderInterface $reader)
{
$this->reader = $reader;
}
public function doSomethingWithConfig($configFile)
{
$fileContent = $this->reader->readFile($configFile);
// Whatever this class was meant to do
}
}
// All 3 implement ReaderInterface
$xmlReader = new XmlReader();
$jsonReader = new JsonReader();
$yamlReader = new YamlReader();
$config1 = new Config($xmlReader);
$config2 = new Config($jsonReader);
$config3 = new Config($yamlReader);
In this example, we created a class that handles configuration values from different config formats like XML, JSON and YAML.Config
itself. We created 3 instances for Config
but every one has a different state.<?php
class Car
{
private $gasStation;
public function __construct(GasStation $gasStation)
{
$this->gasStation = $gasStation;
}
}
We need gasoline for a car, right? Where do we get gasoline? From a gas station of course, so a car has a dependency to a gas station, right?GasStation
instance, there must be a method that can give us some fuel for our car, but as you know, there are other ways to fill up a car. Theoretically, you can even use your bare hands to fill in the gasoline little by little. Currently, we can only use a gas station, so our car class example isn't really close to a real world car's dependency. You should know where this is going to by now: Our real dependency to Car
is gasoline.<?php
class Car
{
private $gasoline;
public function __construct(Gasoline $gasoline)
{
$this->gasoline = $gasoline;
}
}
I'll use a Gasoline
object to make the examples more readable. In reality, the code for Car
would be more complex.<?php
class Car
{
private $gasoline;
public function setGasoline(Gasoline $gasoline)
{
$this->gasoline = $gasoline;
}
public function drive()
{
// ... some code
// Error if $this->gasoline is null
$this->gasoline->reduceBy($number)
// ... some code
}
}
We would be forced to add if-blocks for every call on the gasoline methods.Car::drive()
can drain our gasoline. But no one would ever say "I need to go and set gasoline into my car". Neither do we "add" gasoline. We fill up.<?php
class Car
{
private $gasTank;
public function __construct(GasTank $gasTank)
{
$this->gasTank = $gasTank;
}
// Methods like drive()
// Instead setGasoline() or addGasoline()
public function fillUp(Gasoline $gasoline)
{
$this->gasTank->add($gasoline);
}
}
You can enhance these examples by adding a more generic "gas tank" that can allow Car
to be an electric car, while Gasoline
can simply be another form of fuel now. This will be the second time where we have a change of dependency in our car example. Car::setGasoline()
or Car::addGasoline()
wouldn't be a good name for an electric car anymore and changing the API of class' methods can take some time in a big project. As you can see, setter injection (or "add") can also come in different names. Despite the naming, which can make code easier to read, the contract between a car and its fuel stays the same.<?php
class Foo
{
public function doFoo(array $foo) : array
{
// work with array $foo
}
}
class Bar
{
private $foo;
// ... Inject Foo instance in constructor.
public function doBar() : array
{
$array = [
'id' => 3,
'name' => 'bar',
'amount' => 4,
];
$fooArray = $this->foo->doFoo($array);
return $fooArray;
}
}
Here, we have a class Bar
that has a dependency to Foo
. It uses a method called Foo::doFoo()
on its dependency to let it do some work with an associative array it provides. Bar
doesn't and shouldn't care what Foo::doFoo()
does inside its method body, it only needs to know that doFoo needs an array as an argument and that it returns an array. Everything inside the method is supposed to be a black box. But if we take a look inside doFoo(), we will realize that there can be more dependencies between classes than just the contract of type hints and interfaces.<?php
class Foo
{
/**
* Possible array injected by Bar
*
* [
* 'id' => 3,
* 'name' => 'bar',
* 'amount' => 4,
* ]
*/
public function doFoo(array $foo) : array
{
$id = array_shift($foo);
// Do stuff with $foo and return a new array
}
}
If you take a close look at the example above, we now have a new dependency between Bar
and Foo
: A dependency to the array's order of elements. The example expects the first element to be the id, but how should Bar
know that? If it changes the order, Foo::doFoo()
will not recognize it and stop working as expected. In case that the element name becomes first, you may get notices,warnings or a fatal error depending on what your code is doing with the values. But in case of amount it can work, silently creating inconsistent data, that may only become visible, for example, in form of foreign key constraint errors of a database one day. Unit tests won't find such an issue, which is why integration tests are a must have for a bigger project. Even if you use the array key to access a value, you can either assume, that an array key exists or use a check for it and implement a logic, that handles missing values. It doesn't matter, you decide between error prone code or more complex code, that needs more tests. With fixed accessors like id, name and amount you can also choose and prefer to create a value object with an interface containing getter methods instead of an array and introduce a much clearer contract.