Subthemes und Templates in Drupal 7

Ein »Quick & Dirty«-Ansatz, um schnell vorhandene Themes an eigene Bedürfnisse anzupassen

Grundlagen

Sobald man ein schönes Theme auf der Drupal-Seite gefunden, heruntergeladen und aktiviert hat, stellt sich heraus, dass es vielleicht sehr gut passt, aber so eine winzige Kleinigkeit dann doch anders sein sollte. Viele Themes sind konfigurierbar, aber oftmals reicht es nicht. Da ist die Versuchung groß, einfach die entsprechenden Stellen im Theme selbst zu verändern (zu »hacken«) und das Problem damit zu lösen. Allerdings hat dieser Ansatz gewaltige Nachteile, wenn einmal das ursprüngliche Theme aktualisiert werden sollte, da dann kaum noch nachvollziehen kann, wo genau die Änderungen waren.

Drupal hat zu diesem Zweck jedoch einen sehr einfachen und schnellen Mechanismus, der »sub-theme« genannt wird.

Drupal 7 verlangt minimal für ein Theme ein Verzeichnis unter sites/all/themes (bzw. unter der entsprechenden Seite), das eine .info-Datei, in der das Theme beschrieben wird, und eine template.php enthalten muss, wobei letztere auch leer sein kann. Die Benennung des Themes orientiert sich dabei im Grunde an denselben System wie Module auch, sprich es sind nur Kleinbuchstaben, Zahlen und der Unterstrich _ erlaubt.

Die .info-Datei muss mindestens folgende Zeilen enthalten:

{syntaxhighlighter class="brush: php"}
name = Neues Theme
core = 7.x
{/syntaxhighlighter}

In der ersten Zeile wird ein Name festgelegt, der so dann auch im der Verwaltung angezeigt wird. core weist darauf hin, dass das Theme für Drupal 7 gedacht ist. Nun noch eine leere template.php dazu und schon taucht das neue Theme in der Verwaltungsansicht auf und kann aktiviert werden.

CSS-Anpassungen im Subtheme

Natürlich ist das Endergebnis hässlich, denn das neue Theme definiert ja nichts, aber ich wollte ja auch nicht beschreiben, wie man ein komplett neues Theme angelegt, sondern nur, wie man ein bestehendes anpasst. Dazu muss lediglich eine Zeile base theme in der .info-Datei ergänzt werden. Für diese Seite hier ist das das Theme Corolla, was wiederum ein Subtheme von Adaptive Theme ist. Wie man hier sieht, können Subthemes beliebig geschachtelt werden, also z. B. bei Multi-Seiten ein Basis-Theme, das gemeinsame Elemente enthält, und jeweils ein Sub-Theme pro Unterseite.

Somit ergibt sich für die neues_theme.info:

{syntaxhighlighter class="brush: php"}
name = Neues Theme
core = 7.x
base theme = corolla
{/syntaxhighlighter}

Dies genügt. Nach einem Leeren des Cache sieht die Seite schon deutlich besser aus – allerdings fehlen noch sämtliche CSS-Definitionen. Um diese ebenfalls zu übernehmen, muss nur eine einzige CSS-Deklaration in der .info stehen (was ohnehin der Fall sein sollte, denn man will ja quasi immer irgend etwas anpassen):

{syntaxhighlighter class="brush: php"}
name = Neues Theme
core = 7.x
base theme = corolla
stylesheets[all][] = neues_theme.css
{/syntaxhighlighter}

Nun kann man in der neues_theme.css beliebige CSS-Regeln hinzufügen und abändern, was vermutlich bereits für die meisten Theme-Anpassungen reichen sollte.

Template-Hooks

Nicht alle Dinge lassen sich über eine eigene CSS-Datei lösen, z.B. lassen sich keine externen CSS-Dateien angeben, da die Aggregation von Drupal diese entfernt. Hier kommt die template.php ins Spiel, die bisher leer war.

Wie für jedes Drupal-Modul, so gibt es auch für Themes spezielle Hooks, die je nach den Bedürfnissen implementiert werden können. Um etwa externe CSS-Dateien in den Header der Seite zu schreiben, ist beispielsweise hook_preprocess_html() in der template.php vonnöten:

{syntaxhighlighter class="brush: php; auto-links: false"}
<?php

