📜 ⬆️ ⬇️

Building a modular architecture of the application on Forwarding decorators (author's translation)

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.
image
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:


However, this approach also has its disadvantages:


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; /** *    ,    DecoratorInterface ( :    ,   ) */ class ModifiedFoo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } 

 // ... -    $object = new Foo(); $object->bar(); // will echo 'baz 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:

 //    ,   ,     class Foo extends \Module1\ModifiedFoo { // move the implementation from here to FooOriginal } 

 namespace Module1; //     ,         abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } 

 //      .        class FooOriginal { public function bar() { echo 'baz'; } } 

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; /** * @Decorator\After("Module1") */ class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' twice'; } } 

 // ... -    $object = new Foo(); $object->bar(); //  'baz modified 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:
image
You can also use the following annotations:


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.


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.

Source: https://habr.com/ru/post/328970/


All Articles