OXID and Symfony Part 2: DependencyInjection

Modern PHP application has lots of objects which are responsible for various things like email sending or data retrieval from database. Chances are great that you may want to have objects inside inside other objects, especially if you follow Single Responsibility Principle. This part of OXID and Symfony post series will focus on explaining why to use Dependency Injection and showing how to have Symfony DependencyInjection component in OXID eShop.

I believe learning by example is the best way to learn, so lets discuss a case for WeatherService which:

  • Is able to retrieve Weather object on passing a Location object;
  • Uses HTTP Client for fetching weather from weather provider;
  • Has a parser that transforms HTTP response into Weather object.

Our concrete YahooWeatherService would look like so:

DO NOT rush to facepalm just yet. First we are going to do this a wrong way so we would know a reason why it shouldn’t be that way.

It is easy to create objects if you do it like in the example above but it is really hard to configure objects that this service depends on. What if HttpClient and YahooDataParser requires some parameters while constructing them. Everything would be hard coded into YahooWeatherService class. Also, every new instance of YahooWeatherService would create new instances for classes that it depends on (yes, YahooWeatherService is not that good of an example for this point, but Product class would be). We could solve this problem by using registries:

Now we do not have to care about configuration of HttpClient or YahooDataParser. Also we can easily switch concrete implementations of HttpClient as long as it has same interface. But still YahooWeatherService depends on a whole registry so your components can not be shared between projects that do not have this registry and it is really difficult to unit-test this class.

Lets go back to YahooWeatherService class. Instead of constructing object dependencies inside the object we inject them as construct parameters.

Now this weather service depends only on necessary classes or interfaces instead of depending on the whole registry. Also unit-testing became easier because we can pass mocks while constructing objects. But creating a new object became more complex, therefor we need object container.

Object container

Object container is an object which is aware of other objects and their dependencies which are created on demand. Other objects must not know that they are being controlled by object container. Lets create very primitive object container:

Maybe you have noticed that setService expects a function as a second parameter but not an actual object. This is that way because we want to create object only on demand. There is an example of usage:

Now we can easily get objects which are in object container without caring about dependencies. Code becomes more maintainable because we have only the one place where we write a recipe how those objects are dependent on each other.

Symfony DependencyInjection Component

As we have mentioned in the first part of this post series, there are dedicated projects which tackles specific problems and helps us not to reinvent the wheel. We have described a very simple usage of object container above but in most cases we want more from our object container, e.g. pass and use parameters or create object container from configuration file.

Symfony DependencyInjection component allows you to standardize and centralize the way objects are constructed in your application.

Or to put this in other words: Symfony DependencyInjection provides us with tools to create object container. We install this component via Composer (if you do not have Composer in your project read Part 1 of this post series) by requiring symfony/dependency-injection. Example usage of Symfony DependencyInjection:

Now we can simply not worry about technical implementation of object container on your own. As we have a huge community doing maintenance for us.

Container From Configuration File

We can use configuration files instead of describing object relations in PHP code. It is recommended to describe object relations in configuration files even for small applications as it is more readable. To be able to achieve that we must install Symfony Config component and Symfony Yaml if you want your configration to be in yaml format. Example code:

And configuration file:

We can have lots of configuration files. This gives us an ability to group them.

Container Compilation and Extensions

Symfony DependencyInjection component allows us to compile object container. There are various of reasons why we want to do this, such as: better performance or checking for potential errors.

Object container can be compiled by calling compile method on ContainerBuilder object.

If we are compiling our container we have an ability to have extensions. The main purpose of extension is to register new services. Extensions gives us an ability to have modular application.

Symfony DependencyInjection in OXID

Symfony DependencyInjection component has way more capabilities than we have reviewed so far. We only did brief introduction to make you understand why and how to use it. Read more about Symfony DependencyInjection component at official website.

Spoiler alert! We will end up not using an implementation which is described below. Container building is a responsibility of HttpKernel component. But it is still good to read an implementation to get the idea why you may want to use HttpKernel.

We want to have some sort of a kernel class where we will register dependency injection container extensions and compiler passes. Lets start from top to bottom. Create app directory. It will have main configuration files and ContainerKernel.php file. Directory tree:

So our goal for ContainerKernel.php is that we could register extensions like so:

Now we need to think how we are going to make our container object accessible in OXID. I can think of three solutions:

  • Make Container object as singleton in ContainerBridge component
    • PRO: we do need to change OXID
    • CON: it is a singleton which we later have to support
  • Make Container accessible via oxRegistry with oxRegistry::get('container'):
    • PRO: it is using oxRegistry which familiar amongst OXID developers
    • CON: if you are going more Symfony way I am pretty sure you want to deprecate oxRegistry at some point of time, so you would have to readjust that again
  • Inject Container on every instance of ContainerAwareInterface object constructed via oxNew
    • PRO: we are not tied to oxRegistry or any other OXID object directly
    • CON: extension of oxNew to magically inject Container to ContainerAwareInterface objects

I very ofter think of second and third options. Can not make my mind yet. For example implementation I am going to go with a third option.

We will create a seperate component to bridge Symfony DependencyInjection into OXID. Normally this would be a seperate package installed by Composer but as we are not planning to keep this implementation for a long time lets do this within the source of our project. Edit composer.json to autoload files by PSR-4 rules for src/ directory:

And after running composer update you will get autoloader regenerated. So now we can implement base ContainerKernel class:

And MergeExtensionConfigurationPass:

Great! Now we need to build this Container in OXID so we could use it. We could do this via module but we might want to use Container parameters in config.inc.php so lets create bootstrap file independent from OXID:

And bootstrap it in bootstrap.php:

Ok. Now we have something to add to our Symfony module. We will create oxUtilsObject extension. So first register this in metadata:

And the extension:

Congratulations! You can now have container injected in any OXID object. How can you benefit from this you might think. You can create components independent from OXID itself, register it as container extension and create a lean modules as bridges to use that in OXID project.

Credits

Explanation of Dependency Injection in general was highly inspired by Fabien Potencier slides which are available at slideshare.net

 

0.00 avg. rating (0% score) - 0 votes
1 reply
  1. Philip Washington Sorst says:

    Hi Ellis, this is impressive stuff and certainly useful for concrete projects that want to use some of the Symfony framework components. So I think project developers will find these posts really helpful for their projects.
    Although we are working on similar ideas, for example composer integration and the introduction of Symfony components, we unfortunately won’t be able to port the changes into the core as you propose it. Our development goal is to provide our customers and partners with a stable and reliable ecosystem, so risks for bigger BC (backward compatibility) breaks have to be assessed thoroughly and I do see quite some sideeffects that will break a lot of stuff, like changing the directory structure and how to support modules that will suddenly be depending on another framework. That is actually my second point. If we try to introduce Symfony as a second framework next to OXID, we will rather increase the complexity than reducing it, so I prefer a solution where we actually replace parts instead of becoming inconsistent by offering different solutions.

    Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *