Get Hook-ed on Object-Oriented Programming
Steven Jones
When writing a hook implementation, for example of hook_cron
, there's often a tendency to write purely procedural code, like this:
function my_module_cron() {
$entity_type_manager = \Drupal::entityTypeManager();
$node_storage = $entity_type_manager->getStorage('node');
// More code goes here.
}
If you've got one or two easily understandable lines of code, fine, but frequently you'll end up with a little mini-application jammed into a hook implementation and it can be very easy to end up with something that's not particularly readable, let alone maintainable.
In several articles I've read they mention that you 'cannot' use Object-Oriented Programming code in a hook, and while sort of technically true, there's several easy ways to get back into the OOP world from a Drupal hook:
Static method
This is sort of one for people who simply don't feel right unless they are programming in a class because it doesn't bring all that many advantages over just writing your code in a function, but your hook implementation could look like this:
function my_module_cron() {
\Drupal\my_module\Utility\MagicalCronApplication::run();
}
And then you'd have a simple class in src\Utility\MagicalCronApplication.php:
namespace Drupal\my_module\Utility;
class MagicalCronApplication {
public static function run() {
$entity_type_manager = \Drupal::entityTypeManager();
$node_storage = $entity_type_manager->getStorage('node');
// More code goes here.
}
}
So that gets you into an OOP landscape, but it's a static method, so not all that different from the function implementing the hook, we can do better.
Create an instance
We could do a little refactor and make it so that our hook implementation instantiates a class and then calls a simple method on it, this would help our code look a little more familiar and indeed allow breaking up our little application into more methods to aid readability.
function my_module_cron() {
$instance = new \Drupal\my_module\Utility\MagicalCronApplication();
$instance->runApplication();
}
And then you'd have a simple class in src\Utility\MagicalCronApplication.php:
namespace Drupal\my_module\Utility;
class MagicalCronApplication {
/**
* The node storage instance.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
public function __construct() {
$entity_type_manager = \Drupal::entityTypeManager();
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
public function runApplication() {
// More code goes here.
}
}
We've got more code than ever before, but it's going to be simpler to split up our mini-application and tightly couple the methods together into a single class now.
We could also go a bit further and use the dependency injection pattern to get:
function my_module_cron() {
$instance = new \Drupal\my_module\Utility\MagicalCronApplication(\Drupal::entityTypeManager());
$instance->runApplication();
}
And in src\Utility\MagicalCronApplication.php:
namespace Drupal\my_module\Utility;
use Drupal\Core\Entity\EntityTypeManagerInterface;
class MagicalCronApplication {
/**
* The node storage instance.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
public function runApplication() {
// More code goes here.
}
}
Class resolver
Dependency injection is a lovely programming pattern that allows nice things like easily passing in mocked objects and if nothing else explicitly listing out our code dependencies. But if you don't have a Dependency Injection Container around, it can make instantiating classes pretty tricky/verbose, thankfully Drupal does and it has also has a super nifty class that'll help us access services in the container too. (Technically this is dependency container injection into a factory method, but hey!)
So we can rework our code to be like this:
function my_module_cron() {
\Drupal::service('class_resolver')
->getInstanceFromDefinition(\Drupal\my_module\Utility\MagicalCronApplication::class)
->runApplication();
}
And in src\Utility\MagicalCronApplication.php:
namespace Drupal\my_module\Utility;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MagicalCronApplication implements ContainerInjectionInterface {
/**
* The node storage instance.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
public function runApplication() {
// More code goes here.
}
}
Now we can write code that's much more familiar to the rest of Drupal and much more importantly we can copy/paste code from other classes as we need without having to think too much or doing various gymnastics to get the services we need, we can just add them nicely to the constructor and factory and we're away!
This approach also allows calling this code from multiple places much more easily, say if you want to provide a Drush command that calls this code or even a button in the web UI that runs the exact same code. These things are now super simple.
Bonus: If we happen to not actually need any services from the container, then we can actually drop the create
factory method and the implements
and the class resolver will basically call new
for us, but the day we do want those things we can pop them back and we don't have to go around worrying that usages of our class will break.
We could stop there...and we probably should...but.
A service
We're now not actually that far away from creating a full-blown service, which essentially is only different from the last example in that the class is also made available in the dependency injection container for other code to use too.
For that we could declare our service in a my_module.services.yml
file like so:
services:
my_module.magical_cron_service:
class: Drupal\my_module\Utility\MagicalCronApplication
arguments: ['@entity_type.manager']
And then our hook becomes:
function my_module_cron() {
\Drupal::service('my_module.magical_cron_service')
->runApplication();
}
We can then drop the factory static method from our class:
namespace Drupal\my_module\Utility;
use Drupal\Core\Entity\EntityTypeManagerInterface;
class MagicalCronApplication {
/**
* The node storage instance.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
public function runApplication() {
// More code goes here.
}
}
Aside from probably now being really badly named, a service allows some other fun things, like other code could swap out the actual class instantiated when the service is requested. If you did really want to do something like that I'd argue that you can actually swap out the hook implementation itself using a module_implements_alter
hook implementation instead, which is probably clearer as to what's going on (it's still not very clear, obviously).
If you are declaring a service you should probably also give the class an interface and because you are advertising to the world that they can call your code you should expect that to happen. You might now have to think about what issues you might cause if you change that code in the future. You might end up supporting an entire ecosystem of modules off of your service, that's a lot of potential code to break.
It's possible that a hybrid approach might be appropriate, whereby you declare some service that contains small, useful functions that other code from other modules might want to call, but that you keep your hook implementations either simply calling the methods on those simple services or where a little bit more logic is required: having a tightly coupled utility class, like in the 'Class resolver' example, that can do the work in a more OOP way.
Wrapping up
I'd argue that unless your hook implementation is no more than two or three lines, you might as well spin up a quick utility class and use the class resolver to instantiate the class. The future you will be grateful that you put in the extra minute to make your codebase more consistent, more copy/pasteable, and more flexible.
Additionally, if your utility class ends up with a lot of useful code in it I'd strongly consider refactoring it into a service so that other code can use your useful code too, but probably don't reach for the services.yml every time you need instantiate an object. But equally, you can use Object-Oriented Programming to implement a hook, almost.
Photo by James Harrison on Unsplash