Drupal Module Hooks

Drupal is a Content Management System. Drupal is also deeply, deeply weird. While systems like Magento often confuse people, the MVC structure that most people are used to is still there, it’s just more abstract. Web MVC systems are systems designed to, first and foremost, help programmers manage the complexity of their code.

Drupal is something else. It’s a system designed, first and foremost, to help website developers get their content on a website. Drupal contains several programming systems, and is built around object oriented concepts implemented without classes. In fact, architecturally Drupal can be thought of as a container that holds multiple systems, and all these systems interact to get a piece of content on a website.

Drupal has just undergone a major version release. With that revision comes refinements to the system, as well as a renewed interest in the platform from outside the community. Over the next few months I’m hoping to explore and write about Drupal from the point of view of a PHP programmer. Not just using the systems, but understanding how they’re implemented and in turn, uncovering their full potential/power. The specifics will be for the just released Version 7, but I imagine many of the concepts will be applicable to older versions as well.

We’ll be starting with Drupal’s module system, which is the glue that binds all the other sub-systems together.

Setup

Before we start, we’ll need to write some wrote voodoo setup code. This code may seem weird an unintuitive at first. The explanations for what you’re doing may not make sense. Follow the instructions, persevere, and at the end of this article come back and things should make more sense.

We’re going to setup two empty Drupal modules which we’ll use later. We’ll also being copying the default Drupal index.php so we can run some code in isolation to show you how Drupal modules work.

We’ll start with creating a module.

Create the following folder (directory paths are relative to the Drupal site root)

mkdir sites/all/modules/modulea

and then create two empty text files (we’re using the *nix “touch” command for this)

touch sites/all/modules/modulea/modulea.info
touch sites/all/modules/modulea/modulea.module

Finally, add the following contents to the modulea.info file.

