Blog

Code Snippet: Spalte "Application" in Oxygen Template Liste

(2020)
Überholt!
Es gibt eine neue Version dieses Snippets.

Inhalt

Aufgabe

Oxygen ist ein mächtiges Plugin für WordPress, welches das WordPress Theme deaktiviert, die Erstellung eigener Templates mit einem visuellen Builder unterstützt, und damit ein ganz individuelles Design für die Website ermöglicht.

Innerhalb jedes Oxygen Templates wird dabei definiert, unter welchen Bedingungen es genutzt wird, beispielsweise für das Archiv oder den Blog, die Suchergebnisse, Einzeldarstellung von Seiten oder Beiträgen, all das für alle oder nur bestimmte Kategorien, Autoren, usw.

In der Liste der Templates sind diese Bedingungen leider nicht ersichtlich. Beim Einsatz vieler Templates geht daher schnell der Überblick verloren.

Lösung

Also habe ich ein Code Snippet entwickelt, das in der Liste der Oxygen Templates in einer neuen Spalte "Application" die definierten Bedingungen für jedes Template anzeigt.

Download

Das Code Snippet steht hier zum Download zur Verfügung:

oxygen-show-column-application-in-template-list.code-snippets.json
Version 1.0.0, 2020-09-12

Zur Installation und Nutzung dieser JSON Datei wird das Plugin Code Snippets benötigt.
Dort kann diese JSON Datei mit der Funktion "Import" hochgeladen und anschließend aktiviert werden.

Credits

Dieses Code Snippet habe ich entwickelt nach einer Idee von Gagan Khatri in der Oxygen User Group auf Facebook.

Disclaimer

Das Code Snippet habe ich nach bestem Wissen und Gewissen unter WordPress 5.5.1 und Oxygen 3.5 entwickelt und getestet.
Ich stelle das Code Snippet zur freien Verwendung zur Verfügung.
Eine Garantie für die Funktionalität in allen denkbaren WordPress Umgebungen kann ich nicht geben.
Download und Nutzung dieses Code Snippets erfolgen auf eigene Gefahr und Verantwortung.

Entwicklung

Die nachfolgende Beschreibung meiner Lösung setzt gewisse Grundkenntnisse in der WordPress Entwicklung voraus.

Datenquelle

Zur Lösung dieser Aufgabenstellung muss ich zunächst nachvollziehen, wie die Konditionen für die Verwendung der Templates in der WordPress Datenbank gespeichert werden. Dazu erstelle ich mir ein Template "Demo", bei dem ich alle Konditionen anhake bzw. ausfülle.
In der Tabelle postmeta schaue ich mir die zugehörigen Metadaten an:

Einträge für ein Oxygen Template in der Tabelle postmeta

Die Struktur und Bedeutung der Daten ist mir recht schnell klar.

Code Snippet

Meine favorisierte Methode, um Code zu implementieren, ist das Plugin Code Snippets. Hier erstelle ich ein neues Snippet.
Es muß nur in der WordPress Administrations-Ansicht ausgeführt werden. Also setze ich den Schalter "Only run in administration areas".

Neue Spalte in Template Liste

Aus vorherigen Projekten kenne ich bereits das Verfahren, wie man in WordPress zu administrativen Listenansichten neue Spalten hinzufügen und diese mit Daten füllen kann.
Im Grunde sind das zwei Schritte, für die WordPress Filter Hooks zur Verfügung stellt:

  1. Spalte mit Titel hinzufügen über einen Filter Hook zu manage_xxx_posts_columns, wobei das xxx ersetzt wird durch den Post Typ Slug (post, page, ...)
  2. Spalten-Inhalt hinzufügen für einen Filter Hook zu manage_xxx_posts_custom_column , wobei das xxx ... siehe oben.

Die Oxygen Templates haben den Post Type ct_template.
So ergibt sich die weltverändernde Formel xxx = ct_template.

Aus diesen Erkenntnissen kann ich in meinem Code Snippet das Grundgerüst wie folgt erstellen:

// add column title
add_filter('manage_ct_template_posts_columns', 'maltmann_oxygen_template_table_head');
function maltmann_oxygen_template_table_head( $columns ) {
	$columns['maltmann_oxy_tpl_apply']  = 'Application';
	return $columns;
}

// add column content
add_action( 'manage_ct_template_posts_custom_column', 'maltmann_oxygen_template_table_content', 10, 2 );
function maltmann_oxygen_template_table_content( $column_name, $post_id ) {
	if ($column_name != 'maltmann_oxy_tpl_apply') return;
    echo 'my column content';
}

In Zeile 2 füge ich einen neuen Filter zum Hook manage_ct_template_posts_columns hinzu.
Hier ist xxx schon ersetzt durch den korrekten Post Type ct_template.
Dieser Hook ruft meine eigene Funktion maltmann_oxygen_template_table_head() auf.
Im Parameter $columns wird ein Array mit den bisher registrierten Tabellen-Spalten an meine Funktion übergeben.
Ich füge dem Array meine neue Spalte namens maltmann_oxy_tpl_apply hinzu und gebe das neue Array $columns zurück.

Danach folgt in Zeile 9 mein Filter für den Hook manage_ct_template_posts_custom_column, mit dem ich den Spalteninhalt ausgebe.
Ganz zu Beginn prüfe ich in Zeile 11, ob der Hook tatsächlich für meine neue Spalte aufgerufen wurde. Wenn nicht, raus hier!
Danach gebe ich in Zeile 12 den Inhalt der Spalte aus. Machen wir erst mal ein provisorisches "my column content".
Speichern. Testen.
Sieht doch schon akzeptabel aus:

Meine neuen Hooks funktionieren.

Metadaten ermitteln

Um überhaupt aus meinem Snippet auf die in der Tabelle postmeta gespeicherten Daten zugreifen zu können, muss ich sie erst mal einlesen.

Das geht ganz gut mit der Funktion get_post_meta().
Da diese Funktion zwingend einen Parameter $post_id haben will, muss ich mir also erst mal Zugriff auf den aktuellen $post verschaffen.

function maltmann_oxygen_template_table_content( $column_name, $post_id ) {
	if ($column_name != 'maltmann_oxy_tpl_apply') return;
	
	global $post;
	$meta = get_post_meta($post->ID);
}

Das macht mir erst ein bisschen Sorgen, weil die Funktion get_post_meta() ohne Einschränkung auf bestimmte Meta-Daten erst mal ganz stupide alle Meta-Daten zu diesem $post aus der Datenbank liest. Da wären auch alle zig Revisionen zu jedem Template dabei. Das will ich natürlich vermeiden.
Per Debugging finde ich allerdings schnell heraus, dass die Metadaten bereits bei der Auflistung der Templates in einem Rutsch gelesen wurden, und ich mit der Funktion get_post_meta() auf die bereits im Cache befindlichen Daten zugreife. Alles fein.

Spalte beleben: 1) Other

Am einfachsten erscheint mir die Bedingungen für den Abschnitt "Other".
Obwohl die Zuordnung ziemlich klar ist, setze und lösche ich diese Bedingungen abwechselnd, um die Änderungen in der Datenbank bestätigt zu wissen.

Zuordnung Bedingungen "Other" in Datenbank

Mit diesen Erkenntnissen kann ich die ersten Konditionen in meiner neuen Spalte ausgeben.

function maltmann_oxygen_template_table_content( $column_name, $post_id ) {
	if ($column_name != 'maltmann_oxy_tpl_apply') return;
	
	global $post;
	// get_post_meta does not fire a db query since post meta is still cached from listing
	$meta = get_post_meta($post->ID);

	// --- Others ----------------------------------------------------------
	// collect settings for Oxygen's Other section
	$apply = [];
	if (@$meta['ct_template_front_page'][0] == 'true') 		{$apply[] = 'Front Page';}
	if (@$meta['ct_template_blog_posts'][0] == 'true') 		{$apply[] = 'Blog Posts Index';}
	if (@$meta['ct_template_search_page'][0] == 'true') 	{$apply[] = 'Search Page';}
	if (@$meta['ct_template_404_page'][0] == 'true') 		{$apply[] = '404';}
	if (@$meta['ct_template_inner_content'][0] == 'true') 	{$apply[] = 'Inner Content';}
	if (@$meta['ct_template_index'][0] == 'true') 			{$apply[] = '<i>Catch All</i>';}
	// Others Final Output
	if (count($apply)) {echo '<b>Others:</b><br/>' . join('<br/>',$apply).'<br/>';}
}

In Zeile 10 definiere ich erst mal ein Array $apply, in dem ich alle Konditionen speichern werde.
Anschließend kann ich alle zuvor identifizierten Konditionen aus den Metadaten lesen und in verständlicher Form in meinem Array speichern.
WordPress liefert Metadaten als Array. Deswegen muss ich jeweils mit [0] auf das erste Element im Array zugreifen. Und da nicht sicher gestellt ist, dass ich überhaupt valide Daten zurück bekomme, greife ich mit dem Präfix @ auf die Elemente zu. Das unterdrückt Warnungen im Debug Log.
Im Anschluß prüfe ich in Zeile 18, ob ich im Abschnitt "Other" irgendwelche Konditionen gesammelt habe und gebe diese ein bisschen aufgehübscht aus.
Speichern. Testen.
Ich bin zufrieden:

Meine Hooks zeigen erste Daten aus dem Abschnitt "Others".

Spalte beleben: 2) Singular

Nächste Schwierigkeitsstufe sind die Konditionen im Abschnitt "Singular". Hier haben wir die Definition einzelner Post Typen, Taxonomien und Parent IDs. Das wird schon spannender.
In der Datenbank identifiziere ich folgende Verknüpfungen:

Zuordnung Bedingungen "Singular" in Datenbank

Neben den einfachen boolschen Feldern sehe ich hier serialisiert gespeicherte Daten ("a:2:{i:0;s:11..."). Die muss ich erst mal in Datenstrukturen zurück wandeln. In PHP geht das normalerweise mit unserialize(). WordPress bietet aber mit maybe_unserialize() eine noch praktischere Funktion, die nicht gleich mit Fehler auf die Nase fällt, wenn die Daten sich nicht deserialisieren lassen.

Der zusätzliche Code sieht wie folgt aus:

    // --- Singular --------------------------------------------------------
    // collect settings for Oxygen's Sigular section
    $apply= [];
    // Singular Post Types
    $post_types = [];
    if (@$meta['ct_template_single_all'][0] == 'true') 		{$post_types[] = '<i>All</i>';}
    $specific_post_types = maybe_unserialize(@$meta['ct_template_post_types'][0]);
    if (is_array($specific_post_types) && count($specific_post_types)) {
    	$post_types = array_merge($post_types,$specific_post_types);
    }
    if (is_array($post_types) && count($post_types)) {
    	$apply[] = 'Post Types: ' . join(', ',$post_types);
    }
    // Singular Taxonomies
    if (@$meta['ct_use_template_taxonomies'][0]=='true') {
    	$taxonomies = maybe_unserialize(@$meta['ct_template_taxonomies'][0]);
    	if (is_array($taxonomies) && count($taxonomies)) {
    		$apply[] = 'Taxonomies: ' . join(', ',$taxonomies);
    	}
    }
    // Singular Parents
    if (@$meta['ct_template_apply_if_post_of_parents'][0]=='true') {
    	$parents = maybe_unserialize(@$meta['ct_template_post_of_parents'][0]);
    	$apply[] = 'Parent IDs: '.join(', ',$parents);
    }
    // Singular Final Output
    if (count($apply)) {echo '<b>Singular:</b><br/>'.join('<br/>',$apply).'<br/>';}

Speichern. Testen.
Noch nicht ganz, wie ich mir das vorstelle:

Hooks zeigen Daten aus dem Abschnitt "Singular"

Dann brauche ich wohl noch ein paar Hilfsfunktionen, um Post Typen lokalisiert darzustellen und Taxonomien (Kategorien, Schlagworte) von ID (bzw. Array) in Text umzuwandeln.

//-----------------------------------------------------------------------------
// Helper function to translate post type slugs (page, post, ...) to localized names (Seiten, Beiträge, ...)
function _maltmann_parse_oxy_post_types($post_types) {
	$retval = [];
	if (is_array($post_types) && count($post_types)) {
		foreach($post_types as $post_type) {
			$post_type_object = get_post_type_object($post_type);
			if ($post_type_object) {$retval[] = $post_type_object->label;}
		}
	}
	return $retval;
}
//------------------------------------------------------------------------------
// Helper function to retrieve term names from term IDs list or Oxygen names,values array 
function _maltmann_parse_oxy_taxonomies($taxonomies) {
	$retval = [];
	if (is_array($taxonomies) && count($taxonomies)) {
		if (array_key_exists('values',$taxonomies)) {$taxonomies = $taxonomies['values'];}
		foreach ($taxonomies as $term_id) {
			$term = get_term($term_id);
			if ($term) {$retval[] = $term->name;}
		}
	}
	return $retval;
}

Die Funktion _maltmann_parse_oxy_post_types() ermittelt die lokalisierte Bezeichnung für Post Types.

Mit der Funktion _maltmann_parse_oxy_taxonomies() ermittle ich anhand von IDs die tatsächlichen Namen der Taxonomien, also Kategorien und Schlagworte. Dabei kann der Funktion ein einfaches Array mit IDs übergeben werden, als auch ein Array mit den Feldern names und values. Das sind zwei Varianten, in denen Oxygen Taxonomien für verschiedene Konditionen speichert. Der interessierte Leser darf das gerne selbst im Detail debuggen. Für 5 Euro ergänze ich hier die detaillierten Erklärungen.

In beiden Hilfsfunktionen habe ich meines Erachtens ausreichende Fehlerbehandlung implementiert, um für alle Fälle gewappnet zu sein.

