Rewrite Rules in WordPress und warum dessen Cache nicht im Frontend geleert werden soll

Gerade bin ich über ein Problem gestoßen welches es eigentlich nicht geben sollte: flush_rwrite_rules() im Frontend ausführen.

Was sind Rewrite Rules in WordPress?

So genannte “Rewrite Rules” sind Regeln, die aufzeigen, wohin ein Aufruf geleitet werden soll. Sie finden immer dann Anwendung, wenn “schöne Links” bzw. “Pretty Permalinks” aktiv sind: wenn also unter Einstellungen -> Permalinks etwas anders als “Einfach” ausgewählt wurde:

WordPress Einstellungsfenster für Permalinks
Hier wurden die schönen Permalinks aktiviert.

Nun kann ein Blogpost eben (wie das Beispiel zeigt) nicht über eine eindeutige ID sondern über den so genannten Post-Slug aufgerufen werden. Denn folgendes ist doch viel schöner:

https://wp-typ.de/beispielbeitrag/

anstatt das hier:

https://wp-typ.de?p=456

Interne Umleitung an die index.php

Intern passiert dabei folgendes: über die passende Rewrite-Regel wird festgestellt, was an die index.php von WordPress übergeben werden soll. Wird also z.B. folgende URL aufgerufen:

https://wp-typ.de/uncategorized/hello-world/

Wird intern folgende Regel ausgeführt (Regulärer Ausdruck):

(.+?)/([^/]+)(?:/([0-9]+))?/?$

Was dazu führt, dass der Aufruf wie folgt an die index.php weitergegeben wird:

category_name=uncategorized&name=hello-world&page=

Nun kennt WordPress zwei Dinge: Den Parameter category_name und name. Also den Post- und der Kategorie-Slug. Beide sind in der Regel WordPress-weit eindeutig und können dadurch in der Datenbank identifiziert werden.

Wer noch tiefer einsteigen will, der kann sich ein kleines Plugin schreiben, welches die globale Variable $wp ausgibt:


<?php
/*
Plugin Name: Rewrite-Test
Description: Rewrite-Test
*/

add_action( 'wp', function ( $wp ) {
 if ( is_admin() ) {
 return;
 }

 var_dump( $wp );
 die();
} );

Dort steht detailliert, was passiert:

WP::__set_state(array(
 'public_query_vars' => 
 array (...),
 'private_query_vars' => 
 array (...),
 'extra_query_vars' => 
 array (),
 'query_vars' => 
 array (
 'page' => '',
 'name' => 'hello-world',
 'category_name' => 'uncategorized',
 ),
 'query_string' => 'name=hello-world&category_name=uncategorized',
 'request' => 'uncategorized/hello-world',
 'matched_rule' => '(.+?)/([^/]+)(?:/([0-9]+))?/?$',
 'matched_query' => 'category_name=uncategorized&name=hello-world&page=',
 'did_permalink' => true,
))

Eigene Rewrite-Regeln

Natürlich ist es möglich, eigene Rewrite-Regeln via Plugin zu definieren. Und viele machen davon Gebrauch. So z.B. mein Touren-Plugin welches bei xc-ski.de zu finden ist:

add_action( 'init', 'xc_tours_rewrite_rules', 5 );

function xc_tours_rewrite_rules() {
 // match a single tour (with leading country)
 add_rewrite_rule( '^loipen/([0-9a-zA-Z/-]*)(/)(.?)+-l([0-9]+)/??', 'index.php?page_id=' . XC_TOURS_DETAILS_PAGE_ID . '&xc_tour_id=$matches[4]&xc_tour_region=$matches[1]', 'top' );
}

Wie man oben erkennen kann, muss diese Regel mit jedem ‘init’ hinzugefügt werden. Aber ein großer Stolperstein ist immer wieder folgendes: das Erstellen einer Regel heißt nicht automatisch, dass diese sofort wirksam ist. Der Grund ist, dass WordPress einen internen Rewrite-Cache hat (gespeichert in der wp_options Tabelle im Datensatz “rewrite_rules”). Deswegen findet man in den Plugins meist auch folgende Funktionen:

register_activation_hook( __FILE__, function () {
 flush_rewrite_rules();
} );

