Since we have covered some of the principles behind the Drupal theme system—most notably, the separation of concerns—let's go a bit deeper and take a look at how they are actually put into practice. This all starts with the theme hooks. Yes, Drupal always loves to call things hooks.
Theme hooks define how a specific piece of data should be rendered. They are registered with the theme system by modules (and themes) using hook_theme(). In doing so, they get a name, a list of variables they output (the data that needs to be wrapped with markup), and other options.
The modules and themes that register theme hooks also need to provide an implementation (one that will be used by default). In Drupal 7, this was done in the following two ways: either via a PHP function that returned a string (markup) or a PHPTemplate template file. Both were equally important, but the latter was always more "correct" in my (and many people's) opinion. This is also supported by the fact that the function approach has been completely ditched in Drupal 8 in favor of Twig templates. Also, together with a complete overhaul of the theme system, almost everything is now output via a Twig template file, which is great.
As an example, let's take a look at two common ways of registering a theme hook we'll often find. For this, we will use Drupal core examples that already exist:
function hook_theme($existing, $type, $theme, $path) {
return [
'item_list' => array(
'variables' => array('items' => array(), 'title' => '', 'list_type' => 'ul', 'wrapper_attributes' => array(), 'attributes' => array(), 'empty' => NULL, 'context' => array()),
),
'select' => array(
'render element' => 'element',
),
];
}
In the preceding hook_theme() example, I included two theme hooks from Drupal core. One is based on variables, whereas the other is based on a render element. There are, of course, many more options that can be defined here, and I strongly encourage you to read the Drupal.org API documentation page for this hook.
However, right off the bat you can see how easy it is to register a theme hook. In the first case, we have item_list, which, by default (if not otherwise specified), will map to the item-list.html.twig file for outputting the variables. In its definition we find the variables it uses, with some handy defaults in case they are not passed in from the client. The second theme hook is select, which doesn't use variables but a render element (which we will discuss soon). Also, its template file is easy to determine based on the name: select.html.twig. I encourage you to check out both these template files in the core code (provided by the System module).
In addition to the actual implementation, the modules and themes that register a theme hook can also provide a default template preprocessor. The responsibility of this is to "preprocess" (that is, prepare) data before being sent to the template. For example, if a theme hook receives an entity (a complex data object) as its only variable, a preprocessor can be used to break that entity into tiny pieces that are needed to be output in the template (such as title and description).
Template preprocessors are simple procedural functions that follow a naming pattern and are called by the theme system before the template is rendered. As I mentioned earlier, the modules and themes that register a theme hook can also provide a default preprocessor. So, for a theme hook named component_box, the default preprocessor function would look like this:
function template_preprocess_component_box(&$variables) {
// Prepare variables.
}
The function name starts with the word template to denote that it is the original preprocessor for this theme hook, then follows the conventional preprocess word, and ends with the name of the theme hook. The argument is always an array passed as a reference and contains some info regarding that theme hook, and more importantly, the data variables that were defined with the theme hook and passed to it from the calling code. That is what we are usually working with in this function. Also, since it's passed by a reference, we don't return anything in this function, but we always manipulate the values directly in the $variables array. In the end, the template file can print out variables named after the keys in this array. The values will be, of course, the values that map to those keys.
Another module (or theme) can override this preprocessor function by implementing its own. However, in its naming, it needs to replace the word template with the module name (to avoid collisions). If one such override exists, both preprocessors will be called in a specific order. The first is always the default one, followed by the ones defined by modules and then the ones defined by themes. This is another great extension point of Drupal because altering data or options found inside the preprocessor can go a long way in customizing the existing functionality to your liking.
As an alternative to following the previous naming convention, you also have the option to register the preprocessor function names in the hook_theme() definition when you register it. However, I recommend that you stick to the default naming convention because it's much easier to spot what the purpose of the function is. As you become more advanced, you'll, in turn, appreciate being able to quickly understand these convention functions at a quick glance.
I mentioned a bit earlier that modules and themes can also override theme hooks defined by other modules and themes. There are two ways to do this. The most common one is for a theme to override the theme hook. This is because of the rationale I was talking about earlier—a module defines a default implementation for its data, but a theme can then take over its presentation with ease. Also, the way themes can override a theme hook is by simply creating a new Twig file with the same name as the original and placing it somewhere in its templates folder. If that theme is enabled, it will be used instead. A less common but definitely valid use case is for a module to override a theme hook defined by another module. For example, this might be because you need to change how data is rendered by a popular contributed module. To achieve this, you will need to implement hook_theme_registry_alter() and change the template file used by the existing theme hook. It's also worth adding that you can change the entire theme hook definition using this hook if you want, not just the template. Also, since we mentioned this hook, note that theme hooks, upon definition, are stored and cached in a theme registry for optimized performance, and that registry is what we are altering with this hook. This also means that we regularly need to clear the cache when we make changes to the theme registry.
All this is good and fine, but the business logic still has to interact with the theme system to tell it which particular theme hook to use. In Drupal 7, we had the theme() function which took the hook name as an argument and was responsible for everything: determining which template file (or function) to use, calling the preprocessors, processors, and so on. In Drupal 8, the theme() function no longer exists and has been replaced with a more robust system based on render arrays, which contain the theme hook information, the variables, and any other metadata on how that component needs to be rendered. We will also talk about render arrays in this chapter.