While coding the MongoDB integration for Drupal 8 I hit a wall first with the InstallerKernel which was easy to remedy with a simple core patch but then a similar problem occurred with the TestRunnerKernel and that one is not so simple to fix: these things were not made with extensibility in mind. You might hit some other walls -- the code below is not MongoDB specific. But note how unusual this is: you won’t hit similar problems often. Drupal 8 very extensible but it has its limits. But when you need to bypass those limits, PHP has a solution for you to extend classes (to some extent) that were not meant to be extended. Yes, it’s a hack. But when all else fails...

Be very careful because the next version of Drupal core or other software you are commando-extending might not work the same internally and so your hack will break.

With that caveat, let me introduce you to reflection. While some might think it was only meant to investigate objects, already in PHP 5 there was a ReflectionProperty::setValue method. In PHP 5.3 ReflectionProperty::setAccessible was added.

$r = new \ReflectionObject($kernel);
$services = $r->getProperty('serviceYamls');
$services->setAccessible(TRUE);
$value = $services->getValue($kernel);
$value['app'][] = 'modules/mongodb/mongodb.services.yml';
$services->setValue($kernel, $value);

Let’s investigate carefully what happens here because it is not evident and the documentation is lacking. $services is a ReflectionProperty object and while it was created by a getProperty call on a ReflectionObject encapsulating the $kernel object, the $services object is completely ignorant that it belongs to the $kernel object. We could’ve created it from the InstallerKernel class for the same result, it doesn’t matter.

Once this is understood, there are few surprises in this block of code: the property is protected so we need to make it accessible. The setAccessible() call will not make $kernel->serviceYamls public because, again, the $services ReflectionProperty object is ignorant of the $kernel object it was created from. It will, however, enable us to call getValue and setValue on it. But again, we need to pass $kernel to both the getValue and the setValue call.

So this way and only this way we can change a protected object property.

Even better, this works for private properties as well. So if you need to extend a class which has private properties (many Symfony classes do this) then instead of extending, wrap the object (this is called the delegate pattern) and use this method to access its private properties. As an example, let’s extend Symfony\Component\DependencyInjection\Scope (this is the shortest class I could find with a private property and an interface, the example is totally nonsensical and has nothing to do with the Scope class, it’s just an example):

class MyScope implements ScopeInterface {
  public function __construct($name, $parentName = ContainerInterface::SCOPE_CONTAINER) {
    $this->scope = new Scope($name, $parentName);
  }
  public function getName() {
    // Awful hack breaking encapsulation.
    $reflection_scope = new \ReflectionObject($this->scope);
    $name = $reflection_scope->getProperty('name');
    $name->setAccessible(TRUE);
    $value = ‘my’ . $name->getValue($this->scope);
    $name->setValue($this->scope, $value);
    return $value;
  }
  public function getParentName() {
    // This is proper: delegate the call.
    return $this->scope->getParentName();
  }
}

 

 

Drupal Security Monitoring by the Experts

Tag1 has you covered with our Drupal security monitoring solution, Tag1 Quo.