Part 3: Extending OXID eShop using the Symfony DI container

Extension of the shop logic using the Symfony DI container

In the second part of this blog post series we learned how modules can use the DI container, simply by placing a services.yaml file in the module’s root directory. However, we did the actual code extension in the traditional way by registering an extension in the metadata.php file for a shop class.

General problems with direct extensions of shop services

But why not simply extend a shop service directly in the services.yaml file? In this case, the file extension with the somewhat strange _parent construct could be dropped. Why not just create a subclass of a shop service and register it under the key of the original class? That, actually, would be obvious.

Unfortunately, that won’t work: As a matter of fact, OXID framework will refuse to activate a module in which a service from the shop is overwritten. Why this harassment? Here are two reasons:

On the one hand, only a single module could extend one service. If two different modules try to do so, the module which is activated later, wins. This isn’t a desirable behaviour, is it.

Extend Multishop environments

Secondly, in a multi store environment, the change would automatically be active for all shops. One of the advantages of OXID modules is that they can be activated selectively for individual shops, while their installation in other shops would have no effect. But this would not work if shop services would have been replaced. In this case, all services would have always be changed for the entire installation.

But doesn’t this also apply to internal services of a module? In general yes – in practice no. Because, as described in the last blog post, we’d need to have a traditional entry point that is instantiated via oxNew(). And this entry point would depend on whether the module is activated in a shop or not. This prevents that internal services of a module are called for sub-shops for which they are not intended.

Overwriting Shop Services with Modules

But what if I know for sure that only one of my modules overwrites this one shop service, and that I need the change for all my sub-shops? Shouldn’t I be able to use the capabilities of the DI container? This is indeed a legitimate request. And this is where the var/configuration/configurable_services.yaml file mentioned in the last blog post comes into the game. This is the only place where all services may be replaced by alternative implementations. The implementation itself can lie quietly in a module and can also be configured through the module’s services.yaml file. Only the alternative implementation of the one service to be replaced needs to be entered in the configurable_services.yaml file. This has to be done manually. The developer should make a conscious decision: Yes, I want to overwrite a shop service and I want to do this with exactly this one class from my module. In this way, the configurable_services.yaml file also becomes documentation of which shop services have been replaced by alternative implementations in a particular project.

OXID composer packages

These implementations do not necessarily have to be located in an OXID module. There is also the possibility to do this in a normal composer package. And the DI container can also be used in this package: If the type is set to “oxideshop-component” in the composer.json file of a composer package, the OXID composer plugin checks if there is a services.yaml file in this package. If so, it will be included in the generated_services.yaml file like an OXID module. The same applies here as for the services.yaml file in modules: services can be configured within the package, but it is not possible to replace shop services. To do this, the service must be entered in the configurable_services.yaml file.

However, this type of extension is an either/or: either a service is replaced or not. But what about code extensions that should only be active in certain subshops? The traditional OXID programming style with oxNew() allows you to enable or disable class extensions specifically for individual subshops. With entries in the configurable_services.yaml file this is not possible. What is entered here is valid for the entire installation, independent of the respective shop.

Shop-specific Symfony Events

This is where the other extension option comes into play, which is provided by the Symfony DI container: Events. A detailed explanation of how events work and how to configure them can be found in the Symfony documentation.

The principle is simple: event calls are placed at various points in the shop code. For these events you can subscribe to a so-called EventSubscriber; and when the execution of the code reaches the appropriate place, the code of the EventSubscriber is called. The subscriber is passed the event object, which, depending on the event, can also contain a certain payload. The developer documentation contains information on which events there are; there are also code examples that explain how to use them.

It should only be pointed out here that the EventSubscribers can be written in a way that they are only called for shops for which the module is also activated. This can be easily done by not simply letting the subscriber implement the EventSubscriber interface provided by Symfony, but inheriting it from the class AbstractShopAwareEventSubscriber, which is part of the OXID framework. This automatically ensures that the corresponding EventSubscribers are configured to know for which shops they are active. This is also done in the generated_services.yaml file mentioned in the previous blog post of this series.

The same mechanism is implemented for the OXID console, which is based on the Symfony console. Again, you can register console commands in the DI container so that they are only active for certain shops. The abstract class to inherit from is called AbstractShopAwareCommand, which is not very original.

And this brings us to the end of our short tour, where we showed how the Symfony DI container integrated in OXID can be used in module and project development. We hope that this helps you in your work; and of course we are always happy about Feedback.