geeks4change/composable-inheritance

Provides composable inheritance via a magic classloader.

1.0.x-dev 2022-04-28 13:02 UTC

This package is auto-updated.

Last update: 2024-11-29 22:38:44 UTC


README

The problem

Augmenting services by decorating them is a well established practice. There is a big set of classes though, that is simple to extend but paractically impossible to decorate. Take classes with protected methods, and/or tight self-coupling (methods heavily call other methods or use protected/private state), augmenting one method typically leads to essentially copy-pasting lots of other methods, or duplicating internal state, essentially defeating the initial idea of code reuse.

We can subclass such classes, but this is not composable: In a component oriented architecture, there is no way that component A and component B can both subclass a service class without knowing thus tightly coupling each other.

An example use case

Given (like in DreamHooks) you want to augment Drupal's ModuleHandler via overriding ModuleHandler::buildImplementationInfo. It's protected anyway. But even if it were public, in a decorator we'd have to duplicate all calling methods.

So OK, we can subclass it and swap the implementation via symfony service decoration. But this is fragile (not composable): If another module does the same, only one can win.

The solution

Composable Inheritance solves this by creating extension classes at runtime, in a way that multiple extensions can subclass the same service class without needing any coupling between them.

How to use

For the above example, we Drupal's ServiceProvider mechanism to alter the container like this:

use Drupal\dreamhooks\LegacyBridge\ModuleHandlerExtensionTrait';
use Drupal\dreamhooks\LegacyBridge\ModuleHandlerExtensionInterface';
use geeks4change\composable_inheritance\ComposableInheritance;

class DreamhooksServiceProvider extends ServiceProviderBase {

  public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('module_handler');
    $class = $definition->getClass();
    // Or alternatively use ComposableInheritance::alter($class).
    $newClass = ComposableInheritance::create($class)
      ->useTrait(ModuleHandlerExtensionTrait::class)
      ->implementsInterface(ModuleHandlerExtensionInterface::class)
      ->class();
    $definition->setClass($newClass);
  }

}

The magic class name then will at runtime be resolved to a class that extends (subclasses) the base class (whatever its name) with the trait \Drupal\dreamhooks\LegacyBridge\ModuleHandlerExtensionTrait, and additionally implements the interface \Drupal\dreamhooks\LegacyBridge\ModuleHandlerExtensionInterface.

In plain Symfony, you can use a suitable Compiler Pass.

Opcache

If you want Opcache support for the dynamically generated classes, set an environment variable named COMPOSABLE_INHERITANCE_CLASS_STORAGE_DIRECTORY to an empty directory outside your webroot (so not to open an uploaded-file remote code execution attack vector).

Caveats

Of course composing extensions or decorations can break if the logic does not play well with each other, and may or may not be fixable via priority-ordering.

Composable Inheritance can only make subclass extension composition possible in general.