When a developer plans the architecture of his future web application, it is useful to think about its extensibility in advance. The modular architecture of the application can provide a good degree of extensibility. There are quite a few ways to implement such an architecture, but all of them are similar in their fundamental principles: the separation of concepts, self-sufficiency, mutual compatibility of all components.
However, there is one approach that can be found quite rarely in PHP. It includes the use of native inheritance and allows you to patch the code "more better" (c). We call this method “Forwarding Decorator”. It seems to us quite effective, and, by the way, spectacular too, although the latter is not so important in production.
As the author of the original English-language article "
Achieving Modular Architecture with Forwarding Decorators ", published on SitePoint, I present to you the author's version of the translation.
In it, I kept the originally conceived meaning and idea, but I tried to improve the flow as much as possible.
')
Introduction
In this article, we will look at the implementation of the approach using Forwarding Decorator, as well as its pros and cons. Let's compare this approach with other well-known alternatives, namely with the use of hooks, code patching or DI (dependency injection). For clarity, there is a demo application
here in this GitHub repository .
The basic idea is to treat each class as a service, and modify this service by inheriting and reversing the chain of heirs when compiling code.
In a system based on such an idea, in any module you can create a special decorator class (marked in a special way). Such a class will receive fields and methods of another class through the inheritance mechanism, but after compilation it will be used everywhere instead of the original class.
Actually, this is why we call such classes Forwarding decorators: these decorators are a superstructure over the original implementation, but they are being pushed forward at the places of use.
The advantages of this approach are obvious:
- Any part of the system can be extended with the help of a module - any class, any public / protected method. No need to pre-mark the extension points with a special code.
- One subsystem can be modified by several modules at the same time.
- Subsystems are weakly interconnected, so they can be updated separately, independently of each other.
- You can limit extensibility using familiar constructs: private methods and private (final) classes.
However, this approach also has its disadvantages:
- First of all, it is the lack of clear interfaces of interaction with the expandable system. We can expand everything that is not explicitly prohibited through private, but the system can not expect that it was entered from the wrong end and will work inadequately in cases that the developer of the module did not think about. You need to carefully inspect the code for unwanted side effects.
- You will have to implement a kind of compiler (details below).
- When developing modules, one should strictly observe the public interface of the subsystems and not violate the Liskov substitution principle , otherwise these modules will break the system.
- The presence of an additional compiler complicates debugging code. You cannot run XDebug directly on the source code; any change to the code first requires running the compiler. However, this problem can be solved using tricky PHP tricks so that the compiled files will run, but you will see the source code in the debugger.
This way of expanding the system is in some sense an intermediate solution between direct code patching (low-level, no rules of the game, god mode, greatest power but with greatest responsibility, etc.) and plug-in-based architecture, with a clear definition of how may be a plugin, which subsystems and how it can change \ provide. The system of decorators allows to solve a certain range of tasks well, but this is not a silver bullet at all and is not an ideal way to organize modularity.
How can such a system be used?
Here is an example:
class Foo { public function bar() { echo 'baz'; } }
namespace Module1; class ModifiedFoo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } }
How did that happen? This is street magic) We are turning the chain of inheritance back. The original class is without internal code. As a result of the compilation, we preprocess the code so that the contents of the original class go into a separate class, which will be the new parent for the chain:
namespace Module1;
In short, a compiler is built into the application, which builds intermediate classes, and an autoloader, which will load these intermediate classes instead of the original ones.
And now a little more. The compiler builds a list of all classes used in the system, and for each class that is not a decorator, it finds all the subclasses that will decorate it using
DecoratorInterface . It creates a tree of decorators, checks if there are no cycles, sorts the decorators by their priority about this in more detail later) and builds intermediate classes where the inheritance chain will be reversed. The source code is converted to a new class, which will become the new parent class for the inheritance chain.
It sounds hard. So it is, it is really a complex system. However, it allows very flexible combination of modules, and with the help of these modules you can modify absolutely any part of your application.
And if one class is rewritten by several modules?
If several decorators come into play at the same time, they fall into the chain of decoration according to their priority. Priority can be set using annotations (we use Doctrine \ Annotations) or configs.
Consider an example:
class Foo { public function bar() { echo 'baz'; } }
namespace Module1; class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } }
namespace Module2; class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' twice'; } }
In this example, the
Decorator \ After annotation is used to put the decorator of another Module 1 in front of Module 2. The compiler will analyze the files, take into account the annotations and build an intermediate class with this inheritance chain:

