Dependency injection for project developers with OXID eShop - Part 1: Basics

There is a whole range of techniques and heuristics for writing testable and maintainable software. But in the end, they all boil down to one basic rule: decoupling all software components as far as possible. The logic behind this is simple: If the individual building blocks are self-sufficient, they can also be tested in isolation. And thus, if the tests are written sensibly, it can be ensured for each individual component that it meets the requirements placed on it.

But of course these individual building blocks must be integrated into a whole at some point. In order not to create new dependencies and to avoid losing the painstakingly achieved decoupling of the individual components, one uses the technique of inversion of control. This is also called the Hollywood principle, according to the statement: “Don’t call us, we call you”. Translated into the world of software development: If one of our software blocks needs the functionality of another block, then it may not instantiate this block himself. Instead, it gets it injected from outside.

To understand why this is essential, let us take a very simple example:

class BlockI {

public function __construct()
{
	$this->b2 = new BlockII();
}

public function someFunction(int $param): int
{
	$input = $param * $this->getFactor();
	return $this->b2->doSomething($input);
}

private function getFactor(): int
{
	return 12;
}

}

If we now want to test the method someFunction(), we need to know what exactly the class BlockII does to test the result, because BlockII is hardwired to the class we want to test here. So how should we replace ? in the following test code? Without knowledge of BlockII we do not know.

class BlockITest {

public function testSomeFunction()
{
	$blockI = new BlockI();
	$this->assertEquals(?, $blockI->someFunction(5));
}

}

And even if we have determined a value, it can still cause us problems. What if BlockII returns different results, depending e.g. on the moon phase (or more realistically: the time zone or a configuration option)? Then the return value may change and our test may fail in some cases, but not in others.

These are all questions that we don’t want to ask ourselves at all when we test BlockI, because actually we don’t want to deal with what BlockII does. That is the task of those who develop this component.

Therefore it is better to inverse control. BlockI is not supposed to know anything about BlockII – except that the latter provides the method doSomething(). In other words: BlockII should implement an interface and only be addressable via this interface:

interface BlockIIInterface {

public function doSomething(int $input): int

}

Now we can rewrite the constructor of BlockI and reverse the dependency relationship:

public function __construct(BlockIIInterface $b2)
{
$this->b2 = b2;
}

Now we can write a test for BlockI without worrying in the least about BlockII:

public function testSomeFunction()
{
$blockIIMock = $this->createMock(BlockIIInterface::class);
$blockIIMock->expects($this->once())
->method(‘doSomething’)
->with(60);

$blockI = new BlockI($blockIIMock);
$blockI->someFunction(5);

}

We now only test that the correct parameter is passed to BlockII, which is what we implemented in BlockI as a kind of business logic. We don’t care what BlockII does with it, which is why we replaced it with a mock that produces an error if the method doSomething() is not called exactly once with parameter 60 during the test.

And thus we have written a real unit test for our BlockI, which completely ignores the business logic of BlockII (well, if you follow the very pure doctrine, it is not completely ignored either, because we still require knowledge of implementation details, namely this very call of BlockII; but in practice this cannot always be avoided).

So in order to write maintainable and testable code, the principle of inversion of control is indispensable. And as our fully executed example shows, no framework or similar is needed to implement this principle. Inversion of control gives us a technique for removing dependencies within our code.

During the runtime of the program, the individual blocks must of course be wired together. This can be done – at least for small projects – without any problems within the code itself. Above a certain level of complexity it is recommended to use a framework that ‚plugs‘ the individual components together. In principle it should not matter which framework is used: The code should not change because of that.

Meanwhile OXID has decided to integrate the Symfony DI container into its ecommerce framework. Because of the following reasons:

It is an established framework that is continuously maintained.
The container offers a very good caching and therefore a good performance.
It supports additional useful functionalities, especially events. 

When first introduced in version 6.0, the Symfony DI container could only be used within a special namespace within the OXID framework and was therefore not really interesting for project developers. Now we have opened the use of the DI container also to module and project developers.

This is the first blog article of a three-part series. The following blog posts will discuss how the Symfony DI container can (and should) be used in modules and for project development.

The next part will deal with how to use the DI container functionality within an OXID module.

And third part covers which possibilities the DI container offers to extend or replace shop functionalities without having to use oxNew().