Part 2: Dependency Injection within Modules

In the first part of our series on dependency injection within the OXID eShop framework, we looked at why the technique of inversion of control is important for writing maintainable and testable software. In this and the next part we will look at the concrete implementation.

Let’s first look at how you can use the DI container within a module to better structure your own code and practice inversion of control. For the extension of the shop code, let’s leave it at the traditional method of extension using the oxNew() mechanism. Newer ways of extension will be introduced in the third part of the series.

Symfony DI container und Inversion of Control

As mentioned before, OXID uses the Symfony DI container to support inversion of control. In an application based entirely on inversion of control, you usually don’t come into contact with the DI container at all, at most during configuration. In Symfony, for example, the routing component injects the services from the container directly into the controller – so you don’t touch the container itself.

In OXID this is unfortunately not so easy, because the DI container still encounters traditional routing. Therefore there is the possibility to get the DI container directly and request needed services from the container – at least partly using it as a resource locator. For this purpose there is the ContainerFactory class. This class provides a getContainer() method. The factory itself is instantiated by a static call:

$container = ContainerFactory::getInstance()->getContainer();

The container itself is a PSR-11 compatible Symfony DI container, which is normally read from a cache file, which makes it quite fast. This cache file is called container_cache.php and is located in the tmp directory of the application. So if you change the configuration of the container manually, you have to delete this file to update the container.

Configure the container

Now how is the container configured? At OXID we have decided to configure the container completely with yaml files. The following files, if they exist, are read in order:

  • Internal/services.yaml in oxideshop-ce
  • Internal/services.yaml in oxideshop-pe
  • Internal/services.yaml in oxideshop-ee
  • var/generated/generated_services.yaml
  • var/configuration/configurable_services.yaml

The logic for the first three services.yaml files is fairly obvious: First, the configuration of the services for the community edition is read. Then the professional edition and the enterprise edition get the chance to reconfigure certain services: Usually, this involves replacing the simple services from the community edition with more complex services from the higher editions.

But what’s the point of the generated_services.yaml file? Here it becomes interesting for module developers: This file is written by the OXID framework itself. It should therefore neither be edited nor deleted by hand. Among other things, this file can change when a module is activated or deactivated. If there is a services.yaml file in the root directory of an OXID module, this file is included in the generated_services.yaml file. For module writers, this means: If I want to write my own services for my module, all I have to do is put a services.yaml file with my service definitions in the root directory of my module. When the module is activated, my services will be accessible via the container.

A practical example

How does that look in practice? Let’s assume someone wants to write a module for the price calculation that completely overwrites the price calculation in the article class. Then, first of all, an entry point is created, very traditionally:

class MyArticle extends MyArticle_parent { public function getPrice($dAmount = 1) { // Here goes our override code } }

Then this class is registered in the metadata.php of the module as an extension of the Article class, also in the traditional way. What is new is how the actual code for the price calculation is then executed:

public function getPrice($dAmount = 1) { $container = ContainerFactory::getInstance()->getContainer(); $priceCalculationBridge = $container->get( PriceCalculationBridgeInterface::class); return $priceCalculationBridge->getPrice($dAmount); }

We do just one thing: we get the container, get an entry class from there and execute exactly one method on it. We can then build the rest of the implementation using the inversion of control principle, write tests, etc.

Of course we also have to register our implementation in the container. For this purpose we create a services.yaml file in our module. This could typically look like this (for more information about the structure and possibilities of such a file, see the symfony documentation)

services: _defaults: autowire: true public: false MyCorp\MyModule\PriceCalculationBridgeInterface: class: MyCorp\MyModule\PriceCalculationBridge public: true MyCorp\MyModule\PriceCalculationServiceInterface : class: MyCorp\MyModule\PriceCalculationService MyCorp\MyModule\PriceCalculationDaoInterface: class: MyCorp\MyModule\PriceCalculationDao

First, standard parameters for the service definitions are created: Autowiring should be used and the services should not be public. Autowiring means that if the constructor of one service contains the interface of another service and this is unique, then the dependency does not need to be configured at all, the container automatically resolves this dependency. Of course this only works if the interfaces of the classes are used as service keys. But this is good practice anyway, which we also use for OXID. And module writers should also take this to heart, if possible.

That the services should not be public is also good practice to keep the public API as small as possible. We at OXID have decided to call these public classes “Bridges” and would recommend this to module developers as well. In this example we have two more service classes, the PriceCalculationService, which implements the business logic, and a PriceCalculationDao, a data access object, in which we encapsulate the database logic.

The dao is injected into the domain service, the domain service is then injected into the bridge and thanks to autowiring this is done automatically if we create the signature of the constructors accordingly:

class PriceCalculationService { public function __construct(PriceCalculationDaoInterface $dao) { $this->dao = $dao; } }

When the module is activated, the import of this services.yaml file is then added to the generated_services.yaml file and the services are available in the container.

In the next part we will look at the configurable_services.yaml file and alternative ways to extend the code and business logic of the OXID eShop framework beyond the traditional oxNew() method.