register_deactivation_hook( __FILE__, function () {
 flush_rewrite_rules();
} );

Mit flush_rewrite_rules() wird der Cache der Rewrite Rules neu aufgebaut. Das hat zur Folge, dass neu hinzufügte Regeln dann auch bei der nächsten Page-Impression berücksichtigt werden.

Löschen des Rewrite-Caches im Frontend = böse

Fatale Folgen kann aber das Leeren des Rewrite-Caches im Frontend haben. Bei einigen meiner Kunden nutze ich die Möglichkeit, MU-Plugins (Must-Use) einzubinden. Sie werden geladen, bevor alle anderen Plugins geladen werden. Mit dem Filter option_active_plugins lassen sich dann nachträglich einzelne Plugins deaktivieren. Ich mache das gerne mit Plugins, die nur im Backend benutzt werden und im Frontend so nix zu suchen haben (warum PHP-Code ausführen, der gar nicht benötigt wird?).

Oben genanntes Touren-Plugin ist nur auf den dafür vorgesehen Unterseiten aktiviert. Konkret hatten wir ein Problem mit dem MyMail Plugin aus CodeCanyon. Dort findet man im Frontend folgende Zeilen:

if ( is_404() ) {
 if ( preg_match( '#^(index\.php/)?mymail/#', $wp->request ) && !isset( $_REQUEST['mymail_error'] ) ) {
 flush_rewrite_rules();
 $redirect_to = add_query_arg( array( 'mymail_error' => 1 ), home_url( $wp->request ) );
 wp_redirect( $redirect_to, 302 );
 exit;
 }
}

Wie man sieht hatte der Programmierer eigentlich eine gute Absicht. Wird ein 404-Fehler erzeugt wird geprüft, ob es such um eine MyMail-Seite handeln könnte. Falls ja, werden die Rewrite-Rules gelöscht und eine Umleitung findet statt. Da mein Touren-Plugin auf 404-Seiten nicht aktiv ist, werden dessen Rewrite-Rules aus dem Cache geleert und sind dann nicht mehr aufrufbar.

Plugins im Frontend Deaktivieren: ja oder nein?

Vielleicht kommt dann sofort auch die Frage auf, ob es überhaupt sinnvoll ist, einzelne Plugins auf bestimmten Unterseiten zu deaktivieren. Ich sage ja. Und zwar ganz klar aus Performance-Gründen. Aber auch deswegen, weil WordPress letztlich genau diese Möglichkeit mit dem option_active_plugins Hook bietet. Würde man Plugins im Backend deaktivieren, würde sie WordPress komplett deaktivieren, wenn man das nächste Mal die Plugins-Seite aufruft. Letzteres macht also keinen Sinn. Ersteres allerdings schon.

Rewrite-Cache richtig löschen

Grundsätzlich gilt also: kein flush_rewrite_rule() im Frontend! Stattdessen sollte man selbst ein Flag setzen und sie dann im Backend löschen lassen. So macht das z.B. das populäre Plugin CustomPostTypeUI aber auch – und das ist hoch interessant – das MyMail Plugin selbst:

Bei einem Update von einer alten Version setzt es die Option:

$mymail_options['_flush_rewrite_rules'] = true;

Dummerweise wird dann allerdings wieder im init das Flushing selbst ausgeführt:

add_action( 'init', array( &$this, 'init' ) );

public function init() {
 ...
 if ( mymail_option( '_flush_rewrite_rules' ) ) {
   flush_rewrite_rules( true );
   mymail_update_option( '_flush_rewrite_rules', false );
 }

}

Besser wäre also gewesen:

add_action( 'admin_init', array( &$this, 'admin_init' ) );

public function admin_init() {
 ...
 if ( mymail_option( '_flush_rewrite_rules' ) ) {
   flush_rewrite_rules( true );
   mymail_update_option( '_flush_rewrite_rules', false );
 }

}

Update:

Bitte…bitte…bitte! Macht nicht folgendes:

update_option('rewrite_rules', ''); # das ist böse!

oder gar:

delete_option('rewrite_rules'); # das ist auch böse!

Das führt zwar dazu, dass WordPress die Regeln bei der nächsten Page-Impression neu aufbaut aber es ist nicht sichergestellt, ob alle Plugins aktiv sind.