Eine der Fragen, die ich in verschiedenen Gelegenheiten häufig höre, ist diese: „Welches Plugin verwendest Du auf jeder Website?“. Darauf habe ich normalerweise keine Antwort, da es meiner Meinung nach kein „Pflicht-Plugin“ gibt. Aber ein Plugin, das ich fast immer einsetze, zumindest während der Entwicklung einer Website, ist das Plugin Query Monitor. Es hilft mir sehr dabei, Probleme mit der Website zu finden. Ich habe es sogar auf einigen Produktionsseiten laufen.
Da ich an einigen Projekten arbeite, die auf WordPress VIP gehostet werden, wurde ich gebeten, an einigen VIP-Trainings teilzunehmen. In einer dieser Training ging es um verschiedene Möglichkeiten, wie man eine Website debuggen kann. Und natürlich ist Query Monitor auch auf WordPress-VIP-Websites verfügbar.
Erweiterung des Query Monitor Plugins
Da ich Query Monitor häufig benutze, bin ich mit den meisten Panels davon sehr vertraut. Einige davon benutze ich ständig, andere eher selten. Aber ich wusste bisher nicht, dass man „einfach“ eigene Panels mit eigenen Debugging-Daten zu Query Monitor hinzufügen kann. Einige Plugins verwenden diese Möglichkeit schon, um Informationen hinzuzufügen. Andere stellen zusätzliche „Debugging Plugins“ bereit, um die Panels hinzuzufügen. Auf der Query Monitor Website findet ihr eine Liste dieser „Add-On Plugins“.
In dem VIP-Training gab es ein Beispiel dafür, wie man ein neues Panel mit eigenen Daten hinzufügt. Das hat mich so fasziniert, dass ich es direkt mal ausprobieren wollte. Ich habe auch einen schönen Anwendungsfall für meinen Blog gefunden. Ich verwende MultilingualPress, um meine Blogbeiträge zu übersetzen. Daher fand ich die Idee spannend, eine Liste der übersetzten Inhalte für Seiten oder Beiträge zu sehen. Genau das werde ich hier also demonstrieren.
Sammeln der Daten
Das Query Monitor Plugin ist in OOP (objektorientierte Programmierung) geschrieben und das erste, was wir implementieren müssen, ist eine Klasse, die die QM_DataCollector
Klasse von Query Monitor erweitert. Diese sammelt die Daten, die wir später anzeigen wollen.
Die Daten werden in einem „Storage“ gespeichert. Wir müssen keinen eigenen implementieren, aber ich wollte eine vollständige Lösung schreiben, also habe ich zuerst diese Implementierung einer QM_Data-Klasse
erstellt:
class QM_Data_MLP_Translations extends QM_Data {
/**
* @var Translation[]
*/
public $translations;
}
Die MultilingualPress-API verwendet eine eigene Klasse Translation
, von der wir ein Array dieser Objekte in unserer Datenklasse verwenden. Jetzt können wir unseren QM_DataCollector
implementieren:
class QM_Collector_MLP_Translations extends QM_DataCollector {
public $id = 'mlp_translations';
private $current_locale;
public function __construct() {
parent::__construct();
$this->current_locale = get_locale();
}
public function get_storage(): QM_Data {
return new QM_Data_MLP_Translations();
}
public function process(): void {
$args = TranslationSearchArgs::forContext(
new WordPressContext()
)->forSiteId( get_current_blog_id() )->includeBase();
$translations = resolve( Translations::class )->searchTranslations( $args );
$this->data->translations = [];
foreach ( $translations as $translation ) {
if ( $translation->language()->locale() !== $this->current_locale
&& $translation->remoteContentId() !== 0 ) {
$this->data->translations[] = $translation;
}
}
}
}
Jeder QM_DataCollector
benötigt eine $id
Eigenschaft. Wir verwenden hier den Wert mlp_translations
mit einem passenden Vendor-Präfix. Im Konstruktor speichern wir auch die aktuelle „Locale“ ab, da es aus irgendwelchen Gründen später auf die Locale aus dem Profil geändert wurde. In der Funktion get_storage()
verwenden wir das QM_Data
Objekt, das wir zuerst implementiert haben.
Der wichtigste Teil ist die Funktion process()
. Hier sammeln wir die Daten und fügen sie unserem Storage hinzu. Die ersten paar Zeilen müsst ihr nicht verstehen, da dies die spezifischen Funktionen von MultilingualPress sind, um eine Liste aller Übersetzungen zu einem Beitrag oder einer Seite zu erhalten. Die Sammlung eurer Daten kann anders aussehen, aber vermutlich werdet ihr Ende eine Schleife haben, um die Daten, die ihr verwenden wollt, in $this->data
einzufügen. In meinem Fall filtere ich die aktuelle Übersetzung heraus und füge auch nur die mit einer remoteContentId
hinzu, was mir nur „Einzel-Inhalte“ liefern würde.
Initialisierung des Collectors
Der letzte Schritt besteht darin, das „Collector“-Objekt in einem Filter zu initialisieren. Dies wird mit dem folgenden Filter durchgeführt:
function register_qm_collector_mlp_translations( array $collectors ) {
$collectors['mlp_translations'] = new QM_Collector_MLP_Translations();
return $collectors;
}
add_filter( 'qm/collectors', 'register_qm_collector_mlp_translations', 10 );
Nichts wirklich Ausgefallenes hier. Nachdem wir nun unseren „Collector“ mit den Daten haben, die wir anzeigen wollen, können wir uns ansehen, wie wir das umsetzen.
Hinzufügen unserer Daten zum Query Monitor
Query Monitor verfügt über mehrere Panels. Wir können entweder ein eigenes Panel hinzufügen oder wir fügen Daten zu einem bestehenden Panel als „Child Panel“ hinzu. Ich habe mich für den zweiten Ansatz entschieden, da es mir logischer erschien, meine Daten dem Panel „Sprachen“ hinzuzufügen. Hier ist der vollständige Code (ohne Kommentare):
class QM_Output_MLP_Translationsextends QM_Output_Html {
protected $collector;
public function __construct( QM_Collector $collector ) {
parent::__construct( $collector );
add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 20 );
}
public function name() {
return __( 'Translations', 'multilingualpress' );
}
public function output() {
$data = $this->collector->get_data();
if ( empty( $data->translations ) ) {
$this->before_non_tabular_output();
$notice = __(
'No translations for this content.',
'multilingualpress-qm'
);
echo $this->build_notice( $notice );
$this->after_non_tabular_output();
return;
}
$id = sprintf( 'qm-%s', $this->collector->id );
$this->before_tabular_output( $id );
$this->output_translation_table( $data->translations );
$this->after_tabular_output();
}
protected function output_translation_table( array $translations ): void {
echo '<thead>';
echo '<tr>';
printf( '<th>%s</th>', esc_html__( 'Locale', 'multilingualpress-qm' ) );
printf( '<th>%s</th>', esc_html__( 'Title', 'multilingualpress-qm' ) );
printf( '<th>%s</th>', esc_html__( 'Post ID', 'multilingualpress-qm' ) );
printf( '<th>%s</th>', esc_html__( 'Site ID', 'multilingualpress-qm' ) );
echo '</tr>';
echo '<tbody>';
foreach ( $translations as $translation ) {
echo '<tr>';
printf(
'<th scope="row"><code>%s</code></th>',
esc_html( $translation->language()->locale() )
);
printf(
'<td class="qm-ltr">%s</td>',
$this->build_link(
$translation->remoteUrl(),
$translation->remoteTitle()
)
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $translation->remoteContentId() )
);
printf(
'<td class="qm-num">%s</td>',
esc_html( $translation->remoteSiteId() )
);
echo '</tr>';
}
echo '</tbody>';
}
public function panel_menu( array $menu ) {
if ( ! isset( $menu['qm-languages'] ) ) {
return $menu;
}
$data = $this->collector->get_data();
$menu['qm-languages']['children'][] = $this->menu(
[
'title' => sprintf(
esc_html__(
'Content Translations (%s)',
'multilingualpress-qm'
),
number_format_i18n( count( $data->translations ) )
)
]
);
return $menu;
}
}
Lasst mich einige der wichtigsten Funktionen erklären. Im Konstruktor verwenden wir einen Filter, um unsere panel_menu()
Funktion
aufzurufen, die dann das Panel hinzufügt. Hier könnt ihr sehen, dass ich die Daten zu den children
von qm-languages
hinzufüge. Außerdem hole ich mir hier die gesammelten Daten, um die Anzahl der Übersetzungen zum Titel des Panels hinzufügen zu können.
In der output()
Funktion erzeugen wir die eigentliche Ausgabe für das Panel. Zuerst wird geprüft, ob überhaupt Daten vorhanden sind. Wenn nicht, geben wir nur einen Hinweis aus. Vielleicht sind auch die ganzen Funktionen mit Namen wie $this->before_*_output()
und $this->after_*_out()
aufgefallen. Diese werden von der übergeordneten Klasse QM_Output_Html
geerbt und ersparen uns das Schreiben von HTML-Markup.
Die wichtigste Ausgabe findet in der Funktion output_translation_table()
statt. Dies ist eine Hilfsfunktion, die ich hinzugefügt habe. Vielleicht braucht ihr eine solche Funktion nicht. Ihr könnt sie auch beliebig umbenennen, damit sie besser zu eurem Anwendungsfall passt. Ich habe sie einfach ähnlich benannt wie andere Funktionen in Query Monitor selbst.
Nachdem wir den <thead>
mit einigen Tabellenspaltennamen erzeugt haben, geben wir die Daten für die Übersetzungen aus. Wir verwenden einige Methoden der Translation
Objekte von MultilingualPress, um die Site-ID, die Post-ID, den Beitragstitel und den Permalink für den übersetzten Inhalt zu erhalten.
Initialisierung der Ausgabeklasse
Genau wie bei der Collector-Klasse müssen wir auch unsere HTML-Ausgabeklasse initialisieren. Wir verwenden hier einen weiteren Filter:
function register_qm_output_mlp_translations( array $output ) {
$collector = QM_Collectors::get( 'mlp_translations' );
if ( $collector ) {
$output['mlp_translations'] = new QM_Output_MLP_Translations( $collector );
}
return $output;
}
add_filter( 'qm/outputter/html', 'register_qm_output_mlp_translations', 50 );
Mit der Implementierung dieser letzten Klasse haben wir alle Teile zusammen, die wir für unser eigenes Query Monitor Panel brauchen.
Alles zusammenfügen
Jetzt fehlt nur noch ein letzter Schritt. Wir müssen noch alle Dateien laden. Das können wir am einfachsten wie folgt machen:
if ( defined( 'QM_DISABLED' ) && constant( 'QM_DISABLED' ) ) {
return;
}
add_action(
'plugins_loaded',
static function () {
require_once __DIR__ . '/data/languages_mlp_translations.php';
require_once __DIR__ . '/collectors/languages_mlp_translations.php';
require_once __DIR__ . '/output/html/languages_mlp_translations.php';
}
);
Um zu vermeiden, dass die Klassen geladen werden, wenn Query Monitor deaktiviert wurde, überprüfen wir zuerst die Konstante QM_DISABLED
. Dann benötigen wir Dateien mit den drei Klassen, die die Filter enthalten, um sie zu initialisieren. Bei der Implementierung wollte ich mich so eng wie möglich an die Codebasis von Query Monitor orientieren. Normalerweise würde ich Filter zur Initialisierung von Klassen nicht in derselben Datei wie die Klassen selbst unterbringen.
Das Ergebnis
Nachdem wir all das umgesetzt haben, wollt ihr wahrscheinlich alle das Ergebnis unserer Arbeit sehen, richtig? Dann lasst mich das Plugin mal schnell auf meinem Blog installieren und euch die Ausgabe für meinen letzten Blogbeitrag zeigen:

Ziemlich cool, oder? Da mein Blog nur zwei Sprachen hat und es nur eine Übersetzung anzeigt, ist das vielleicht nicht besonders nützlich. Aber uns fallen sicher auch andere Informationen ein, die in diesem Panel angezeigt werden könnten. Vielleicht die Übersetzung aller im Blogbeitrag verwendeten Taxonomien. Oder einige wichtige Einstellungen von MultilingualPress.
Fazit
Das Hinzufügen eines eigenen Panels zum Query Monitor kann euch helfen, wichtige Informationen einfacher zu debuggen. Vielleicht habt ihr einen ganz anderen Anwendungsfall für ein eigenes Query Monitor Panel. Oder vielleicht habt ihr es in der Vergangenheit sogar selbst schon umgesetzt. Dann teilt gerne einen Link zu eurer Lösung mit uns, falls sie öffentlich ist.
Wenn ihr auch MultilingualPress auf eurer Website einsetzen oder einfach nur den vollständigen Code dieses Beispiels sehen möchtet, findet ihr ihn auf Github. Dort könnt ihr ihn auch als Plugin-ZIP herunterladen und direkt testen.