UPCOMING: Magento 2 Bootcamp of four days in The Netherlands (April 29th - May 2nd)
background

May 22, 2023

Shopware decorators only on interface?

Yireo Blog Post

During a Shopware training session, I heard an experienced developer say that you could only create a Symfony service decorator for an interface and not a class. Is this true? The answer is no. Here's a write-up of how decorators work.

Service decorators in a nutshell

Service decoration follows from the OOP design pattern Decorator (the Decorator pattern) which allows to change the behaviour of an object dynamically, without affecting the behaviour of other objects from the same class. It is different from replacing the class with a child-class, because in that case, you would be replacing that class with your own, while the Decorator pattern suggests this is all done with objects, not classes - so in runtime.

In Symfony (and thus Shopware), the DI container (aka service container) supports decoration through a service declaration. To explain this all properly, let's run a little example.

Dummy class with interface

First, a service decorator assumes that some target - for instance, an interface or a class - is already declared as a service. For instance, we could say that there is a class Yireo\ExampleBundle\SomeService that is implementing an interface Yireo\ExampleBundle\SomeServiceInterface:

namespace Yireo\ExampleBundle;

class SomeService implements SomeServiceInterface 
{
    public function getSomething(): string 
    { 
        return 'something'; 
    }
}

And the interface:

namespace Yireo\ExampleBundle;

interface SomeServiceInterface 
{
    public function getSomething(): string; 
}

And then this class is declared as a service with this:

<service id="Yireo\ExampleBundle\SomeService"/>

However, it is always nicer to depend on abstractions instead of concrete implementations (SOLID and things). So, we could also declare the interface to be an alias of the original service class:

<service id="Yireo\ExampleBundle\SomeServiceInterface" alias="Yireo\ExampleBundle\SomeService"/>

Now, the interface (instead of the class) could be injected somewhere else:

class SomewhereElse
{
    public function __construct(
        private \Yireo\ExampleBundle\SomeServiceInterface $someService
    ) {}

    public function echoSomething() 
    { 
        return $this->someService->getSomething(); 
    }
}

This is the ideal scenario: The interface is used as a service contract, the actual implementation lies behind it.

Service decorators in a nutshell

Building on top of this scenario, a decorator service could be created with the decorates attribute referring to the original service:

<service id="Yireo\AnotherBundle\SomeDecorator" decorates="Yireo\ExampleBundle\SomeServiceInterface" />

Now, the idea is that the code of SomewhereElse will be kept the same - it will depend upon the original \Yireo\ExampleBundle\SomeServiceInterface (an abstraction) and nothing in that will change.

However, when the interface is requested from the DI container, the DI container will normally use the original class, but now will swap out the original class with the decorator class. To make sure that the signature of the decorator is matching the requested interface, the decorator simply needs to be made compatible with that interface - by implementing it:

namespace Yireo\AnotherBundle;

class SomeDecorator implements SomeServiceInterface 
{
    public function getSomething(): string 
    { 
        return 'decorated something'; 
    }
}

Done. The decorator is now used instead of the original class.

The real pattern

However, this silly swapping out of classes could also be used with service rewrites. Instead, the real Decorator pattern refers to the fact that the decorator tries to guarantee that the original functionality of the service is still called upon. In other words, our decorator should call upon the original service (by injecting it and then calling its methods). The decorator above shows that we could also do this differently, but the point of building a decorator is that you don't just rewrite the class.

To inject the original service into the service class, we add an argument to the service declaration of the decorator. And note the .inner identifier is used to tell the DI container that the original service declaration should be used, not the replacing decorator (which could cause a loop):

<service id="Yireo\AnotherBundle\SomeDecorator" decorates="Yireo\ExampleBundle\SomeServiceInterface">
    <argument type="service" id="Yireo\ExampleBundle\SomeServiceInterface.inner"/>
</service>

And the PHP side shows how the original interface was injected:

namespace Yireo\AnotherBundle;

class SomeDecorator implements \Yireo\ExampleBundle\SomeServiceInterface 
{
    public function __constructor(
        private \Yireo\ExampleBundle\SomeServiceInterface $originalSomeService
    ) {}

    public function getSomething(): string 
    { 
        return 'decorated ' . $this->originalSomeService->getSomething(); 
    }
}

Hurray, all good. That's how service decorators are working with interfaces.

And now without an interface, but just a class

But what if the entire story does not involve an abstraction at all? What if there is no interface, but just a class? Well, then the class is a bit simpler of course:

namespace Yireo\ExampleBundle;

class SomeService 
{
    public function getSomething(): string 
    { 
        return 'something'; 
    }
}

And with that, there is no alias for the interface needed either. And people would just inject the class directly into their constructor, when needed. Even though, it gets ugly later, the code at first is simpler.

What about the decorator class? Well, it doesn't change much: The decorator no longer implements an interface, but just extends the original class:

namespace Yireo\AnotherBundle;

class SomeDecorator extends \Yireo\ExampleBundle\SomeService 
{
    public function __constructor(
        private \Yireo\ExampleBundle\SomeService $originalSomeService
    ) {}

    public function getSomething(): string 
    { 
        return 'decorated ' . $this->originalSomeService->getSomething(); 
    }
}

In this theoretical example, there is little difference between the interface-based version and the class-based version. It is just dummy code.

The problem in real-life

In real-life however, things get more complicated. The implementation (the class) could become complex over time. In a Symfony-based environment, this probably means that the class would get more and more dependencies, most of them being injected through the constructor. The number of constructor arguments grows. And that's where the real problem kicks in:

With the class-based version of this example, the decorator would need to extend upon the original class and inject it (to retain the decorator pattern). But when injecting the original class, it will also need to duplicate the entire parent constructor, which leads into a lot of fluff code, simply to satisfy the parent constructor. With the interface-based version of this example, the interface would not change, when the class would become more complex overtime, so the decorator remains simple as well.

In short, with Symfony decorators, the story becomes more effective when services consist of interfaces-plus-implementations, so that the interfaces could be decorated. Shorter, interfaces should be preferred over classes. Or: Composition over inheritance.

Meet the Shopware code, which is not always perfect

Now within the Shopware code, there is a growing amount of service interfaces that are pushed to forward to guarantee things like backward compatibility, plus ease the use of decorators (amongst other things). But the fact is also still that there are numerous classes without proper interfaces out there. And all of them could be decorated.

Back to the original question of this blog: Does this mean that you can't decorate classes? Totally not true. It is just that with class-only services the decorator would need to duplicate the constructor arguments, which could end up messy. To keep the code clean, the usage of interfaces is therefor recommended.

Posted on May 22, 2023

About the author

Author Jisse Reitsma

Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.

Sponsor Yireo

Looking for a training in-house?

Let's get to it!

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.

Do not miss out on what we say

This will be the most interesting spam you have ever read

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.