Die Funktion maltmann_oxygen_template_table_content() muss jetzt natürlich noch ein bisschen angepasst werden, um die neuen Funktionen zu implementieren.

	// --- Singular --------------------------------------------------------
	// collect settings for Oxygen's Sigular section
	$apply= [];
	// Singular Post Types
	$post_types = [];
	if (@$meta['ct_template_single_all'][0] == 'true') 		{$post_types[] = '<i>All</i>';}
	$specific_post_types = maybe_unserialize(@$meta['ct_template_post_types'][0]);
	if (is_array($specific_post_types) && count($specific_post_types)) {
		$post_types = array_merge($post_types,_maltmann_parse_oxy_post_types($specific_post_types));
	}
	if (is_array($post_types) && count($post_types)) {
		$apply[] = 'Post Types: ' . join(', ',$post_types);
	}
	// Singular Taxonomies
	if (@$meta['ct_use_template_taxonomies'][0]=='true') {
		$taxonomies = maybe_unserialize(@$meta['ct_template_taxonomies'][0]);
		$taxonomies =  _maltmann_parse_oxy_taxonomies($taxonomies);
		if (is_array($taxonomies) && count($taxonomies)) {
			$apply[] = 'Taxonomies: ' . join(', ',$taxonomies);
		}
	}
	// Singular Parents
	if (@$meta['ct_template_apply_if_post_of_parents'][0]=='true') {
		$parents = maybe_unserialize(@$meta['ct_template_post_of_parents'][0]);
		$apply[] = 'Parent IDs: '.join(', ',$parents);
	}
	// Singular Final Output
	if (count($apply)) {echo '<b>Singular:</b><br/>'.join('<br/>',$apply).'<br/>';}

Speichern. Testen.

Singular Konditionen mit lokalisierten Post Types und Taxonomien im Klartext

Spalte beleben: 3) Archives

Jetzt wird es lustig. Der Abschnitt "Archives" besteht nun zum größten Teil aus Referenzen zu irgendwas.
Ich suche mir wieder die Referenzen in der Datenbank:

Zuordnung Bedingungen "Archive" in Datenbank

Für Post Types und Taxonomien habe ich schon Hilfsfunktionen. Für Autoren (Benutzer) brauche ich eine neue Hilfsfunktion:

//------------------------------------------------------------------------------
// retrieves author IDs to names
function _maltmann_parse_oxy_authors($authors) {
	$retval = [];
	if (is_array($authors) && count($authors)) {
		foreach ($authors as $user_id) {
			if ($user_id == 'all_authors') {$retval[] = '<i>All</i>'; continue;}
			$user = get_userdata($user_id);
			if ($user) {$retval[] = $user->user_login;}
		}
	}
	return $retval;
}

Auch diese Hilfsfunktion kann gut mit Fehlern umgehen.

Danach kann ich die Konditionen für den Abschnitt "Archive" mit den bereits vorhandenen Mitteln aufbereiten.

	// --- Archive ---------------------------------------------------------
	// collect settings for Oxygen's Archive section
	$apply = [];
	// All Archives
	if (@$meta['ct_template_all_archives'][0]=='true') 		{$apply[] = '<i>All</i>';}
	// Archive Taxonomies
	if (@$meta['ct_template_apply_if_archive_among_taxonomies'][0]=='true') {
		$taxonomies = maybe_unserialize(@$meta['ct_template_archive_among_taxonomies'][0]);
		$taxonomies =  _maltmann_parse_oxy_taxonomies($taxonomies);
		if (is_array($taxonomies) && count($taxonomies)) {
			$apply[] = 'Taxonomies: ' . join(', ',$taxonomies);
		}
	}
	// Archive Post Types
	if (@$meta['ct_template_apply_if_archive_among_cpt'][0]=='true') {
		$post_types = maybe_unserialize(@$meta['ct_template_archive_post_types'][0]);
		$post_types = _maltmann_parse_oxy_post_types($post_types);
		if (is_array($post_types) && count($post_types)) {
			$apply[] = 'Post Types: ' . join(', ',$post_types);
		}
	}
	// Archive Authors
	if (@$meta['ct_template_apply_if_archive_among_authors'][0]=='true') {
		$authors = maybe_unserialize(@$meta['ct_template_authors_archives'][0]);
		$authors = _maltmann_parse_oxy_authors($authors);
		if (is_array($authors) && count($authors)) {
			$apply[] = 'Authors: ' . join(', ',$authors);
		}
	}
	// Archive Date
	if (@$meta['ct_template_date_archive'][0] == 'true') 	{$apply[] = 'Date Archive';}
	// Archive Final Output
	if (count($apply)) {echo '<b>Archive:</b><br/>'.join('<br/>',$apply).'<br/>';}

Speichern. Testen.
DAS sieht gut aus!

Finale Lösung.



magnifier