; File: sites/all/modules/modulea/modulea.info (don't include this line)
name = Module A
description = First Example Module
package = Alanstormdotcom
version = VERSION
core = 7.x

files[] = modulea.module

That’s enough to create a simple module. Drupal will use the module’s folder name (in our case, modulea), as the module’s internal, programatic identifier. The .info file is a configuration file (ini style) that contains information about the module. Its presence and contents is what tells Drupal the module exists.

The modulea.module file will eventually contain the PHP code for our module. If you’re using a search tool like ack you’ll want to add the .module extension to list of valid extensions for PHP files (while you’re there, add .inc as well). Depending on your editor you may need to make similar tweaks to get syntax highlighting working in these files.

We’re placing the module in the sites/all/modules folder. This is where third party modules (also called contrib modules) that we want made available to all sites should go. Drupal offers feature that allows you to enable modules on a per-site (hostname) basis. By placing the module in all, its functionality will be available to everyone.

To ensure we’ve installed our module correctly, Navigate to

Modules

while logged into Drupal as an Admin user. There’s a lot of Drupal functionality that’s only available via the GUI Admin, or by knowing which database bit to flip. This is one of things that seems weird to outsiders. Drupal isn’t a system meant to help you program, its a system meant to help you manage your content in a website, which means you’ll always prefer a browser GUI. If you don’t, then you’re (obviously!) a core Drupal developer who knows their business and doesn’t need an extra layer between you and the Core code.

I’m not exactly advocating for this view of the world, but if you’re going to become involved in the Drupal community you need to accept that’s where they’re coming from.

Enough moaning! Back to work! If you click on the Module admin menu item, you’ll see a list of all the Modules installed in the system. Below the Core modules group, you should see a new module group named Alanstormdotcom (this name is derived from the modula.info file).

You may need to clear your Drupal cache for this module to show up. Drupal, like most modern systems, implements internal caching to speed up certain tasks. You can do so by navigating to

Configuration -> Development -> Performance

and clicking the Clear all caches button.

Over the years this “Use the GUI or known the system” approach has left some developers wanting, and a robust community of tools has grown up around the Drupal. You may want to look into some of the alternative modules and tools for quickly clearing/disabling the Drupal cache. I’ve only been using it for a bit, but I’m a big fan of drush, a command line too for performing common system tasks.

Finally, I’m new enough to the system that I’m not sure where/when clearing the cache is or isn’t required. If something you’re trying isn’t working, it never hurts to clear out your cache and try again.

Enabling the Module

Just because a module is installed doesn’t mean its enabled. Drupal will only load code from modules that are enabled. This allows you to install third party code, but quickly turn it around off if the need arrises. If it isn’t, check the checkbox and then click Save Configuration. Your new, behavior-less, “Module A” is now a part of the system! Let’s do the same for a second Module, Module B

Create the following folder (from the Drupal site root)

mkdir sites/all/modules/moduleb

and then create the following files

touch sites/all/modules/moduleb/moduleb.info
touch sites/all/modules/moduleb/moduleb.module

And then add the following contents to the .info file.

#File: sites/all/modules/modulea/moduleb.info (don't include this line.  That goes for all the #File lines in this tutorial.  We won't tell you again.)
name = Module B
description = Second Example Module
package = Alanstormdotcom
version = VERSION
core = 7.x

files[] = moduleb.module

That’s Module A and Module B in place! For our last bit of voodoo setup, copy your index.php file.

cp index.php working.php

and then edit working.php so it contains the following

1
2
3
4
5
6
define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
echo '<p>Done</p>';
###menu_execute_active_handler();

and load your working.php file in a browser.

http://drupal.example.com/working.php

You should see the word “Done” displayed on a white background. What we’ve done here is commented out the code that would normally display a Drupal page, and but kept in the code that bootstraps the Drupal system. We’ll cover this in greater detail later.

With that out of the way, we’re ready to begin!

What a Module Is

As I’m fond of pointing out, there’s no word in software development more hijacked that the word module. As you’d expect, Drupal has it’s own take on modules that’s different than other systems you may have worked with.

Drupal’s module system implements a Hook system. This system resembles class based observer/listener patterns you may be familiar with, but no classes or objects are used. Instead, the system is implemented entirely with PHP’s variable function features. This may seem weird if you’re just coming into PHP development, but remember Drupal’s history.

Drupal is over a decade old, its first versions surfacing in the open source community sometime around 2001. PHP was a popular language at the time, but PHP 4 was the new version. Objects based on classes were not references, which means traditional OOP patterns weren’t readily availability. Even once PHP 5 broke onto the scene, it’s class/object system was noticeably slower that solutions implemented using only functions. Code performance speed approaches deity like status in the Drupal community, so rather than play with the cards PHP dealt them, Drupal developers chose to implement their own system. Also, it’s more fun to roll your own!

This sort of decision is one of the many things that programmers like to argue about. My suggestion to you is accept that Drupal made the decision, and learn to use the systems they’ve provided. If you can’t do that, and you’re bent out of shape about it, pick another system. If you can’t pick another system and hate the one you’re being forced to you, meditate on why you’re working where you are.

So, what are Hooks? The way I’ve come to understand them is that hooks allow you to distribute a single function call over multiple include files. Let’s pretend we’re writing a PHP function to do something

1
2
3
4
5
6
7
8
9
10
11
12
function do_something($thing)
{
    //code here to do something awesome
    //insert some database information
    //mark a flag
    //chat up a potential spouse
    //sing a song          
    return $something
}
 
... somewhere else in our program  ...
do_something();

Once we’ve written that function and deployed the code, we can’t easily change it. That is, it’s easy enough to edit the file, but other coders may have become reliant on how that function works. It’s very hard for a programmer to communicate, purely through code and comments, everything a function is supposed to do. Even if you have a team with perfect communication, there’s the problem of unintended side effects a function may have. A tenant of systems programming is never change existing code, even if you think you understand everything it does.

Here’s the problem we’re trying to solve. When “something” happens, we want to take additional actions. Requirements change, new functionality is needed. These are the standard conditions on the ground for a developer. So how do we not change code that we don’t own (Drupal core), but still take those additional actions?

That’s the problem Hooks solve. When you want to do something, instead of calling a function, you invoke a hook.

1
2
3
4
5
//old way, calling do_something function
//do_something();
 
//new way: invoking do_something hook
module_invoke_all('do_something');

When a hook is invoked, Drupal will

  1. Get a list of all installed and enabled modules

  2. Ask each module “Have you implemented the do_something hook”?

  3. If so, then Drupal calls the function in those modules that implement the hook

This way, as a core developer, you can achieve what you want while still letting other programers “hook into” what you’re doing.

What does this look like?

Makes sense in the abstract, but what does that look like in real code? Let’s take a look. We’re going to

  1. Write code that invokes a hook named helloworld
  2. Write code that implements the helloworld hook in Module A
  3. Write code that implements the helloworld hook in Module B

Let’s get started.

In the setup section we created an alternate index.php file named example.php. Change the contents of this file so they match the following

1
2
3
4
5
6
define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
module_invoke_all('helloworld');
echo '<p>Done</p>';
###menu_execute_active_handler();

We’ve commented out the menu_execute_active_handler function call. This is the call that would actually render out a Drupal page. For the purposes of this tutorial, we don’t want that to happen. This is why we’ve commented it out.

We’ve left in the call to the drupal_bootstrap function. This loads up the Drupal system and includes all the code necessary for Drupal to run. We need this loaded in, because its the bootstrapping that loads in the module system code.

In-between those two standard function calls we’ve added two lines

1
2
module_invoke_all('helloworld');
echo '<p>Done</p>';

The echo is a sanity thing to ensure that PHP is executing down to the bottom of the file. If you load your page and don’t see Done, then you know there was an error previously. Strictly speaking this isn’t needed, but I’ve worked on enough PHP systems to know you’re never sure where/when error reporting is done.

The module_invoke_all function call is the interesting bit. This function isn’t the only way to invoke a hook, but it’s the most obvious way to do so. With the code above, we’ve handled our first requirement, invoking a Hook. The Hook’s name is helloworld.

Load working.php in a browser, and … only the word Done shows up. While we’ve invoked a hook, there’s no Drupal modules that implement that hook. Therefore, nothing extra happens. Let’s take care of that.

Implementing a Hook

Earlier in this article, we created two modules. Module A, and Module B. Make sure these modules are enabled before continuing.

Next, open up Module A’s .module file, and enter the following

1
2
3
4
5
6
#File: sites/all/modules/modulea/modulea.module
<?php
function modulea_helloworld()
{
    echo "<p>Our friends at ".__FUNCTION__." want to say Hello World</p>";
}

Remember, despite the .module extension this is just another PHP file. Clear your Drupal cache and reload working.php. You should see the text

Our friends at modulea_helloworld want to say Hello World

That’s it! You’ve now implemented your first hook. To implement a hook in a module, all that’s required is

  1. Create a function in your module’s .module file

  2. The function’s name should start with the name of your module, and then an underscore (modulea_)

  3. The function’s name should continue with the name of the hook to implement (helloworld)

  4. Implement whatever code you want in your new function

Let’s do the same in Module B. Open up Module b’s .module file, and enter the following

1
2
3
4
5
6
#File: sites/all/modules/moduleb/moduleb.module
<?php
function moduleb_helloworld()
{
    echo "<p>Our friends at ".__FUNCTION__." want to say Hello World</p>";
}

We’re editing moduleb.module, and have created a function name by combining moduleb (our module name) and helloworld (our Hook name). Reload working.php, and you’ll see two messages

Our friends at modulea_helloworld want to say, Hello World

Our friends at moduleb_helloworld want to say, Hello World

And that’s it. While this example is trivially simple, the possibilities and power of this sort of distributed function calling is what helps make Drupal such a powerful and customizable system.

Hook Return Values

Hopping back up to working.php, let’s change our invocation slightly

1
2
$results = module_invoke_all('helloworld');
var_dump($results);

Instead of just calling module_invoke_all, we’re calling it and assigning its results a variable. If you reload working.php, you’ll get something like

Our friends at modulea_helloworld want to say, Hello World

Our friends at moduleb_helloworld want to say, Hello World

array
    empty

Our call to module_invoke_all returned an empty array. Let’s change our two hook implementations slightly.

1
2
3
4
5
6
7
8
9
10
11
12
#File: sites/all/modules/modulea/modulea.module
...
function modulea_helloworld()
{
    return "<p>Our friends at ".__FUNCTION__." want to say, Hello World</p>";
}
...
#File: sites/all/modules/moduleb/moduleb.module
function moduleb_helloworld()
{
    return "<p>Our friends at ".__FUNCTION__." want to say, Hello World</p>";
}

Instead of echoing our strings, we’re returning them. Give the browser a refresh, and you should now see something like.

array
    0 => string '<p>Our friends at modulea_helloworld want to say, Hello World</p>' (length=65)
    1 => string '<p>Our friends at moduleb_helloworld want to say, Hello World</p>' (length=65)

Invoking a hook will always return an array, and that array will contain the values of each module’s hook implementation return value. if a hook implementation returns null (or lacks an explicit return) nothing is added to the return array. That’s why our initial return value was an empty array.

Let’s change Module A so it returns an array of strings.

1
2
3
4
function modulea_helloworld()
{
    return array("one","two","three");
}

You might expect your hook invocation to return a nested array. Refresh the page, and you should see.

array
    0 => string 'one' (length=3)
    1 => string 'two' (length=3)
    2 => string 'three' (length=5)
    3 => string '<p>Our friends at moduleb_helloworld want to say, Hello World</p>' (length=65)

Instead of returning a nested array, Drupal has merged the hook implementation’s return values into the invocations return array. If you use an array with non-sequenced keys, (an “associative array”) something similar happens

1
2
3
4
function modulea_helloworld()
{
    return array("foo"=>"one","baz"=>"two","bar"=>"three","function"=>__FUNCTION__);
}

Returns something like …

array
    'foo' => string 'one' (length=3)
    'baz' => string 'two' (length=3)
    'bar' => string 'three' (length=5)
    'function' => string 'modulea_helloworld' (length=18)
    0 => string '<p>Our friends at moduleb_helloworld want to say, Hello World</p>' (length=65) 

Returning an associative array merges the keys into our invocation’s return value. So what happens if there’s a key collision between two hook implementations? Give the following a try

1
2
3
4
5
6
7
8
9
10
11
12
#File: sites/all/modules/modulea/modulea.module
...
function modulea_helloworld()
{
    return array ("foo"=>"one","baz"=>"two","bar"=>"three","function"=>__FUNCTION__);
}
...
#File: sites/all/modules/moduleb/moduleb.module
function moduleb_helloworld()
{
    return array('function'=>__FUNCTION__);
}

Both Hook implementations are returning an array with a key named ‘function’. Reload the page, and you’ll see

array
    'foo' => string 'one' (length=3)
    'baz' => string 'two' (length=3)
    'bar' => string 'three' (length=5)
    'function' => 
        array
        0 => string 'modulea_helloworld' (length=18)
        1 => string 'moduleb_helloworld' (length=18)

So, if the keys collide, Drupal will merge the colliding keys into a nested array. When you’re invoking hooks in your Drupal code, it’s important to be aware of all the return values you could possibly get. When you’re implementing hooks (ex. for existing Drupal systems) it’s important to know what sort of return value the invoking code expects.

One last note: This merging of values described above only happens with PHP’s built in array type. If you’re using the standard library ArrayObject or something that implements ArrayAccess, Drupal will treat the returned value as an object, and merge the object itself into the array.

array
    0 => object (ArrayObject)[5]
    1 => object (ArrayObject)[5]
    etc ...

Again, remember Drupal’s pre-PHP 5 OOP history. When Drupal was created ArrayObject’s weren’t even a gleam in Rasmus’s eye.

Hooks vs. Events

Return values are where hooks differ from most of the traditional event/observer systems I’ve seen. In most systems, the core code will do something, and then issue an event to let outsiders know it happened. This way, when Event A happens in the core system, outside developers can have other things happen.

An event observer will, typically, not return a value to be used by the core system. Hooks, on the other hand, take an active role in defining the behavior of the core system. One way of thinking about is that Event/Observers say

Hey, just so you know, this happened

Whereas Hooks say

Hey, we’re doing this thing, do you want to be part of it?

That’s why I prefer to think of Hooking as distributing a function call over multiple include files. It’s a subtle different, but once you understand it the rest of Drupal will start making a little more sense.

For Design Patterns folks, ircmaxell over at Stack Overflow told me that Hooks are an implementation of the Mediator pattern.

Invoking Hooks

There’s a few more things to cover in regards to invoking hooks. First, there may be times when you don’t want to trigger all the implemented hooks in the system. This is most typically done when a hook implementation provides a valuable bit of programing logic you want to take advantage of. There may also be times where a full, all module invocation would change the system state in an undesirable way.

In an ideal world, all our systems and modules would be architected such that this sort of thing wasn’t needed. Since none of us are riding Unicorns, Drupal provides the module_invoke function

1
$results = module_invoke('modulea','helloworld');

The code above would call only our modulea_helloworld hook implementation. Use it sparingly, as you’re denying other modules the opportunity to “hook into” whatever part of the code you’re using.

Next up is parameters for hooks. Both module_invoke and module_invoke_all accept an unlimited number of extra parameters.

1
2
module_invoke_all('helloworld','one','two','three');
module_invoke('modulea','helloworld','one','two','three');

These parameters are, in turn, passed on to the hook implementations. Consider the following

1
2
3
4
function modulea_helloworld($param, $another_param, $third)
{  
    return array('joined'=>implode('##',array($param, $another_param, $third)));
}

The strings ‘one’,’two’, and ‘three’ are passed in as $param, $another_param, $third.

Behind the scenes, Drupal implements this by using PHP’s func_get_args function. This implementation presented a problem, which brings us to the last thing we want to cover on hook invocation.

There are times where you want your hook implementation to be able to change some data structure you’re working with. In our 2011/PHP 5 world, we’d most likely achieve this by passing in an object.

1
2
3
4
$object = new SomeClass();
module_invoke('modulea','helloworld',$object);
//we've got an object that's been modified by our hook implementations
$object;

Objects in PHP 5 are references, which means hook implementations could make all the changes they want. However, remember the context of Drupal. It’s a PHP 4 world, and everything is pass by copy. Passing in big arrays (or even PHP 4’s primitive objects) to functions and then returning them comes with potential performance problems, as another copy of the array or object needs to be made. PHP offers a solution to this, allowing you to declare your function in such a way that parameters are passed by reference

1
2
3
4
5
function somefunction(&$param)
{
    //the & in front of the parameters means if we change it down
    //in the function, it will be changed in the main program.
}

However, because module_invoke and module_invoke_all use func_get_args, it’s impossible to pass values via the hook invocation by reference.

This led to the Drupal community coming up with two solutions

The first is the drupal_alter function, which is an alternative way to invoke hook implementations. Doing

1
drupal_alter('helloworld',$data);

would invoke the hook helloworld_alter, with the variable $data being passed into hook implementations by reference.

Read that again and make sure the following sinks in. There’s no way to invoke the hook named hello_world using drupal_alter. The drupal_alter function forces you to name you hooks with the _alter naming convention. This is useful to people implementing the hooks, as they can expect that any hook ending with the name alter with have parameters passed by reference. In Drupal 7 you can pass up to three parameters in as reference parameters.

1
drupal_alter('helloworld',$data, $foo, $bar);

Another thing to watch out for is drupal_alter doesn’t return any values. It’s strictly for allowing modules to hook in and alter variables.

Breaking Encapsulation

Because of the constraints on drupal_alter, there’s second alternative invocation method, which will give some of you the encapsulation jibblies. However, it’s in common use among the Drupal community; even core modules use it. Whether or not you agree with the post you’ll want to be familiar with it.

Drupal provide a function named module_implements which will return a list of modules that implement a particular hook. Try the following

1
2
$results = module_implements('helloworld');
var_dump($results);

and you should get an array with your two modules

array
  0 => string 'modulea' (length=7)
  1 => string 'moduleb' (length=7)

When someone wants to pass a variable by reference into a hook implementation and not use the _alter naming syntax, or they want to hook implementation to return something, you’ll see something like this.

1
2
3
4
5
6
7
8
9
10
11
12
$hook = 'helloworld';
$etc = array('one','two');
$param_by_ref = 'four';
$all = array();
foreach(module_implements($hook) as $module)
{
    $function = $module . '_' . $hook;
    $single_hook_return = $function($param_by_ref, $etc);
    $all = array_merge($all, $single_hook_return);
}
var_dump($all);
echo '<p>Done</p>';

Jibblies! The functionality provided by module_invoke_all is replicated here, but outside the function. This breaks encapsulation, and we’re seeing another example of the black/white split in the core Drupal philosophy of “you’re either not a programmer, or you know the system explicitly”. Breaking module_invoke_all’s encapsulation of the Hook system isn’t a big deal, because who needs a level of encapsulation between a programmer and the core system? AMIRITE?

The problem with this being in wide use is it essentially forces the hook implementation that exists today to exist for all time, or else backward compatibility with code that uses this be broken. That’s a trade off the community is willing to make, so be ready to see code like this in the wild.

Consumer of Hooks

All of that said, most of your time as a day-to-day Drupal programmer, especially in the beginning, will be spent implementing hooks provided by the core system and contrib (third party) modules. The Core Drupal APIs and Systems that are heavily promoted are all implemented, internally, using the module system. Each API may provide a number of different hooks for you to implement, each with it’s own conventions. Some may want you to alter a variable passed by reference, others may expect you to return values in a specific format.

Again Drupal’s various sub-systems use the module system to implement functionality. Third parties can use the module system to change default system behavior. Each sub-system may have a different design philosophy. This difference between sub-systems is, I think, the reason some people group Drupal into the “hard to learn” category. Instead of learning a single system, you need to learn many. It’s Hooks that tie them all together.

Like most steep learning curves, once you understand a particular API its power quickly makes the time investment worth it. Knowing how the module system works is the first step towards mastering Druapl. It’s the system all other systems use and rely on, and once you’re comfortable with it, you can start invoking hooks in your own modules, which will in turn allow other developers to use your module as an API. This is a key philosophy of the Drupal community, and one of the reasons we’re still talking about the platform more than a decade after it’s creation.

注:原文链接http://alanstorm.com/drupal_module_hooks

posted @ 2015-04-23 10:17  amw863  阅读(259)  评论(0编辑  收藏  举报