You can also use the following annotations:
- Decorator \ Before (to place the decorator in front of the decorators of another module or higher in weight)
- Decorator \ Depend (to enable the decorator, only if the specified module is enabled in the system)
This set of annotations (Before, After, Depend) is absolutely enough to build any combination of modules and classes.
Are there any working examples?
There is! For clarity, I have prepared an application demo, it is located
in this GitHub repository . This PHP application has a modular architecture, and modules can mix in code without recompilation. In this case, the modules can be added and removed, but in this case, recompilation is already required. In more detail all this is described in the
readme file.
There are quite "combat" examples. There are already several software products on the market that use this approach. In particular, something very similar is used in OXID eShop. By the way, they have a
cool blogging style . In another platform, X-Cart 5, this approach is implemented exactly in the form in which I described it - the
X-Cart 5 code was even taken as a basis for this article. This allowed us to create a very flexible e-commerce solution, which can be expanded as far as the developer’s imagination (or customer’s money =) suffices, and without breaking subsequent kernel upgrades.
Hooks and patches are better! Or not?
As with the Forwarding Decorators approach, using hooks and head-on patching has its pros and cons.
- Hooks (or any implementation of the Observer template) are widely used in many popular applications, such as Wordpress. Among the advantages is a well-defined API, a transparent way to register an Observer. The biggest drawback is the limited number of entry points for embedding extensions, also the inconvenience is the order of execution (it is difficult to rely on the result of other hooks)
- Patching in the forehead is the most trivial and obvious way of expansion, but it seems to us quite risky. Firstly, it significantly complicates the reading and analysis of the code, secondly, it complicates the rollback of changes in case of their incorrectness. Also, the imposition of several patches at the same time is complicated so that they do not contradict each other and do not break the functionality. In other words, this is the least controlled and controlled way, and if it justifies itself in simple solutions, then with the complication of the system, these disadvantages grow in proportion to its complexity.
- Dependency Injection - the code in the system with DI is built around the understanding that the necessary dependencies are not obtained manually, but are delivered from somewhere outside, or they are accessed indirectly - again through some supplier (most often it is some kind of IoC container).
Dependencies satisfy some interface and are a complete implementation of some functionality. Through the extension system, one implementation of the dependency can be substituted for another based on the current system configuration.
Implementations can be inherited from the base or decorated in the classical sense of the decorator - as in Symfony 2, for example, as described here . The problem with this architecture is that all code must be built using DI-style dependencies. The difference from the system described in the article is that the forwarding decorator allows you to replace the classes completely transparent at all points of use.
In addition, it is not clear how to organize the composition of several modules that extend the same service - you will have to write a separate system, since popular IoC containers do not solve this problem (this is outside the responsibility of such libraries).
Conclusion
Forwarding decorators are an approach that at least deserves attention. It can be used to solve the problem of developing an extensible modular architecture of applications in PHP. This will use familiar constructs, such as inheritance or the field of view of fields / methods / classes.
Implementing such a concept is not a trivial task, there may be difficulties with debugging, but they are surmountable provided that you spend some time properly setting up the compiler.
If there is interest in this material, in the next article I will write how to make an optimal compiler with an autoloader and use streaming filters (PHP Stream filters) to enable step-by-step debugging of the source code via XDebug. Interesting? Let us know in the comments. And I will be happy for your questions, advice and constructive criticism.