A service container for a mocked Drupal 7 core
In previous posts we talked about dependency injection containers.
In this post we look at a real-world example: A container with services that replicate parts of Drupal 7 core, for easier unit testing.
Motivation: Unit testing for xautoload.
Drupal web tests are slow.
Real unit tests, however, are fast.
We are talking about minutes vs seconds.
The difference is so significant, I finally decided that xautoload should mainly be unit tests.
The problem was, we want to test how xautoload behaves during a Drupal bootstrap, on module installation, etc. And usually we would need a full-fledged Drupal installation, with database and expensive filesystem operations.
- Turn all of Drupal core that is relevant for these tests into services.
- Mock out the database.
- Mock out expensive filesystem operations.
- Manage the virtual core components with a service container.
The resulting DrupalComponentContainer is a 1:1 implementation of the pattern described in the previous post.
The calling code around that is still a bit messy, but the container itself is quite ok.
So, now we can
And it will test different scenarios of bootstrap, module enabling and install, with or without libraries API, with or without hook_xautoload(), etc.
Turning Drupal 7 core into services / components
Drupal 7 core has a lot of functions that operate on global or static variables.
Every function call and every usage of a global or static variable is hard-wired, that means, you cannot swap any one function out for a different implementation. E.g. drupal_static() is always going to be the one and only implementation from Drupal core.
Now we want to turn this procedural world into a modern universe of loosely-coupled services, which can have each other as dependencies, but where each service could be swapped out, and each service could be used standalone with mocked dependencies for testing.
One question comes up: Which methods should live in the same class/service? And which methods should rather live in separate classes?
E.g. we could say "Everything from includes/module.inc should go into one class, ModuleInc.".
Or we could say "Everything related to bootstrap goes into class DrupalBootstrap".
But let's face it: These D7 core include files are mostly just a collection of stuff that smells similar, but that does not really have that much in common.
There are only few functions that would actually benefit from being in the same class / service with another function.
And, some functions could even be further subdivided, once they are turned into a class.
As a result, we now have separate classes for DrupalStatic and SystemRebuildModuleData, which are 1:1 from drupal_static() and system_rebuild_module_data(). But there are also combined components such as HookSystem, which cover more than one of the original procedural functions.
Static variables become object properties
As we turn functions into service classes, static variables naturally turn into private or protected object properties.
static $files in drupal_get_filename() turns into private $files.
Nothing needs to be static.
Fake or "Mock" implementations
With this new architecture, it is much easier to swap out specific services with fake implementations, that are good enough for testing, and a lot faster than their production counterparts would be.
E.g. we don't need a complete database, we only need a Cache and SystemTable. And they don't even need any real persistence.
For the given use case, we can start directly with the fake objects, since they will never be used on production.
But even if this was made for production, we could simply subclass the container and override some of the factory methods, to replace the real system table with a fake one.
The example shows that almost everything from the prehistoric procedural PHP world can be turned into objects and services with properly injected dependencies.
There is no need for singletons, global variables, or any kind of global state.
The example is just a 1:1 conversion, which is good enough for what it's meant to do. If we were to really turn Drupal 7 into this service-oriented paradigm, we would certainly change a few more things.
(Some of this happens in Drupal 8, but a lot of the low-level code is still procedural, or entangled in global state, or squeezed into the unpleasantly big DrupalKernel class.)