Achieving Modular Architecture with Forwarding Decorators

Eugene Dementjev
Share

This article was peer reviewed by Younes Rafie and Christopher Pitt. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


As your web application becomes larger, you certainly start to think more about designing a flexible, modular architecture which is meant to allow for a high amount of extensibility. There are lots of ways to implement such architecture, and all of them circle around the fundamental principles: separation of concerns, self-sufficiency, composability of the parts of an app.

There is one approach which is rarely seen in PHP software but can be effectively implemented — it involves using native inheritance to provide manageable patching of the software code; we call it the Forwarding Decorator.

Picture for attention

Introduction to the Concept

In this article, we are going to observe the implementation of the Forwarding Decorator approach and its pros/cons. You can see the working demo application at this GitHub repository. Also, we’ll compare this approach to other well-known ones such as hooks, and code patching.

The main idea is to treat each class as a service and modify that service by extending it and reversing the inheritance chain through code compilation. If we build the system around that idea, any module will be able to contain special classes (they will be marked somehow to separate from the usual classes), which can inherit from any other class and will be used anywhere instead of the original object.

Comparison of the original and compiled classes

That’s why it is called Forwarding decorators: they wrap around the original implementation and forward the modified variant to the forefront to be used instead.

The advantages of such an approach are obvious:

  • modules can extend almost any part of the system, any class, any method; you don’t have to plan extension points in advance.
  • multiple modules can modify a single subsystem simultaneously.
  • subsystems are loosely coupled and can be upgraded separately.
  • extension system is based on the familiar inheritance approach.
  • you can control extensibility by making private methods and final classes.

With great power comes great responsibility, so the drawbacks are:

  • you would have to implement some sort of compiler system (more about that later)
  • module developers have to comply with the public interface of the subsystems and not violate the Liskov substitution principle; otherwise other modules will break the system.
  • you will have to be extremely cautious when modifying the public interface of the subsystems. The existing modules will certainly break and have to be adapted to the changes.
  • extra compiler complicates the debugging process: you can no longer run XDebug on the original code, any code change should be followed by running the compiler (although that can be mitigated, even the XDebug problem)

How Can This System Be Used?

The example would be like this:

class Foo {
    public function bar() {
        echo 'baz';
    }
}
namespace Module1;

/**
 * This is the modifier class and it is marked by DecoratorInterface
 */
class ModifiedFoo extends \Foo implements \DecoratorInterface {
    public function bar() {
        parent::bar();        
        echo ' modified';
    }
}
// ... somewhere in the app code

$object = new Foo();
$object->bar(); // will echo 'baz modified'

How can that be possible?

Achieving this would involve some magic. We have to preprocess this code and compile some intermediate classes with the reversed inheritance graph, so the original class would extend the module decorator like this:

// empty copy of the original class, which will be used to instantiate new objects
class Foo extends \Module1\ModifiedFoo {
    // move the implementation from here to FooOriginal
}
namespace Module1;

// Here we make a special class to extend the other class with the original code
abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface {
    public function bar() {
        parent::bar();        
        echo ' modified';
    }
}
// new parent class with the original code, every inheritance chain would start from such file
class FooOriginal {
    public function bar() {
        echo 'baz';
    }
}

The software has to implement the compiler to build the intermediate classes and the class autoloader, which will load them instead of the original ones.

Essentially, the compiler would take a list of all classes of the system and for each individual non-decorator class find all children that implement DecoratorInterface. It will create a decorator graph, make sure it is acyclic, sort decorators according to the priority algorithm (more on that later), and build intermediate classes, where the inheritance chain would be reversed. The source code would be extracted to the new class which is now parent for the inheritance chain.

Sounds pretty complicated, yeah?

Complicated it is indeed, unfortunately, but such a system allows you to combine the modules in a flexible way, and the modules will be able to modify literally any part of the system.

What If There Are Multiple Modules To Modify a Single Class?

In cases when multiple decorator classes should be in effect, we can place them in the resulting inheritance chain according to their priority. The priority can be configured through annotations. I highly recommend using Doctrine Annotations or some config files. Take a look at this 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';
    }
}
// ... somewhere in the app code

$object = new Foo();
$object->bar(); // will echo 'baz modified twice'

Here, Decorator\After annotation can be used to place another module decorator further up the inheritance chain. The compiler would parse the files, look for annotations, and build the intermediate classes to have this chain of inheritance:

Compiled classes diagram with annotation

Also, you could implement Decorator\Before (to place decorator class higher), Decorator\Depend (to enable decorator class only if another module is present or non present). Such a subset of the annotations would be pretty much complete to make any required combination of the modules and classes.

But What About Hooks Or Patching The Code? Is This Better?

Just like Decorators, each of the approaches has its advantages and disadvantages.

For example, hooks (based some sort of Observer pattern) are widely used in WordPress and many other applications. Their benefits are having a clearly defined extension API and a transparent way of registering an observer. At the same time they have a problem of the limited number of extension points and indeterminate time of the execution (difficult to depend on the result of the other hooks).

Patching the code is trivial to get started but it is often considered to be very dangerous, as it can lead to unparseable code and it is often very hard to merge several patches of a file or undo the changes. Patching the system may require its own DSL to control the modifications. Complex modifications would require deep knowledge of the system.

Conclusion

The Forwarding Decorators pattern is at least an interesting approach that can be used to tackle the problem of achieving a modular, extensible architecture of PHP software while using familiar language constructs like inheritance or execution scope to control extensibility.

Some applications have already incorporated the described concept, notably OXID eShop uses something very similar. Reading their dev docs is kinda fun, these folks do have a sense of humor! Another platform, X-Cart 5 eСommerce software, uses the concept exactly in the form described above – its code was taken as a basis for the article. X-Cart 5 has a marketplace for the 3rd party extensions that modify the behavior of the system yet do not break the upgradeability of the core.

Such a concept can be difficult to implement and there are some issues with the application’s debugging, but they aren’t impossible to overcome if you spend some time fine-tuning the compiler. In the next article, we will cover how to build an optimal compiler and autoloader and use PHP Stream filters to enable step by step debugging via XDebug on the original code. Stay tuned!

If you have any other tips and tricks you’d like to share – please let us know in the comments section below. Also, any questions are welcome.