function neues_theme_preprocess_html(&$vars) [
drupal_add_css('http://fonts.googleapis.com/css?family=Belgrano', 'external);
}
{/syntaxhighlighter}

Wie man sehen kann, binde ich hier die Schrift Belgrano der Google Webfonts ein, die ich dann in neues_theme.css unmittelbar verwenden kann:

{syntaxhighlighter class="brush: css"}
body {
font-family: 'Belgrano', serif;
}
{/syntaxhighlighter}

Doch mittels Hooks geht noch viel mehr. Auf dieser Seite habe ich etwa eine Auflistung meiner Fan-Romane, die in unterschiedlichsten Formaten bei den PROC Stories herunter geladen werden können. Dazu müssen anhand der Story-ID spezielle URLs konstruiert werden, die folgende Form haben: http://www.stories.proc.org/‹Story-ID›/‹Format›. Dies ließe sich auch (äußerst umständlich) via Views realisieren, jedoch wesentlich schneller und einfacher geht es, indem man einfach die Ausgabe eines Feldes mittels hook_preprocess_field() umschreibt:

{syntaxhighlighter class="brush: php"}
function aki_homepage_preprocess_field(&$vars) {
$element = $vars['element'];
if ($element['#field_name'] == 'field_proc_stories_id') {
$id = $element['#items'][0]['value'];
$pattern = '%s';
$links = array(
sprintf($pattern, $id, 'html', 'HTML'),
sprintf($pattern, $id, 'pdf', 'PDF'),
sprintf($pattern, $id, 'lit', 'LIT'),
sprintf($pattern, $id, 'rb', 'Rocket'),
sprintf($pattern, $id, 'oeb', 'Open eBook'),
sprintf($pattern, $id, 'prc', 'Mobi'),
sprintf($pattern, $id, 'pdb', 'Palm'),
sprintf($pattern, $id, 'text', 'Text'),
sprintf($pattern, $id, "$id.html", "Online lesen")
);
$vars['items'][0]['#markup'] = implode(' | ', $links);
}
}
{/syntaxhighlighter}

Diese Funktion aus dem Theme meiner Homepage (mit dem kreativen Namen aki_homepage *g*) holt sich aus dem übergebenen Variablen $vars das Element und prüft, ob es sich dabei um field_proc_stories_id handelt. Falls ja, werden aus der ID, also den Wert des Feldes $element['#items'][0]['value'] die entsprechenden Download-Links erzeugt und alles dann als String unter $vars['items'][0]['#markup'] abgelegt. Letzteres ist genau das, was das Standard-Template für CCK-Felder ausgibt. Dazu im nächsten Abschnitt mehr.

Template-Dateien

Die Template-Dateien mit kryptischen Namen und der Endung .tpl.php scheinen eins der größsten Mysterien zu sein, dass Drupal zu bieten hat, kann man doch einerseits das Aussehen so ziemlich jeden Elements – bis hin zur gesamten Seite – anpassen, andererseits scheint Drupal die Dateien auf »magische« Weise zu finden – oder eben nicht.

Der einfachste Ansatzpunkt, falls man das Aussehen bzw. die Ausgabe eines entsprechenden Typs komplett ändern möchte, ist es, einfach die Template-Datei in das eigene (Sub-)Theme zu kopieren und anzupassen, also z.B. modules/field/theme/field.tpl.php, die im Original so ausschaut:

{syntaxhighlighter class="brush: php"}

/**
* @file field.tpl.php
* Default template implementation to display the value of a field.
*
* This file is not used and is here as a starting point for customization only.
* @see theme_field()
*
* Available variables:
* - $items: An array of field values. Use render() to output them.
* - $label: The item label.
* - $label_hidden: Whether the label display is set to 'hidden'.
* - $classes: String of classes that can be used to style contextually through
* CSS. It can be manipulated through the variable $classes_array from
* preprocess functions. The default values can be one or more of the
* following:
* - field: The current template type, i.e., "theming hook".
* - field-name-[field_name]: The current field name. For example, if the
* field name is "field_description" it would result in
* "field-name-field-description".
* - field-type-[field_type]: The current field type. For example, if the
* field type is "text" it would result in "field-type-text".
* - field-label-[label_display]: The current label position. For example, if
* the label position is "above" it would result in "field-label-above".
*
* Other variables:
* - $element['#object']: The entity to which the field is attached.
* - $element['#view_mode']: View mode, e.g. 'full', 'teaser'...
* - $element['#field_name']: The field name.
* - $element['#field_type']: The field type.
* - $element['#field_language']: The field language.
* - $element['#field_translatable']: Whether the field is translatable or not.
* - $element['#label_display']: Position of label display, inline, above, or
* hidden.
* - $field_name_css: The css-compatible field name.
* - $field_type_css: The css-compatible field type.
* - $classes_array: Array of html class attribute values. It is flattened
* into a string within the variable $classes.
*
* @see template_preprocess_field()
* @see theme_field()
*/
?>

>
>

>
$item): ?>
>

{/syntaxhighlighter}

Wie man sehen kann, ist es größtenteils selbsterklärend. Die verfügbaren Variablen und deren Inhalt werden aufgelistet und erklärt. In den allermeisten Fällen sollte man allerdings mit der Bezeichnung $label und den eigentlichen Inhalt $items auskommen. Eine mögliches Template könnte etwa so aussehen:

{syntaxhighlighter class="brush: php"}

{/syntaxhighlighter}

Dies ist natürlich ein äußerst schlechtes Beispiel, da davon ausgegangen wird, dass jedes Feld einen Label hat und der eigentliche Inhalt gnadenlos in eine Auflistung gepackt wird, aber ich hoffe, das Grundprinzip ist klar.

Was nun aber, wenn man nur bestimmte Dinge ändern möchte, z. B. die Ausgabe des Twitter-Blocks rechts auf meiner Seite? Wenn man Glück hat, liefert das entsprechende Modul gleich die passenden Templates mit, die man dann wie oben angegeben kopieren und anpassen kann (ist etwa bei twitter_pull) der Fall, was macht man aber, wenn das nicht der Fall ist, etwa bei einem selbst erstelltem Feld?

Dazu muss man verstehen, wie Drupal, bzw. die standardmäßige Theme-Engine phptemplate nach einem passenden Renderer sucht. Dieser Renderer kann entweder ein Hook oder eine Template-Datei sein. Beide Ansätze sind gleichwertig, wobei jedoch Hooks als PHP-Funktionen üblicherweise bis zu fünfmal schneller abgearbeitet werden, Template-Dateien normalerweise hingegen sehr viel übersichtlicher zu warten sind.

Um nun etwa eine Darstellung für das CCK-Feld field_untertitel für den Inhaltstyp article zu suchen, werden sukzessive folgende Template-Dateien auf Existenz geprüft:

  • field--field-untertitel--article.tpl.php
  • field--field-untertitel.tpl.php
  • field--article.tpl.php
  • field.tpl.php

Sobald ein erster Treffer erfolgt, wird das entsprechende Template aufgerufen. Sollte überhaupt nichts gefunden werden (bei Standardelementen unmöglich), bricht Drupal mit einer Fehlermeldung ab.

Um nun also das Untertitel-Feld – und nur das Untertitel-Feld – anderweitig unabhängig vom Inhaltstyp darzustellen, muss eine Datei mit den Namen field--field-untertitel.php angelegt werden:

{syntaxhighlighter class="brush: php"}

{/syntaxhighlighter}

Nicht sonderlich hübsch, aber das Ergebnis sollte nicht zu übersehen sein. ;-)

Andere Elemente funktionieren völlig analog. Möchte man etwa das Aussehen des Inhaltstyps article verändern, kann man die passende node.tpl.php kopieren und anpassen, genauso funktioniert das auch mit Blöcken, Menüs usw.

Richtig komplex wird es hingegen, wenn Views über Templates anpassen möchte, da dort gern so richtig komplizierte Dateinamen herauskommen wie etwa views-view-fields--last-blog-entrys.tpl.php. Das Schema ist auch hier genau wie oben beschrieben, aber durch die zahlreichen Verschachtelungen ist nicht unbedingt ersichtlich, wie das Template genau heißen muss. Glücklicherweise bietet Views in der Bearbeitungsansicht selbst eine Übersicht an, indem man rechts auf »Advanced« und dann auf »Theme: Information« klickt. Nun wird eine Liste aller möglichen Templates ausgegeben, aus der man dann auswählen und die passende Datei anlegen kann.

Flattr