Verschachtelte Funktionen in PHP und warum du sie vermeiden solltest!

Diese Woche habe ich den Code einer Website überprüft. Die Website verwendete einige Code-Schnipse und in einem davon habe ich eine verschachtelte PHP-Funktion gefunden. Der Code funktionierte nicht wie erwartet. Nicht wegen der verschachtelten Funktion, aber er hat mich auf die Idee für diesen Blogbeitrag gebracht. Ich kann den ursprünglichen Code nicht teilen, aber ich hoffe, ich kann euch anhand eines Beispiels zeigen, wie verschachtelte Funktionen funktionieren und warum ihr sie wahrscheinlich nicht verwenden solltet.

Was ist eine verschachtelte Funktion?

Eine verschachtelte Funktion in PHP ist eine Funktion, die im Inhalt einer anderen Funktion deklariert wird. Wir könnten sie als „äußere Funktion“ und „innere Funktion“ oder „verschachtelte Funktion“ bezeichnen. Hier ist ein Beispiel für eine solche verschachtelte Funktion:

function multiplyAllByTwo( $array ) {
	function multiplyByTwo( $value ) {
		return $value * 2;
	}

	return array_map( 'multiplyByTwo', $array );
}

Wir haben eine Funktion, um ein Array zu verarbeiten und alle Werte darin mit 2 zu multiplizieren. Durch die Verwendung einer Funktion könnten wir „potenziell“ dasselbe für mehrere Arrays machen. Innerhalb dieser Funktion deklarieren wir eine andere Hilfsfunktion, die einen gegebenen Wert mit 2 multipliziert. Das ist die verschachtelte Funktion. Sie wird dann mit array_map() verwendet, die sie dann auf jeden Eintrag des Arrays anwendet. Wenn wir also ein Array übergeben, erhalten wir das Array zurück, in dem alle Werte mit 2 multipliziert wurden.

$inputArray = [ 1, 2, 3, 4, 5 ];
$resultArray = multiplyAllByTwo( $inputArray );
print_r( $resultArray );
/*
Array
(
	[0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
)
*/

Das ist toll, also wo liegt das Problem? Versuchen wir einfach mal, das resultierende Array erneut zu multiplizieren:

$inputArray   = [ 1, 2, 3, 4, 5 ];
$resultArray  = multiplyAllByTwo( $inputArray );
$resultArray2 = multiplyAllByTwo( $resultArray );
print_r( $resultArray );
print_r( $resultArray2 );

Was erwarten nach den beiden Aufrufen? Ein zweites Array, bei dem alle Werte erneut mit 2 multipliziert wurden, richtig? Aber was erhalten wir stattdessen?

PHP Fatal error:  Cannot redeclare multiplyByTwo() ...

Wenn die Funktion ein zweites Mal aufgerufen wird, erhalten wir einen schwerwiegenden Fehler, da die Funktion nicht erneut mit demselben Namen deklariert werden kann. Eine verschachtelte Funktion funktioniert also nur für eine „äußere Funktion“, die nur einmal ausgeführt wird. Was könnten wir also stattdessen tun?

Verwende keine verschachtelte Funktion

Der einfachste Weg wäre es, die verschachtelte Funktion aus der anderen Funktion zu entfernen und sie im globalen Namespace zu deklarieren.

function multiplyAllByTwo( $array ) {
	return array_map( 'multiplyByTwo', $array );
}

function multiplyByTwo( $value ) {
	return $value * 2;
}

Jetzt können die (vorher) äußere Funktion zweimal aufrufen:

$inputArray = [ 1, 2, 3, 4, 5 ];
$resultArray1 = multiplyAllByTwo( $inputArray );
$resultArray2 = multiplyAllByTwo( $resultArray1 );
print_r( $resultArray1 );
print_r( $resultArray2 );
/*
Array
(
	[0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
)
Array
(
	[0] => 4
    [1] => 8
    [2] => 12
    [3] => 16
    [4] => 20
)
*/

Jetzt erhalten wir das gewünschte Ergebnis. Aber wir füllen auch den globalen Namespace mit vielen Funktionen und müssen dabei sicherstellen, dass wir für diese verschiedenen Hilfsfunktionen nicht denselben Funktionsnamen verwenden. Als Alternative kannst du die Funktion auch einer Variable zuweisen und diese anstelle des Funktionsnamen-Strings verwenden. Aber dazu müsstest du diese Variable wieder innerhalb der äußeren Funktion zuweisen, da sie sonst nicht verfügbar wäre. Oder du müsstest die Variable innerhalb der Funktion mit dem globalen Schlüsselwort verfügbar machen. Beide Lösungen sind nicht wirklich schön, und deshalb möchte ich nicht einmal Code-Snippets dafür zeigen ?

Also, wenn du nicht wirklich eine global deklarierte Funktion benötigst, wie kannst du es dann sonst lösen? Es gibt einen anderen Weg.

Verwende eine anonyme Funktion

Eine anonyme Funktion wird oft in Kombination mit Funktionen wie array_map() und ähnlichen Funktionen verwendet. Unser Code würde dann so aussehen:

function multiplyAllByTwo( $array ) {
	return array_map( function ( $value ) {
		return $value * 2;
	}, $array );
}

In diesem Beispiel deklarieren wir die Funktion in dem Moment, in dem wir sie benötigen. Dadurch entfällt auch die Notwendigkeit, einen schönen Namen dafür zu finden, und wir wissen alle, dass das Benennen von Dingen eine der „zwei schwierigen Dinge in der Programmierung“ ist ?

Mit PHP 7.4 und höher kannst du sogar eine schöne kleine Arrow-Function verwenden, die es dir ermöglicht, einen Einzeiler für dieses Snippet zu schreiben:

function multiplyAllByTwo( $array ) {
	return array_map( fn( $value ) => $value * 2, $array );
}

Sieht gut aus, oder?

Fazit: Wann sollte man was verwenden?

Ich würde empfehlen, niemals eine verschachtelte Funktion zu verwenden! Obwohl das in anderen Programmiersprachen häufig gemacht wird, kann es in PHP leicht zu schwerwiegenden Fehlern und Problemen beim Testen und Debuggen führen.

Wenn es sein kann, dass du die Logik der „inneren Funktion“ für mehrere „äußere Funktionen“ benötigst, dann deklariere die Funktion immer mit einem Namen, entweder im globalen Namespace oder in einer PHP-Klasse.

Wenn du diese Logik nur für diese spezifische „äußere Funktion“ benötigst oder sie wirklich sehr einfach ist, wie in diesem Beispiel, kannst du eine anonyme Funktion oder sogar eine Arrow-Function verwenden.

Fehlende Sidebar-Widgets nach der Migration – was war passiert?

Ich schreibe diesen Blogbeitrag auf meinem neuen Server. Der alte wird in etwa zwei Stunden abgeschaltet. Da ich einige WordPress-Seiten und eine Matomo-Instanz hoste, war die Migration schon eine größere Aufgabe.

Ich bin bei der Migration meinem „5 Minuten Migration-Prozess“ gefolgt und alles lief so weit wie erwartet. Als ich dann aber meinem Blog nach der DNS-Umstellung aufgerufen hatte, haben auf einmal zwei Text-Widgets gefehlt. Was war passiert?

Emojis ?

Seit einer ganzen Weile unterstützt WordPress nativ Emojis. Man kann sie einfach in einen Blogbeitrag einfügen und WordPress zeigt sie an. In der Vergangenheit wurde dazu noch ein SVG-Sprite verwendet. Aber da moderne Betriebssysteme und Browser diese nun nativ unterstützen, ist das nicht mehr notwendig.

Aber wieso hat die Verwendung von Emojis bei nach der Migration zu einem Fehler geführt? Auf dem alten Server habe ich MySQL 8 verwendet und auf dem neuen ist es MariaDB 10. Beim Export der Datenbank habe ich einfach wie immer wp db export ausgeführt. Aber nach dem Import waren dann UTF-8-Multibyte-Zeichen defekt. Anstelle von Emojis wurden nur einige ?? Symbole angezeigt.

Vermutlich hat dies die unserialize() Funktion WordPress, die WordPress verwendet gestört, und das ganze Widget war kaputt. Dies hat dann dazu geführt, dass die ersten beiden Text-Widgets nicht mehr angezeigt wurden, obwohl sie in der Datenbank vorhanden waren.

Exportieren der Datenbank mit utf8mb4

Nachdem ich den Fehler gefunden und ein wenig recherchiert habe, bin ich auf ein WP-CLI Issue zu diesem Effekt gestoßen. Ich konnte anschließend durch das Hinzufügen eines Flags für das Standard-Charset einen funktionierenden Dump exportieren:

$ wp db export --default-character-set=utf8mb4

Das Importieren dieses SQL-Dumps hat den Fehler dann behoben, und die beiden Text-Widgets waren wieder da. Und es wurden auch wieder alle Emojis in den Blogbeiträgen angezeigt.

Fazit: Immer die WP-CLI aktualisieren!

Nachdem ich die Datenbank erfolgreich reparieren konnte, wollte ich wissen, wieso das (noch immer) passiert. Das Issue gibt an, dass der Fehler im db-command behoben und mit Version 2.5.0 der WP-CLI veröffentlicht wurde. Allerdings habe ich auf dem Server noch die Version 2.4.0 verwendet, die diesen Fix noch nicht enthielt.

Nach einem Update auf die aktuelle Version (2.7.1), musste ich das Flag nicht mehr angeben, und habe dennoch einen korrekten Export inklusive Emojis und andere utf8mb4 Zeichen erhalten.

Bevor ihr also wichtige Wartungsaufgaben durchführt, solltet ihr besser immer die WP-CLI auf die neueste Version aktualisieren, um nicht in Fehler zu rennen, die schon lange behoben sind und die ihr vielleicht nicht sofort bemerkt. Hätte ich den Fehler erst in ein paar Tagen/Wochen bemerkt, dann wäre es sehr schwierig geworden, die Datenbank zu reparieren/synchronisieren.

Umgang mit Zeitzonen – wie man es nicht macht!

Letzte Woche hatte ich mit einem interessanten Problem zu tun. Die Seite nutzte einen Custom-Post-Type für Webinare. Wenn man angemeldet war, dann sollte man ein paar Minuten vor dem Beginn des Webinars einen „Beitreten“ Button sehen. Es gab hierzu im Backend auch eine Einstellung für die Anzahl der Minuten, um die Anzeige des Buttons vor dem Beginn des Webinars zu steuern.

Aus irgendeinen Grund hat das aber nicht funktioniert. Da wir bereits ein anderes Problem in dem Plugin in Bezug auf Zeitberechnungen gefunden hatten, habe ich die Zeit erst einmal auf 300 Minuten vor dem Event gesetzt, was das Problem erst einmal löste, denn das Webinar war bereits seit 5 Minuten am Laufen. Aber was genau was eigentlich schiefgegangen. Bei der Suche nach dem eigentlich Fehler bin auf einen „cleveren Code“ gestoßen.

Analyse der Daten

Das Plugin verwendete Daten von einer externen API. Diese API hat mehrere Felder zurückgegeben, die dann in Postmeta gespeichert wurden. Das sind einige der verwendeten Felder:

+---------+-------------------------------+------------------------------+
| post_id | meta_key                      | meta_value                   |
+---------+-------------------------------+------------------------------+
| 1234567 | webinar_created_by            | api                          |
| 1234567 | event_id                      | 12345                        |
| ...     | ...                           | ...                          |
| 1234567 | to_date                       | Montag, 27. März 2023        |
| 1234567 | date                          | Montag, 27. März 2023        |
| 1234567 | date_en                       | 27 Mar 2023                  |
| 1234567 | to_date_en                    | 27 Mar 2023                  |
| 1234567 | start_time                    | 19:00:00                     |
| 1234567 | end_time                      | 20:45:00                     |
| 1234567 | time_zone                     | Mitteleuropäische Sommerzeit |
| ...     | ...                           | ...                          |
+---------+-------------------------------+------------------------------+

Könnt ihr etwas erkennen? Hm, vermutlich nicht, da ihr diesen Beitrag auf Deutsch lest. Aber die Daten sind teilweise übersetzt. So wird als Zeitzone der String „Mitteleuropäische Sommerzeit“ verwendet und nicht der englische Name „Central European Summer Time“ (CEST, GMT+2). Die Zeit ist also der UTC zwei Stunden voraus. Genau deshalb hatte ich auch auf die Schnelle einen Wert größer als 120 Minuten gesetzt, um sicherzugehen, dass ein solches potenzielles Problem dadurch gelöst würde. Aber wenn die Zeitzone doch eigentlich richtig angegeben ist, wieso kam es dann zu einem Fehler?

Umwandlung des Zeitzonen-Strings

In diesem Plugin gab es eine Funktion, die Zeitzonen-Strings in eine UTC-Differenz umwandelt. Die Funktion sieht in etwa wie folgt aus:

function get_timezone_mapping( $name = '' ) {
	$mapping = [
		// ...
		'Central Time' => '-6',
		'Central Standard Time' => '-6',
		'Canada Central Standard Time' => '-6',
		// ...
		'Portugal Winter Time' => '+0',
		'India Standard Time' => '+05:30',
		// ...
		'Восточноевропейское время' => '+2',
		'Eastern European Summer Time (Athens)' => '+3',
		'Eastern European Summer Time' => '+3',
		// ...
		'北京时间' => '+8',
		'台北時間' => '+8',
		'Seoul Time' => '+9',
		'日本時間' => '+9',
	];

	if ( ! empty( $name ) ) {
		if ( ! empty( $mapping[ $name ] ) ) {
			return $mapping[ $name ];
		} else {
			return false;
		}
	}

	return $mapping;
}

Man übergibt also einen String für eine Zeitzone und erhält einen String mit der Differenz zu UTC. Für einige Zeitzonen gab es mehrere Varianten und teilweise auch Übersetzungen der Bezeichnungen. Nur unsere „Mitteleuropäische Sommerzeit“ war nicht dabei, weshalb die Funktion in diesem Fall dann false zurücklieferte.

Der Zeitpunkt, an dem Dinge kaputtgingen

Nun wurde die Funktion und ihr Rückgabewert verwendet, um ein Date Objekt zu erzeugen. Es wurde dann mit der aktuellen Zeit (des Servers) verglichen. Der Code sah in etwa sie folgt auch (vereinfacht):

// Data dynamically queried from the database.
$webinar_data = [
	'time_zone' => 'Mitteleuropäische Sommerzeit',
	'date_en' => '27 Mar 2023',
	'start_time' => '19:00:00',
];
// ...
$timezone = $webinar_data['timezone'];
$start_date = $webinar_data['date_en'];
$start_time = $webinar_data['start_time'];
// ...
$timezone_mapping = get_timezone_mapping( $timezone );
$date_timezone = ! empty( $timezone_mapping ) && ! is_array( $timezone_mapping ) ? new DateTimeZone( $timezone_mapping ) : null;

$current_datetime = new DateTime( 'now', $date_timezone );
$start_datetime = $start_date ? new DateTime( $start_date . ' ' . $start_time, $date_timezone ) : null;
// ...
$pre_buffer_minutes = empty($minutes) ? 15 : absint($minutes);
// ...
// Subtract the buffer from the webinar's starting date & time.
$start_datetime->sub( new DateInterval( "PT{$pre_buffer_minutes}M" ) );
$is_within_time = $current_datetime->getTimestamp() >= $start_datetime->getTimestamp();

Lasst und das mal runterbrechen und versuchen den Fehler zu finden. Die Zeitzone des Webinars wurde an die Funktion get_timezone_mapping() übergeben, aber da es den String „Mitteleuropäische Sommerzeit“ nicht zuordnen konnte, gab sie false zurück, womit dann im Ergebnis null als Wert für $date_timezone in Zeile 13 herauskam. Diese Zeitzone wurde dann für beide Aufrufe von new Date() verwendet. Aber was passiert hier nun? Schauen wir uns dazu die beiden resultierenden Date Objekte einmal an, und welche Zeit sie repräsentieren, wenn die Aufrufe um „19:00“ Uhr (Serverzeit) passieren:

$current_datetime = (new DateTime('now', null))->format('Y-m-d H:i:s');
// 2023-03-27 17:00:00
$start_datetime = (new DateTime('27 Mar 2023 19:00:00', null))->format('Y-m-d H:i:s');
// 2023-03-27 19:00:00

Da für das zweite Date Objekt $start_datetime ein gültiger „Datumsstring“ verwendet wird, erstellt PHP ein Objekt mit exakt dieser Zeit und ignoriert dabei die Zeitzone. Das erste Date Obbjekt $current_datetime hingegen verwenden den String now und hat ebenfalls kein gültiges DateTimeZone als zweiten Parameter. Wenn ein solcher fehlt, dann verwendet PHP immer UTC als Zeitzone. Also selbst, wenn der Server (oder das WordPress-System) auf „Central European Summer Time“ läuft, wird PHP dennoch UTC verwenden. Damit erhalten wir dann die 2-Stunden-Differenz zur gewüschten Zeit. Schließlich wir dann die $is_within_time Variable false sein, bis die $pre_buffer_minutes plus der 2-Stunden-Differenz erreicht sind.

Wie geht man mit Zeitzonen besser um?

Wie ihr an dem Code sehen könnt, ist der Umgang mit Zeitzonen auf diese Weise eine schlechte Idee. Vor allem dann, wenn die Zeitzonene-Strings eventuell auch noch übersetzt sind. Man kann unmöglich eine Liste mit allen möglichen Varianten pflegen. Also was sollte man stattdessen tun?

Ich würde empfehlen einen „Industriestandard“ zu verwenden, der aich die Zeitzone verwendet, wenn man eine Uhrzeit und ein Datum definiert. In der Tabelle am Beginn der Blogbeitrags siehr man selbst für das Datum zwei Schreibweisen: „Montag, 27. März 2023“ und „27 Mar 2023“. Der erste ist dabei auch noch übersetzt. Durch das Aufteilen der Werte für Zeit, Datum und Zeitzonen auf meherer Felder muss man diese dann wieder zusammensetzen (wie in Zeile 16), in daraus dann ein „Datumsobjekt“ in verschiedenen Programmiersprachen erstellen zu können. Aber wieso verwenden wir nicht ein Format wie das folgende, das alle Teile bereits kombiniert?

$webinar_start = 'Mon, 27 Mar 2023 19:00:00 +0200';

Dieser Strings enthält alle Teile für Zeit, Datum und Zeitzone. Das ist auch der Wert den man erhält, wenn man das $date->format('r') Ausgabeformat wählt. Es folgt den Standards RFC 2822/RFC 5322. Wenn jemand nun eure API verwendet, können sie dann aus diesem String ein „Datumsobjekt“ erstellen und davon dann Zeit, Datum, oder andere Werte auslesen.

Fazit

Der Umgang mit Zeitzonen ist bei der Programmierung oft nicht einfach. Wenn man dann auch noch mit verschiednen Zeitzonen zu tun hat, wird es nochmals schwerer. Ihr könnt euch naütrlich dennoch dafür entscheiden, die einzelnen Werte für Zeit/Datum auch separat anzubieten, aber bitte bieete auch einen String wie oben beschrieben an. Und solltet ihr ech entscheiden dies nicht zu tun, dann übersetzt bitte die Namen von Zeitzonen, Monatsnamen oder ähnlichem nicht auch noch, da es sonst zu dem erwähnten (und manchmal schwer zu lösenden) Problem führen kann.

Für dieses spezielle Plugin habe ich bisher noch keine Lösung gefunden. Wenn die API einen solchen String in Zukunft nicht anbietet, dann wird mir wohl nichts anderes übrig bleiben als das statische Array in der Funktion immer wieder mit weiteren (übersetzten) Zeitzonen.String zu erweitern. Ich werde mit aber nie sicher sein können, dass nicht eines Tages ein neuer Wert dazu kommt, den ich noch nicht habe, und der dann wieder zu diesem Problem führt.

Mein CloudFest Hackathon 2023 Erfahrungsbericht

Letztes Wochenende bin ich in den Europa-Park in Rust gefahren, um an meinem zweiten CloudFest Hackathon teilzunehmen. Das diesjährige Event hatte 11 verschiedene Projekte, 7 davon waren WordPress-Projekte (einige andere hatten nur lose mit WordPress zu tun). Im letzten Jahr habe ich das Team des Pluginkollektiv dabei unterstützt, am beliebten Antispam Bee Plugin zu arbeiten. Dieses Jahr hat das Team dann an Statify, einem datenschutzfreundlichen Statistik-Plugin gearbeitet.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Kleine Schritte zu einem neuen und besseren Plugin

Für das Antispam Bee Projekt haben wir letztes das gesamte Plugin neu geschrieben, mit dem Ziel, eine neue Hauptversion zu veröffentlichen. Diese neue Version ist nun in der Alpha-Phase, also selbst ein Jahr nach dem Event, noch immer nicht fertig. Für Statify hatte unser Teamlead Florian Brinkmann im Vorfeld einige Issues auf GitHub ausgesucht, an denen er mit dem Team arbeiten wollte. Manche davon waren sogenannte „low-hanging fruits“, andere wiederum größere Issues und Feature-Requests.

Ein vermeintlich einfaches Ticket

Wir hatten ein paar Teammitglieder, die ganz neu dabei waren, an einem Projekt mitzuwirken und haben versucht für sie einige einfache Issues zu finden. Eines davon hatte den Namen „Show title instead of permalink“ und klang daher recht einfach. Das Ticket war fast 5 Jahre alt, mit einer konstruktiven Diskussion. Es gab auch schon einen PR mit einer einzeiligen Änderung, um einen Teil des Problems zu lösen. Das Issue sollte also einfach zu lösen sein, richtig? Nun, wie sich herausstellte, war es nicht ganz so einfach. Es gab einen Grund, wieso dieses Ticket seit 2018 noch nicht gelöst wurde. Nachdem wir in der Gruppe ausführlich darüber gesprochen hatten, haben wir einige Sonderfälle identifiziert, die eine genauere Planung erforderten.

Erarbeiten einer Lösung und Einführung neuer Möglichkeiten

Nachdem wir mehrere mögliche Lösungswege diskutiert hatten und zu einigen davon ein „proof of concept“ hatten, wurde recht deutlich, dass es die beste Lösung wäre, wenn wir den aktuellen Beitragstitel zum Zeitpunkt des Trackings des Seitenaufrufs durch Statify speichern würden. Da die eigene Tabelle von Statify recht einfach ist, gab es zwei mögliche Lösungswege:

  1. Hinzufügen einer neuen Spalte zur Tabelle
  2. Hinzufügen einer statifymeta Tabelle zum Speichern dies Titels

Wir haben uns für den zweiten Ansatz entschieden. Auch wenn es vielleicht ein wenig zu umfangreiche für ein so kleines Feature wirkt, so kann es die neue Tabelle Statify (und erweiternden Plugins) ermöglichen, zu jedem Eintrag zusätzliche Informationen zu speichern, ohne jedes Mal die Tabelle anpassen zu müssen. Zusätzlich macht es auch die Aktualisierung von Statify einfacher, dass die Änderungen von existierenden Tabellen auf manchen Umgebungen zu Problemen führen kann.

Onboarding neuer Contributoren

Ich muss zugeben, dass ich nicht sehr viel Code geschrieben habe. Ich habe nur eine kleine UX-Verbesserung beigetragen. Stattdessen habe ich aber anderen bei ihren Contributions geholfen. Ein Teammitglied, das sonst eher keinen Code schreibt, hat seine erste Contribution gemacht und der PR wurde auch direkt geprüft und gemerged. Ein anderes Teammitglied hat an dem Beitragstitel Issue gearbeitet und kannte sich mit einigen Dingen noch nicht so gut aus. Nachdem wir zwei Tage an dem Issue gearbeitet hatten, war es an der Zeit, den aktuellen Stand zu commiten. Erst dann hat er mir erzählt, dass er bisher noch nicht mit Git gearbeitet hat, also habe ich ihm eine kleine Einführung gegeben und er konnte einen Commit in seinen Fork machen. Zu diesem Zeitpunkt haben wir dann festgestellt, dass in der Zwischenzeit andere Dinge geändert wurden, die zu Konflikten führten. Also konnte er auch die Behebung davon ein wenig lernen. Jetzt muss der neue Code noch getestet werden, bevor er dann gemerged werden kann. Da dieses Issue, und einige andere, an denen wir gearbeitet haben, recht groß waren, werden wir wohl eine neue Hauptversion von Statify veröffentlichen, die alle Bugfixes und neuen Features des Hackathons enthalten wird.

Ein Event, das man besuchen sollte!

Ich kann leider nicht so viele Details zu den anderen Teams geben. Ich würde euch aber empfehlen, euch mal den Hashtag #CFHack2023 anzusehen, um einen besseren Eindruck vom Event zu bekommen. Für mich war es wieder ein tolles Event! Nicht nur der „hacking“ Teil, sondern auch die soziale Komponente. Es gab einge offizielle Parties und spaßige Aktionen, wie etwa das Mario Kart Turniert am Sonntagabend.

Nach der Verkündung der Gewinner-Teams der verschiedenen Kategorien, hat sich die deutsche Communtiy zu einem sponaten Meta-Meetup getroffen. Darin haben Meetup-Organizer der verschiedenen Städte ihre Erfahrungen und Ideen zu deren Meetups geteilt. Wir planen ein solches Meeting nun häufiger zu haben:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Das CloudFest

Am Nachmittag gab es denn mit dem „WordPress Day“ den offiziellen Startschuss zum CloudFest. Das Haupt-Event, das am Dienstag startete, ist hauptsächlich ein „Industrie-Event“. Beim WordPress Day ging es aber in verschiedenen Vorträgen um Themen rund um das WordPress-Ökosystem, auch wenn es nicht unbedingt die typischen Themen waren, die man sonst auf einem WordCamp sehen würde. Einige davon waren eher Produkt-Präsentationen.

Am Abend fand das „Come2Gather in the Streets“ Event am Haupteingang des Europa-Parks statt. Neben Essensständen und Getränken gab es auch Livemusik. Ich hatte dann auch endlich die Gelegenheit, mit einigen Menschen zu sprechen, die in den Tagen zuvor genauso beschäftigt waren wie ich, oder die gerade erst zum Haupt-Event angereist waren.

Leider musste ich am Dienstag schon um 11 Uhr abreisen und konnte mir daher keine Sessions ansehen. Ich habe es gerade so geschafft, ein wenig durch die „Cloud Fair“, den Ausstellungsbereich, zu schlendern und mit einigen Sponsoren zu sprechen. Ich hatte auch keine Zeit, ein paar Achterbahnen zu fahren, die nur für Teilnehmende offen hatte. Im letzten Jahr hatte ich dazu aber viel Zeit.

Fazit

Auch wenn ich etwas „enttäuscht“ war, dass ich selbst nicht so viel programmiert habe, wie im letzten Jahr, hat es mir doch sehr viel Spaß gemacht, viele neue Menschen an die Open-Source-Contribution heranzuführen. Alle waren sehr aufgeregt und motiviert. Sie wollen auch in Zukunft weiter mitwirken.

Wenn nichts dazwischen kommt, dann bin ich auch im nächsten Jahr wieder gerne dabei. Vielleicht schaffe ich es dann auch, ein Projekt einzureichen. Für das eine Thema, das ich dieses Jahr einreichen wollte, hatte ich nicht genügend Zeit zur Vorbereitung. Wenn ihr selbst noch nie contributed habt, dann kann ich euch sehr empfehlen, einen Hackathon oder ein WordCamp mit einem Contributor-Day in eurer Nähe zu finden. Ihr lernt dabei neue Dinge und neue Menschen kennen!

Verwendung von Git zur Versionierung deiner Server-Konfigurations-Dateien

Ich habe vor kurzem einen neuen Webserver geholt. Auf diesem neuen Server experimentiere ich ein wenig mit OpenLiteSpeed, was ich noch nie zuvor verwendet habe. Es bringt eine eigens Admin-Dashboard mit, um den Server zu verwalten. Aber alles wird in Konfigurations-Dateien gespeichert.

Manchmal möchte oder muss man diese Dateien aber manuell anpassen. Wenn nicht die von OpenLiteSpeed, dann aber vielleicht die SQL oder PHP Konfiguration. Wenn man dann nicht genau weiß, was man tut, macht man schnell Dinge kaputt.

Versionierung deiner Konfigurations-Dateien

Aus diesem Grund habe ich angefangen, meine Konfigurations-Dateien unter Versionsverwaltung zu stellen. Das hat einige Vorteile:

  • Du kannst Dinge ausprobieren und rückgängig machen, wenn etwas kaputt geht
  • Du hast ein „Backup“ deiner Konfiguration
  • Du kannst die gleichen/ähnliche Konfigurations-Dateien auf allen deinen Servern verwenden und die synchronisieren
  • Du siehst alle Änderungen, die von anderen Prozessen gemacht wurden – beispielsweise nach einem Update oder durch ein Konfigurations-Tool

Nur die wichtigsten Dateien versionieren und solche mit sensitiven Informationen ignorieren

Auf vielen Linux-Servern befinden sich alle Konfigurations-Dateien im /etc Ordner. Du könntest jetzt also auf die Idee kommen, einfach den gesamten Ordner zu versionieren. Aber das ist eventuell keine gute Idee, denn einige Dateien enthalten sensitive Daten und man kann auch schnell etwas kaputt machen. Ich habe beispielsweise einmal aus Versehen die /etc/passwd Datei gelöscht, was keine gute Idee war. ?

Ignorieren von nginx Konfigurations-Dateien

Auf meinem alten Server habe ich also nur die Ordner /etc/php und /etc/nginx in zwei separaten Git-Repositories versioniert. Für die nginx Konfigurationen habe ich dann die folgende .gitignore Datei verwendet:

# /etc/nginx/.gitignore
*param*.pem
sites-enabled/
modules-enabled/
modules-available/

In der zweiten Zeile ignoriere ich die dhparam-4096.pem Datei, welche für einen besseren Diffie-Hellman-Schlüssel verwendet wird. Ich ignoriere auch die *-enabled Ordner, die nur Symlinks auf die *-available enthalten. Ihr könnt diese aber auch versionieren, wenn ihr tracken wollt, welche Sites und Modules enabled wurden. Der modules-available Ordner hatte für mich auch keinen größeren Wert, weshalb ich ihn ausgelassen habe.

Ignorieren von OpenLiteSpeed Konfigurations-Dateien

Auf dem neuen Server bin ich immer noch dabei herauszufinden, wie die Konfigurations-Dateien strukturiert sind. Die OpenLiteSpeed Dateien befinden sich im Ordner /usr/local/lsws und aktuell sieht meine .gitignore wie folgt aus:

# /usr/local/lsws/.gitignore
# Ignore all files by default but decent into subfolders
*
!*/

# Allow just some files and subfolders
!.gitignore
!/conf/**
!/lsphp*/**
!/php/**

# Still ignore some files and folders in those subfolders
*.conf.bak
*.conf.dpkg
*.conf.txt
*.conf0
*.conf0,v
*.properties0
*.properties0,v
/lsphp*/bin/
/lsphp*/include/
/lsphp*/lib/
/lsphp*/share/man/

Da der /usr/local/lsws Order nicht nur die Konfiguration, sondern auch Binärdateien und Logfiles enthält, ignoriere ich erst einmal alle Dateien und Ordner. Dann füge ich nur die Ordner hinzu, die Konfigurations-Dateien enthalten, die ich versionieren möchte.

Nachdem ich einige Pakete aktualisiert habe, gab es viele „Backup-Konfigurations-Dateien“, also habe ich auch alle mit diesen Dateiendungen, in den zuvor erlaubten Ordner, ignoriert.

Ich werde diese Ignoriere-Liste in Zukunft vielleicht noch anpassen, aber für den Moment habe ich alle Dateien versioniert, die ich brauche.

Zu Remote-Repository pushen

Wie bereits in den Vorteilen erwähnt, könnt ihr diesen Ansatz auch verwenden, um ein „Backup“ eurer Konfigurations-Dateien zu erstellen. Auch wenn Git (oder jedes andere Versionsverwaltungstool) keine Alternative zu einem Backup ist, kann es doch vorteilhaft sein, das Repository auf einem externen Server zu haben. Da diese Konfiguration nicht unbedingt öffentlich sein sollte, verwende ich für meine ein privates Repository auf GitLab.com als Remote.

Das hat weiterhin den Vorteil, dass ich mir das Repository auf meinen Laptop klonen kann, um dann die Dateien einfacher in PhpStorm bearbeiten kann, als mit vim direkt auf dem Server. Ich kann auch sehr viel einfacher die Änderungen vergleichen und Commits rückgängig machen.

Änderungen automatisch versionieren

Ich mache die Commits (und Pushes) normalerweise manuell. Aber falls ihr häufig Änderungen an Dateien durch andere Prozesse habt, und jede Änderungen versionieren möchtet, dann könnt ihr einen Cron erstellen, der alle Änderungen commited. Dies ist ein Beispiel für einen Cron, der automatisch alle 30 Minuten alle Änderungen an der nginx Konfiguration commited:

30 * * * * cd /etc/nginx && git add -A && git commit -m "auto commit on `date`" && git push

Selbst neue und gelöschte Dateien werden versioniert. Einen solchen Cron zu haben, kann aber auch potenziell zu Problemen führen, wenn eure Ignorieren-Liste nicht richtig aufgesetzt ist, da vielleicht eine neue Datei eine Logdatei ist, die sich häufig ändert. Wenn ihr also einen solchen Cron einsetzt, solltet ihr die automatischen Commits regelmäßig prüfen.

Fazit

Ich liebe Git! Man kann es für so viele verschiedene Dinge verwenden. Das Versionieren meiner Konfigurations-Dateien hat mir schon viel Zeit gespart, wenn ich versucht habe herauszufinden, wieso Dinge auf einmal kaputt waren. Aber auch, wenn ich Änderungen an mehreren Dateien auf einmal machen wollte (bei der lokalen Bearbeitung in PhpStorm).

Verwendet ihr einen ähnlichen Ansatz und möchtet diesen gerne hier teilen? Oder verwendet ihr Git noch für ganz andere Dinge? Dann schreibt es gerne in einen Kommentar.

WordCamp Asia 2023 – Endlich meine erste Reise nach Asien!

Vor etwa zwei Wochen habe ich mich auf eine lange Reise nach Thailand vorbereitet. Dies sollte auch meine erste Reise nach Asien sein. Nachdem das WordCamp Asia 2020 nur 9 Tage vor dem Event abgesagt wurde, war es 2023 endlich so weit. Die erste Ausgabe des neuen Flagship-Events fand statt.

Reise nach Thailand

Ich hatte den Tag nicht wirklich perfekt geplant. Mein Flug von Berlin ging um 08:55 Uhr am Morgen. Da Super Bowl Sonntag war, hatte ich mich schon auf sehr wenig Schlaf eingestellt. (Un)glücklicherweise habe ich mich am Sonntag nicht so fit gefühlt und bin daher früh ins Bett gegangen und habe die Live-Übertragung des Super Bowl verpasst 🙁

Nach einem guten Schlaf konnte ich dann meinen ersten Flug vom BER nach Helsinki nehmen. Von dort aus ging es dann in einem zweiten Flug nach Bangkok. Die etwa 11 Stunden „vergingen wie im Flug“, auch wegen des Entertainment-Programms an Board. Da ich in Flugzeugen nicht schlafen konnte, war ich bei Ankunft um 6 Uhr Ortszeit schon ein wenig müde. Vom Flughafen aus habe ich erste einen Zug und dann die Metro genommen. Den Rest des Weges zum Hotel bin ich gelaufen und konnte ein paar erste Eindrücke sammeln, wie sich Thailand von Europe unterscheidet, besonders in Bezug auf den Verkehr. Aber auch die vielen Street-Feed-Angebote auf dem Weg zum Hotel haben mich überrascht. Der Checkin war eigentlich erst um 14 Uhr, aber ich konnte glücklickerweise direkt ins Zimmer, gegen einen kleinen Aufpreis von 300 Baht (etwa 9€). Nach also fast 24 Stunden Reisezeit konnte ich endlich ein wenig schlafen 🙂

Am Dienstag und Mittwoch hatte ich ein wenig Gelegenheit, die Stadt zu erkunden. Der Mittwoch war zwar der einzige Regentag meiner Reise (zwischenzeitlich auch mal heftiger), aber ich konnte mich dennoch mit Leuten treffen und ein wenig nach China Town gehen. Zuerst kam es mir etwas komisch vor in ein asiatisches Land zu fahren und dort dann in ein China Town zu gehen, es was aber schon sehr faszinierend. Ich habe dann auch eine Fähre auf dem Fluss genommen, eine von den richtig schnellen!

Die Community treffen

Am Donnerstag bin ich in ein anderes Hotel umgezogen, um dort die anderen Inpsyder zu treffen. Wir haben im Hotel direkt neben der Venue gewohnt, in dem viele andere WordCamp-Teilnehmende auch untergekommen sind. Viele von ihnen habe ich seit einer ganzen Weile nicht mehr getroffen. Selbst wenn sie zum WCEU in Porto waren, hatte ich dort kaum Zeit, mit ihnen zu sprechen. Bevor es zum ersten Side-Event ging, hatte ich noch Zeit mir einen der vielen Tempel in Bangkok anzusehen:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Das erste Side-Event war „The WordPress Enterprise Gap“ von Human Made. Es war wie ein WordPress Meetup mit einigen Vorträgen und einem Panel. Die Themen drehten sich alle darum, wie man WordPress für Unternehmen attraktiver machen kann.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Ich wäre auch gerne zur „GoDaddy Pro Island“ Party gegangen, die genau wie in Porto auf einem Boot stattgefunden hat, aber sie hat sich zu sehr mit anderen Side-Events überschnitten. Daher hin ich dann mit Kolleginnen und Kollegen zum „WooCommerce Community“ Event gegangen, wo ich ein paar Freunde getroffen habe. Die Location war auch direkt am Fluss gelegen und obwohl ich nicht wirklich so sehr an eCommerce interessiert bin, hatte ich ein paar gute Gespräche 🙂

Das letzte Side-Event des Tages war die „Superheroes Pride Party“ organisiert von Yoast, codeable und Bluehost. Die Location war das Hotel, in dem wir auch waren, und somit ein perfekter Ausklang des Tages. Viele hatten viel Spaß auf der Party.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Contributor Day

Am Freitag ging das WordCamp Asia dann mit dem Contributor Day offiziell los. Ich war mit anfangs noch nicht ganz klar, in welches Team ich gehen würde, aber dann habe ich Alex getroffen, der Fragen zu Tickets/Registrierung auf WordCamps hatte und so bin ich wieder ins Meta Team gegangen. Wir haben beider versucht die lokale Entwicklungsumgebung für WordCamp.org aufzusetzen, sind aber gescheitert ? Immerhin konnte ich einige Fehler lösen, einen PR erstellen und zu anderen Issues aufmachen. Hoffentlich ist es neuen Mitwirkenden dann in Zukunft möglich, direkt zu starten.

Am Abend bin ich dann mit meiner Kollegin Viola etwas essen gegangen. Zuerst haben wir uns die viele Essenangebote in der Venue angesehen, waren aber vom Angebot etwas überfordert. Wir haben dann aber ein nettes kleines Restaurant die Straße runter gefunden, in dem es einige gute thailändische Gerichte gab.

Der erste Konferenztag

Samstag was dann der erste Konferenztag. Wie jedes Flagship-WordCamp ging es mit den Opening-Remarks los:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Das Programm war vollgepackt mit vielen tollen Themen. Im ersten Slot habe ich aber erst einmal keinen Talk gehört und stattdessen ein paar Freunde getroffen, die ich seit dem WCEU Berlin 2019 nicht mehr gesehen hatte.

Advanced Performance & Scalability for PHP Developers

Im zweiten Slot habe ich mir dann die Session von Alain Schlesser angesehen. Ein Thema seines Vortrags drehte sich um Caches. Er hat ein paar tolle Tipps gegeben, wie man Dinge in verschiedenen Ebenen und in granularer Art cachen kann und wie man dann den Cache invalidiert, wenn es notwendig wird.

Think like a hacker: Attack your WordPress

Im Anschluss habe ich mir angesehen, wie Matthias Held live auf der Bühne die Website eines Teilnehmers gehackt hat. Natürlich hat er nicht die Live-Website gehackt, sondern nur eine Kopie und der Teilnehmer hatte sich freiwillig gemeldet. Obwohl die Seite nicht viele Sicherheitsprobleme hatte, war Matthias in der Lage zu zeigen, wie er mit einer XSS-Attacke die PHP-Session eines eingeloggten (Administrator) Kontos hätte übernehmen können, wenn ein manipulierter Link geklickt wurde, der zu einem veralteten und unsicheren Plugin gehörte.

Success stories of HeroPress

Nach dem Mittagsessen hat Topher DeRosia dann einige Geschichten zu HeroPress geteilt. Ich folge dem Projekt schon mehrere Jahre und viele Bekannte aus der WordPress-Community haben dort bereits ein Essay veröffentlicht. Vielleicht werde ich auch eines Tages meine Geschichte teilen.

The Ultimate Newsroom QA: how to manage your editorial workflow like a boss

Als einzelner Blogger ist es für mich einfach. Ich muss mich einfach entscheiden, worüber ich schreiben möchte und es dann niederschreiben. Wenn man aber in einem größeren Team bloggt, dann ist das sehr viel herausfordernder. Francesca Marano hat viele interessante Einblicke gegeben, wie sie einen erfolgereichen italienischen Blog mit vielen Autorinnen geführt hat.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Die Sponsoren treffen

Kein WordCamp wäre möglich, ohne Sponsoren. In den nächsten beiden Slots habe ich daher keine Sessions besucht und stattdessen mit Sponsoren gesprochen. Bei vielen davon arbeiten WordPress-Freunde und es war schön, mit einigen von ihnen nach Jahren mal wieder zu sprechen.

Migrating WordPress Core to GitHub Actions: A Retrospective

Die letzte Session des Tages war von Jonathan Desrosiers, in der er gezeigt hat, wie WordPress zu automatisierten GitHub Actions migriert ist.

Thailändisches Abendessen mit dem Team

Die ersten beiden Tage waren recht ausgelastet und so konnten wir endlich mal die Zeit finden, mit allen Inpsydern, die beim WordCamp dabei waren, zu Abend zu essen. Wir haben eine Fähre zu „Asiatique The Riverfront“ genommen und haben viele verschiedene thailändische Gerichte probiert. Auf dem Rückweg haben wir dann ein Tuk Tuk genommen, was auch für mich das erste Mal war. Zurück im Hotel haben wir noch einige Teilnehmende des WordCamp in der Bar oben im Hotel getroffen.

Der zweite Konferenztag

Da der vorherige Abend etwas lang war, habe ich erst einmal gut geschlafen, um Energie für die zweite Hälfte der Vorträge zu haben.

AMA – Ask Matt Anything

Daher habe ich mir den ersten Talk auch im Live-Stream angesehen. Auch Matt konnte nicht vor Ort dabei sein, da er sich um seine Familie kümmern misste. Manche Fragen waren aber wirklich interessant. Ich muss mir noch die ganze Session ansehen, aber immerhin hat er beiläufig den Start von Gutenberg Phase 3 verkündet.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Stepping Back To Move Forward

Nach einem Kaffee und einem kleinen Snack habe ich dann den Lightning-Talk von Carole Olinger besucht. Es war der erste von drei Lightning-Talks mit dem Fokus Mental Health. Das sind die Art von Vorträgen, die ich mir auf WordCamps sehr gerne ansehe. Eine Sache, die Carole in ihrem Vortrag präsentiert hat, war JOMO – Joy of Missing Out – um uns bewusst zu machen, dass es OK ist, wenn man nicht an allen Dingen teilnimmt, zu denen man die Gelegenheit hat.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

The Power Of Empathy

Der zweiten Mental Health Lightning Talk hatte den Schwerpunkt Empathie, präsentiert von Ajit Bohra. Das schönst Zitat fand ich das folgende:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Look for the Good

Der letzte Talk wurde von Michelle Frechette gehalten. Sie hat darüber gesprochen, dass es manchmal harte Zeiten gibt – wie die letzten Jahre mit der Pandemie – aber dass sich für sie und viele andere von uns dadurch auch neue Möglichkeiten in diesen schweren Zeiten geboten haben.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Panel: Building WordPress Communities in Your Country

Im Anschluss habe ich das einzige Panel des WordCamp Asia besucht. Rahul D Sarker, M Asif Rahman, Miriam Schwab, Amit Bajracharya, Angela Jin und Hajime Ogushi haben über ihre lokalen Communities erzählt und viele Fragen aus dem Publikum beantwortet, wie man eine starke Community im eigenen Land aufbauen kann.

Ten Minutes on Five for the Future: A commitment to WordPress and the Open Web

Nach dem Mittagessen hat Hari Shanker die „Five for the Future“ Initiative von WordPress vorgestellt. Das Ziel ist es, alle Unternehmen, die mit WordPress arbeiten, zu ermutigen, 5% zum Open-Source-Projekt zurückzugeben. In der Q&A habe ich gefragt, wie einzelne Contributoren mehr Sichtbarkeit gegeben werden könnte, denn aktuell werden nur Unternehmen aufgelistet. Hari hat einen Blogbeitra auf Make WordPress geschrieben und ich werde dort wohl mal ein paar Vorschläge machen, was man tun könnte, um Menschen hervorzuheben, die dem Projekt helfen und nicht bei einem Unternehmen angestellt sind.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Etwas Zeit mit Freunden verbringen

Die beiden Menschen, auf die ich mich am meisten auf dem WordCamp Asia gefreut habe, waren Ellen und Manuel, das Paar hinter Elmastudio und AinoBlocks. Ich hatte seit dem Contributor Day auf dem WCEU 2019 in Berlin nicht mehr mit ihnen gesprochen und was sehr froh mal wieder reden zu können.

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Closing-Remarks … und die Ankündigung der nächsten Stadt

Der Tradition der anderen Flagship-Events folgenden, wurden in den Closing-Remarks allen Beteiligen von den Organizern gedankt. Etwa 1300 Teilnehmende waren beim ersten WordCamp Asia dabei. Ganz am Ende wurde dann noch die nächste Stadt verkündet:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Nächstes Jahr treffen wir uns dann also in Taipeh, Taiwan! Ich hoffe sehr, dass ich auch wieder dabei sein kann, um ein weiteres neues Land in Asien kennenzulernen. Ich möchte allen Organizern des WordCamp Asia 2023 danken und dem neuen Team viel Erfolg wünschen!

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

After-Party!!!

Natürlich gab es auch eine After-Party auf dem WordCamp Asia 😉 Sie fand draußen statt und neben (Live) Musik gab es tolles Essen und Tänze, die ich noch nie zuvor gesehen hatte 🙂

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Wenn ihr noch mehr Bilder sehen möchtet, dann schaut euch den #WCAsia Hashtag auf Twitter an oder schaut in die geteilte Galerie mit Bildern von vielen Teilnehmenden an.

Workation in Thailand

Am Montag sind wir dann noch in einen Ort etwa 3 Stunden südwestlich von Bangkok gefahren. Leider konnte ich nur einen Tag bleiben, aber es war ein schöner Abschluss meiner Reise:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Ein WordCamp der „ersten Male“

Der Besuch des WordCamp Asia war meine erste Reise nach Asien. Es was auch das erste WordCamp Asia. Es war das erste WordCamp (vor Ort) seit einer sehr langen Zeit, bei dem ich nur Teilnehmer war. Und es war das erste WordCamp, seit ich im Oktober meinen neuen Job bei Inpsyde angefangen habe. Meine nächsten WordCamps werden vermutlich das WordCamp Wien, gefolgt vom WordCamp Europe in Athen sein, dieses Mal wohl als Speaker. Ich möchte diesen Blogbeitrag mit dem After-Movie abschließen, das meine Kollegin Viola produziert hat:

Click here to display content from Twitter.
Erfahre mehr in der Datenschutzerklärung von Twitter.

Verwendung von SSH-Keys zur Signierung von Git-Commits

Im November letzten Jahres hatte ich euch erklärt, wie ihr unterschiedliche Git-Einstellungen für private und berufliche Projekte verwenden könnt. In einem Code-Schnipsel hatte ich dabei SSH-Keys zur Signierung von Commits verwendet. In der Vergangenheit hat man Commits in der Regel mit GPG-Keys signiert. Daher möchte ich euch heute zeigen, was ihr tun müsst, um stattdessen einen SSH-Key zur Signierung euer Commits zu verwenden.

Schritt 0: Erstellt ein SSH-Schlüsselpaar

Ihr habt vermutlich schon ein Schlüsselpaar. Wie sonst würdet ihr Repositories von einer Git-Hosting-Plattform klonen? Aber falls ihr noch kein SSH-Schlüsselpaar habt, führt diesen Befehl aus:

ssh-keygen -t ed25519 -C j.doe@example.com

Ich würde euch empfehlen, den ed25519 Verschlüsselungs-Algorithmus zu verwenden. Ihr könnt auch einen Kommentar setzen, ansonsten wird hier user@machine des Geräts verwendet, auf dem ihr den Schlüssel erstellt. Ich würde euch auch empfehlen eine „passphrase“ zu verwenden, aber diese ist nicht zwingend notwendig, um den Schlüssel für die Signierung von Commits zu verwenden.

Schritt 1: Aktualisiert eure Git-Konfiguration

Nachdem wir nun einen SSH-Key haben, können wir ihn zur Konfiguration hinzufügen. Wir können das mit dem folgenden Befehl tun:

git config --global user.signingkey ~/.ssh/id_ed25519

Da Git noch immer GPG-Keys als Standard verwendet, um Commits zu signieren, müssen wir einstellen, dass stattdessen SSH-Keys verwendet werden sollen. Das erreichen wir durch das Hinzufügen der folgenden Einstellung:

git config --global gpg.format ssh

Falls ihr bisher noch nie Commits signiert habt und dies in Zukunft immer tun wollt, dann führt noch diesen Befehl aus:

git config --global commit.gpgsign true

Lasst euch hier nicht durch den Namen gpgsign der Einstellung verwirren, es wird dennoch SSH verwenden, um eure Commits zu signieren.

Schritt 2: Festlegen der „allowed signers“ (optional)

Wenn ihr nun etwas commited und euch das Log inklusive der Signaturen anseht, dann erhaltet ihr folgenden Fehler:

git log --show-signature
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
No signature
Author: Jo Doe <j.doe@example.com>
Date:   Sun Jan 29 19:07:52 2023 +0000

Das passiert, weil Git nicht wissen kann, ob die Public-Keys valide sind. Um das zu lösen, müssen wir noch eine andere Einstellung in die Git-Konfiguration hinzufügen:

git config --global gpg.ssh.allowedSignersFile ~/.git_allowed_signers

Diese Datei müssen wir jetzt noch erstellen, da wir sonst einen anderen Fehler bekommen. Ihr für pro Zeile jeweils einen Public-Key ein und schreibt in die erste „Zeile“ die dazugehörige E-Mail-Adresse, gefolgt vom Inhalt des Public-Keys. Die Datei könnte wie folgt aussehen:

# ~/.git_allowed_signers
j.doe@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ818qdUM98GriqpTKhqMmwYAgeK3iiCg07Qgya5NwN/ j.doe@example.com

Wenn ihr euch jetzt das Git-Log erneut anseht, dann solltet ihr eine gültige Signatur sehen:

git log --show-signature
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
Good "git" signature for j.doe@example.com with ED25519 key SHA256:/qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk
Author: Jo Doe <j.doe@example.com>
Date:   Sun Jan 29 19:07:52 2023 +0000

Nach diesen ganzen Schritten sollte eure Git-Konfiguration (mindestens) wie folgt aussehen:

[user]
	email = j.doe@example.com
	name = Jo Doe
	signingkey = ~/.ssh/id_ed25519
[commit]
	gpgsign = true
[gpg]
	format = ssh
[gpg "ssh"]
	allowedSignersFile = ~/.git_allowed_signers

Schritt 3: Hinzufügen des Schlüssels zu Git-Hosting-Plattformen

Viele von euch werden wohl GitHub, GitLab oder andere Git-Hosting-Plattformen verwenden. Mindestens die beiden genannten Plattformen unterstützen auch SSH-Keys zur Signierung in ihrem UI.

Hinzufügen des Schlüssels zu GitHub

Navigiert zu „Settings | SSH and GPG keys„. Klickt dort auf den „New SSH key“ Button. Im Feld „Key type“ wählt ihr „Signing Key“ aus. Fügt dann euren Schlüssel ein und vergebt einen Titel:

Screenshot des "New SSH key" Formulars zum Hinzufügen des Public-Keys.

Ihr könnt denselben Schlüssel verwenden, den ihr eventuell schon für einen „Authentication Key“ verwendet habt.

Hinzufügen des Schlüssels zu GitLab

Navigiert zu „Preferences (User Settings) | SSH Keys„. Hier könnt ihr den Schlüssel direkt in das Formular eintragen. Wählt dann entweder „Authentication & Signing“ oder nur „Signing“ im „Usage type“ Dropdown aus:

Screenshot des "Add an SSH key Formulars zum Hinzufügen des Public-Keys.

Standardmäßig hat ein Schlüssel bei GitLab ein Verfallsdatum, das ihr aber auch leer lassen könnt. Ihr könnt das Datum aber verlängern, indem ihr den Schlüssel entfernt und neu hinzufügt. Da SSH-Keys normalerweise kein Verfallsdatum haben (im Gegensatz zu GPG-Keys), könnt ihr die Einstellung ja mal testen.

Schritt 4: Prüfen, ob Commits signiert sind

Wir haben ja bereits auf der Kommandozeile im Git-Log überprüft, dass die Signierung funktioniert. In GitHub und GitLab könnt ihr die signierten Commits in fast allen Ansichten direkt neben den Commit-Hashes sehen. In der Commit-Liste auf GitHub könnte das wie folgt aussehen:

Screenshot eines "Verified" Commit bei GitHub.

In der Commit-Liste von GitLab sieht es hingegen wie folgt aus:

Screenshot eines "Verified" Commit bei GitLab.

Der „SSH key fingerprint“ ist hierbei der gleiche, den wir auch schon im Git-Log auf der Kommandozeile gesehen haben: /qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk.

Fazit

Für eine lange Zeit waren es GPG-Keys, die zum Signieren von Commits verwenden wurden. Aber da GPG im Setup und der Verwaltung sehr komplex sein kann, haben viele ihre Commits gar nicht erst signiert. Wenn ihr also GPG nicht ohnehin verwendet – um beispielsweise E-Mails zu signieren und/oder zu verschlüsseln – dann könnt ihr einfach SSH-Keys verwenden, um eure Commits zu signieren. Ihr könnt auch beides für verschiedene Projekte verwenden. In diesem Fall würde ich euch empfehlen, noch einmal meinen Beitrag zu unterschiedlichen Git-Konfigurationen zu lesen und die Einstellungen dann nicht in der globalen Konfigurationsdatei vorzunehmen, sondern in den eingebundenen Dateien.

Marker auf einer Leaflet Karte nach Bundesländern clustern

Wie ich in meinem letzten Blogbeitrag erwähnt habe, gibt es noch ein kleines Bonus-Thema in der Serie zu individuellen Karten. Im vorherigen Beitrag haben wir eine Karte mit Leaflet in JavaScript erstellt. Darauf wurden die Landeshauptstädte visualisiert. Zwei davon liegen recht habe zusammen und man kann sie nur schwer anklicken. Aber wie sähe eine Karte mit allen Großstädten (ca. 100.00 und größer) aus:

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Vor allem in Nordrhein-Westfalen ist es nun unmöglich alle Marker zu sehen, zu hovern und anzuklicken. Aber da wir Leaflet verwenden, gibt es hierfür eine Lösung:

Verwendung von Marker-Clustern

Es gibt eine Erweiterung von Leaflet mit dem Namen Leaflet.markercluster, die wir verwenden können, um die Marker zu clustern. Hierzu müssen wir zuerst ein paar weiter CSS und JavaScript-Dateien einbinden:

<link rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
      integrity="sha256-YU3qCpj/P06tdPBJGPax0bm6Q1wltfwjsho5TR4+TYc="
      crossorigin=""/>
<link rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
      integrity="sha256-YSWCMtmNZNwqex4CEw1nQhvFub2lmU7vcCKP+XVwwXA="
      crossorigin=""/>

<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"
        integrity="sha256-Hk4dIpcqOSb0hZjgyvFOP+cEmDXUKKNE/tT542ZbNQg="
        crossorigin=""></script>

Jetzt können wir eine markerClusterGroup erstellen und die Marker hinzufügen. Am Ende müssen wir die Gruppe dann noch zur Karte hinzufügen:

const markers = L.markerClusterGroup();

cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    markers.addLayer( marker );
} );

markers.addTo( map );

Standardmäßig werden Marker in einem max. Radius von 80 Pixeln zu einem Cluster hinzugefügt. Für die Deutschlandkarte würde das dann wie folgt aussehen:

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Das mag zwar die effektivste Clusterung sein, aber es sieht nicht wirklich schön aus, da wir in manchen Bundesländern mehrere Cluster haben, in anderen aber keine. Daher möchte ich ein Cluster pro Bundesland haben.

Marker nach Bundesländern clustern

Wir können mehrere markerClusterGroup Objekte zu einer Karte hinzufügen. Wir erstellen also 16 Gruppen für die 16 Bundesländer und fügen dann die Marker der jeweils korrekten Gruppe hinzu. Sehen wir uns zunächst einmal den Code dafür an:

const stateMarkers = {};

// Get a unique list of all states from the cities array.
[ ...new Set( cities.map( city => city.state ) ) ].map( state => {
    // Create a markerClusterGroup per state.
    stateMarkers[state] = L.markerClusterGroup( {
        maxClusterRadius: 1000,
        spiderfyOnMaxZoom: false,
        showCoverageOnHover: false,
        disableClusteringAtZoom: 8,
    } );
} );

// Create city markers and add them to the correct markerClusterGroup.
cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    stateMarkers[city.state].addLayer( marker );
} );

// Add all markerClusterGroups to the map.
Object.keys( stateMarkers ).map( state => {
    stateMarkers[state].addTo( map );
} );

Zuerst einmal erstellen wir ein Objekt für die Gruppen aller Bundesländer. Dann verwenden wir etwas JavaScript-Magie, um eine Liste mit allen Bundesländernamen aus dem cities Objekt zu bekommen und erstellen dann jeweils eine Gruppe. Für die Cluster-Gruppen setzen wir den maxClusterRadius auf 100 Pixel, da unsere Karte kleiner als 1000 Pixel ist und somit jede Cluster-Gruppe mindestens so groß ist, wie jedes Bundesland. Wir deaktivieren dann noch zwei Optionen, die wir nicht brauchen und begrenzen das Zoom-Level auf 8. Bei diesem Zoom-Level werden dann alle Cluster aufgelöst, sobald ein Cluster angeklickt wird.

Nachdem wir unsere Gruppen haben, erstellen wir die Marker und fügen sie den Gruppen hinzu. Am Ende fügen wir dann noch alle Gruppen zu Karte hinzu. Damit erhalten wir dann das folgende Ergebnis (Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Manche Bundesländer haben nur einen Marker. In diesem Fall werden sie einfach angezeigt. Alle andern haben Cluster. Für Nordrhein-Westfalen haben wir ein Cluster mit 30 Markern. Klicken wir darauf, erhalten wir die folgende gezoomte Ansicht (Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, gezoomt, CC BY-NC 4.0

Falls eure Karte mehr Marker hat oder diese näher zusammen sind, müsste ihr die Optionen für die Cluster-Gruppen eventuell etwas anders einstellen. Für dieses Beispiel würde ich sagen, dass sie nun anklickbar sind.

Fazit

Das sollte der letzte Beitrag in meiner kleinen Karten-Reihe gewesen sein. Es gibt sicher noch dutzende Themen rund um Karten im Allgemeinen oder zu Leaflet, aber dazu gibt es andere Blogs oder Dokumentationen.

Ich hoffe, dass ihr nun auch Lust habt, mal eigene (dynamische) Karten zu erstellen. Und auch die Beispiele aus diesem Beitrag findet ihr wieder in einer einzelnen HTML-Datei in einem neuen Branch auf GitHub.

Falls ihr noch weiter Themen habt, die ich behandeln soll, hiterlasst bitte einen Kommentar.

Verwende Marker auf deinem eigenen Bild mit Leaflet

In den letzten beiden Blogbeiträgen haben wir PHP-Code zum Geocoding geschrieben. Diese Woche möchte ich euch zeigen, wie ihr etwas ähnliches Leaflet umsetzen könnt, einer bekannten JavaScript-Bibliothek zum Erstellen von Karten. Da es auch eine Funktion zum Hinzufügen eigener Bild-Layer hat, werden wir dies nutzen, um erneut unsere Karte zu erstellen.

Mit Leaflet loslegen

Wenn man Leaflet verwendet, dann muss man erst einmal ein wenig (externes) CSS und JavaScript laden. Ihr könnt mehr dazu im Quick Start Guide nachlesen, aber im Grunde braucht ihr die folgenden Dinge:

 <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
     integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
     crossorigin=""/>

 <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
     integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
     crossorigin=""></script>

<div id="leaflet-map"></div>

Neben dem CSS und JS braucht ihr noch einen Container, den die Map verwenden wird. Ihr könnt auch ein paar Styles angeben, aber dazu kommen wir gleich.

Die Karte aufsetzen

Wie auch in unseren vorherigen Beispielen werden wir erneut eine Deutschlandkarte verwenden, mit einer spezifischen Größe und Grenzen. Diese müssen wir auch bei Leaflet angeben:

// Create the map object.
const map = L.map( 'leaflet-map', {
    center: [ 51.1642, 10.4541 ],
    maxZoom: 10,
    zoomDelta: 1,
    zoomSnap: 0,
    scrollWheelZoom: false,
    trackResize: true,
    attributionControl: false,
} );

// Define the image overlay and its boundaries.L.marker([52.5162746,13.3777041]).addTo(map);
const imageUrl = './ammap-germany-low.svg';
const imageBounds = [
    [ 55.051693, 5.864765 ],
    [ 47.269299, 15.043380 ],
];

// Add the overlay to the map.
L.imageOverlay( imageUrl, imageBounds ).addTo( map );

// Automatically zoom the map to the boundaries.
map.fitBounds( imageBounds );

Zuerst erstellen wir das map Objekt mit einer Basis-Konfiguration. Dann definieren wir die Bildquelle und die Grenzen als GPS-Koordinaten, bevor wir sie schließlich zur Karte hinzufügen. Im letzten Schritt stellen wir sicher, dass das Bild in den Container eingepasst wird.

Der Container sollte eine gute Größe haben, um die Karte darzustellen und auch deren Seitenverhältnis. Wir verwenden einfach die SVG-Größe in unserem CSS:

#leaflet-map {
    width: 585.506px;
    height: 791.999px;
    background: white;
    max-width: 100%;
    max-height: 100%;
}

Es muss nicht so exakt angegeben werden und ihr könnt auch einen Container wählen, der größer/kleiner als das SVG ist.

Hinzufügen der Marker zur Karte

Nachdem wir nun die Karte haben, stellt sich die Frage, wie wir die Marker hinzufügen. Nun, da wir eine Karten-Bibliothek verwenden, ist das sehr einfach. Für einen Marker auf das Brandenburger Tor brauchen wir nur eine Zeile:

L.marker([52.5162746,13.3777041]).addTo(map);

Das ist wirklich alles! Keine Transformation von GPS zu Pixel-Koordinaten mehr. Das wird alles von Leaflet erledigt. Wir bekommen dann das folgende Ergebnis (nur ein Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Der einzige Nachteil ist, dass Leaflet standardmäßig nur die EPSG:3857 Projektion (Web Mercator) unterstützt. Wenn ihr eine andere Projektion benötigt, dann gibt es aber eine PROJ Erweiterung: Proj4Leaflet.

Hinzufügen aller Landeshauptstädte zur Deutschlandkarte

Vermutlich möchtet ihr auf eurer Karte nicht nur einen einzelnen Marker darstellen. Fügen wir also ein paar mehr Marker hinzu. Ich habe hierzu eine Liste aller 16 Landeshauptstädte gemacht:

var capitals = [
    {
        'state': 'BB', 'name': 'Potsdam', 
        'lat': 52.4009309, 'lng': 13.0591397,
        'url': 'https://de.wikipedia.org/wiki/Potsdam'
    },
    {
        'state': 'BE', 'name': 'Berlin', 
        'lat': 52.5170365, 'lng': 13.3888599,
        'url': 'https://de.wikipedia.org/wiki/Berlin'
    },
    {
        'state': 'BW', 'name': 'Stuttgart', 
        'lat': 48.7784485, 'lng': 9.1800132,
        'url': 'https://de.wikipedia.org/wiki/Stuttgart'
    },
    {
        'state': 'BY', 'name': 'München', 
        'lat': 48.1371079, 'lng': 11.5753822,
        'url': 'https://de.wikipedia.org/wiki/München'
    },
    {
        'state': 'HB', 'name': 'Bremen', 
        'lat': 53.0758196, 'lng': 8.8071646,
        'url': 'https://de.wikipedia.org/wiki/Bremen'
    },
    {
        'state': 'HE', 'name': 'Wiesbaden', 
        'lat': 50.0820384, 'lng': 8.2416556,
        'url': 'https://de.wikipedia.org/wiki/Wiesbaden'
    },
    {
        'state': 'HH', 'name': 'Hamburg', 
        'lat': 53.550341, 'lng': 10.000654,
        'url': 'https://de.wikipedia.org/wiki/Hamburg'
    },
    {
        'state': 'MV', 'name': 'Schwerin', 
        'lat': 53.6288297, 'lng': 11.4148038,
        'url': 'https://de.wikipedia.org/wiki/Schwerin'
    },
    {
        'state': 'NI', 'name': 'Hannover', 
        'lat': 52.3744779, 'lng': 52.3744779,
        'url': 'https://de.wikipedia.org/wiki/Hannover'
    },
    {
        'state': 'NW', 'name': 'Düsseldorf', 
        'lat': 51.2254018, 'lng': 6.7763137,
        'url': 'https://de.wikipedia.org/wiki/Düsseldorf'
    },
    {
        'state': 'RP', 'name': 'Mainz', 
        'lat': 50.0012314, 'lng': 8.2762513,
        'url': 'https://de.wikipedia.org/wiki/Mainz'
    },
    {
        'state': 'SH', 'name': 'Kiel', 
        'lat': 54.3227085, 'lng': 10.135555,
        'url': 'https://de.wikipedia.org/wiki/Kiel'
    },
    {
        'state': 'SL', 'name': 'Saarbrücken', 
        'lat': 49.234362, 'lng': 6.996379,
        'url': 'https://de.wikipedia.org/wiki/Saarbrücken'
    },
    {
        'state': 'SN', 'name': 'Dresden', 
        'lat': 51.0493286, 'lng': 13.7381437,
        'url': 'https://de.wikipedia.org/wiki/Dresden'
    },
    {
        'state': 'ST', 'name': 'Magdeburg', 
        'lat': 52.1315889, 'lng': 11.6399609,
        'url': 'https://de.wikipedia.org/wiki/Magdeburg'
    },
    {
        'state': 'TH', 'name': 'Erfurt', 
        'lat': 50.9777974, 'lng': 11.0287364,
        'url': 'https://de.wikipedia.org/wiki/Erfurt'
    },
];

Ähnlich zum Beispiel aus den anderen beiden Blogbeiträgen möchten wir diese Marker auch verlinken. Ich habe hierzu einfach die Links zur deutschen Wikipedia verwendet. Ein Marker kann verschiedene Event-Handler bekommen. Wir verwenden hier den onclick Handler. Das Hinzufügen aller Marker wird dann mit diesem Code umgesetzt:

capitals.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    marker.addTo( map );
} );

Das liefert uns das folgende Ergebnis (beim Hover über einen Marker erscheint zusätzlich der Name der Stadt als Titel):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Standardmäßig können wir in die Karte reinzoomen, aber auch raus. Das ist für ein Bild-Overlay in der Regel nicht optimal. Wenn wir rein und wieder rauszoomen, wollen wir die Karte wieder im Container zentrieren. Beides kann mit folgendem Code erreicht werden, den ich bei den Karten verwendet habe:

// Set the current min zoom to the zoom level after init.
map.setMinZoom( map.getZoom() );

// Re-center the map when zoomed to minZoom level.
map.on( 'zoomend', function () {
    if ( map.getZoom() === map.getMinZoom() ) {
        map.fitBounds( imageBounds );
    }
} );

Fazit

In den letzten drei Blogbeiträgen habt ihr gelernt, wie ihr mit euren eigenen Bildern individuelle Karten erstellen könnt. Wenn ihr nur eine statische Map braucht, dann ist der PHP-Ansatz wohl der beste. Wenn ihr allerdings eine interaktive Karte, ähnlich wie Google Maps (mit Zoomen, etc.) benötigt, dann wollt ihr vielleicht eher Leaflet verwenden.

Falls ihr auch diesen Code wieder selbst testen wollte, dann findet ihr eine lauffähige Version in einem neuen Branch auf GitHub. Ich habe dort alles in einer einzelnen HTML-Datei kombiniert, ihr könnte es aber natürlich auch auf mehrere Dateien aufteilen, z.B. in einem WordPress Theme/Plugin.

Ich habe noch eine Idee für einen „Bonus-Blogbeitrag“ in dieser Serie, aber vielleicht gibt es noch ein weiteres Thema zu interaktiven und individuellen Karten. ☺️

Marker zu einem Satellitenbild hinzufügen

In meinem vorherigen Blogbeitrag habe ich euch gezeigt, wie ihr einen Marker für eine GPS Koordinate zu einer SVG-Bild hinzufügen könnt. In diesem Falle musstet ihr die x/y-Positionen berechnen und konntet dann einen SVG-Kreis oder einen Marker-Pfad zeichnen. Aber was machen wir, wenn wir ein Satellitenbild haben – oder eine andere Pixelgrafik?

Lösung: verwendet auch hier ein SVG 😉

OK, nicht einfach irgendein SVG. Die Technik, die wir hier verwenden werden, könnte man als „moderne Image-Map“ bezeichnen. Diejenigen von euch, die schon länger Websites bauen, werden vermutlich noch den <map> Element kennen, mit dessen Hilfe man eine <area> auf einem <img> anklickbar machen konnte. Die Formen waren dabei aber recht eingeschränkt und bei weitem nicht so flexibel und präzise, wie es mit SVG-Pfaden möglich ist. Statt also eine traditionelle Image-Map zu verwenden, setzen wir ein (leeres) SVG ein, welches wir dann über das Bild legen, um eine „anklickbare Karte“ zu erstellen. Nun, genau genommen erstellen wir nur ein anklickbares SVG, das als Overlay über dem eigentlichen Bild liegt, aber es erzielt genau das Ergebnis, das wir haben wollen. Für diesem Blogbeitrag habe ich folgendes Satellitenbild von Berlin ausgesucht:

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1CC BY 2.0

Die Bildgrenzen berechnen

Wie ich bereits im vorherigen Beitrag erwähnt habe, müsst ihr erst einmal die Grenzen der Karte herausfinden, auf die ihr dann die Marker platzieren möchtet. Wir fangen mit den Grenzen für das Bild an, was der Größe der Pixelgrafik entspricht:

// Init PixelGeocoder using WGS84 and Mercato projection.
$pixel_geocoder = new PixelGeocoder( 'EPSG:4326', 'EPSG:3857' );
// Set boundaries for the map.
$pixel_geocoder->image_boundaries = [
	'xmin' => 0,
	'xmax' => 2400,
	'ymin' => 0,
	'ymax' => 1800,
];

Jetzt müssen wir die GPS-Grenzen des Bildes finden. Diese haben wir für das Bild natürlich nicht. Um sie zu erhalten, habe ich mir einige markante Punkte am Rand der Karte gesucht, die ich dann auf Google Maps finden konnte. So habe ich die folgenden GPS-Koordinaten bekommen, die ich dann als Referenzpunkte verwenden konnte:

$map_edges = [
	[ 13.0467623, 52.5594922 ], // West.
	[ 13.1993623, 52.6484712 ], // North.
	[ 13.5841963, 52.4416892 ], // East.
	[ 13.2766553, 52.4069153 ], // South.
];

$pixel_geocoder->setDstBoundaries(
	$map_edges,
	false,
	true
);

Nachdem wir nun die Grenzen definiert haben, können wir erneut die Koordinaten für das Brandenburger Tor berechnen:

// Calculate the coordinates.
$bb_gate_lat     = 13.3777041;
$bb_gate_lng     = 52.5162746;
$bb_gate_dst_arr = $pixel_geocoder->transformGPStoMapProjection( $bb_gate_lat, $bb_gate_lng );
$bb_gate_coords  = $pixel_geocoder->calculateCoordinatesToPixel( $bb_gate_dst_arr[0], $bb_gate_dst_arr[1] );

var_dump( bb_gate_coords );
/**
 * array(2) {
 *   [0]=>
 *   float(1477.8750879177708)
 *   [1]=>
 *   float(986.3143837577029)
 * }
 */

Aber wie bekommen wir das nun als Marker in einem Overlay über unser Satellitenbild? Hier kommt nun unsere SVG-Image-Map ins Spiel.

Erstellen einer SVG-Image-Map

Eine SVG-Imag-Map ist im Grunde eine leere „SVG-Leinwand“, auf die wir Dinge platzieren können. Wir definieren also ein einzelnes SVG Element mit der gleichen Höhe und Breite wie unser Satellitenbild. Das Bild selbst setzen wir als Geschwister-Element daneben und umschließen beides mit einem Container-Element und setzen ein paar Styles:

<div class="image-map">
	<img class="image-map-background" src="./Berlin-Germany-Flickr-NASA-Goddard-Photo-and-Video1.jpg" alt="Berlin NASA image"/>
	<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800"></svg>
</div>

Um das SVG über dem Bild zu platzieren, definieren wir die Höhe und Breite für den Container (dieser sollte das gleiche „Seitenverhältnis“ wie das Bild haben):

.image-map {
	position: relative;
	width: 600px;
	height: 450px;
}

.image-map-background,
.dynamic-map {
	max-width: 100%;
	height: auto;
}

.dynamic-map {
	position: absolute;
	top: 0;
	left: 0;
}

Jetzt können wir unsere Marker hinzufügen. Ich hole mir dazu meistens eine Liste von Markern, entweder aus einem statischen Array oder aber mit einer WP_Query und einigen Meta-Feldern. Nehmen wir für unser Beispiel einfach ein statisches Array mit dem bereits berechneten Marker:

$markers = [
	[
		'name'  => 'brandenburg-gate',
		'title' => 'Brandenburg Gate',
		'x'	 => $bb_gate_coords[0],
		'y'	 => $bb_gate_coords[1],
		'url'   => 'https://en.wikipedia.org/wiki/Brandenburg_Gate',
	]
];

Da wir vorhaben, die SVG-Pfade und Links auszugeben, können wir hier ein kleines Marker-Template schreiben, das uns die Ausgabe etwas erleichtert:

$marker_markup = '
	<a xlink:title="%1$s" target="_parent" class="marker" id="%2$s" xlink:href="/%3$s/" transform="translate(%4$s,%5$s)">
		<path fill="#c10926" fill-rule="evenodd" d="m -0.266,-28.261 a 4.504,4.504 0 0 0 3.204,-1.343 4.613,4.613 0 0 0 1.327,-3.242 4.615,4.615 0 0 0 -1.327,-3.244 4.508,4.508 0 0 0 -3.204,-1.343 4.512,4.512 0 0 0 -3.206,1.343 4.619,4.619 0 0 0 -1.327,3.244 c 0,1.215 0.478,2.382 1.327,3.242 a 4.51,4.51 0 0 0 3.206,1.343 m -0.613,27.98 -8.895,-28.49 h 0.013 a 10.555,10.555 0 0 1 -0.818,-4.074 c 0,-2.77 1.086,-5.425 3.02,-7.381 a 10.251,10.251 0 0 1 7.294,-3.056 c 2.735,0 5.358,1.099 7.293,3.056 a 10.502,10.502 0 0 1 3.021,7.38 c 0,1.414 -0.284,2.798 -0.819,4.076 h 0.012 z" clip-rule="evenodd"/>
	</a>';

Fügen wir das ganze jetzt in einer Schleife innerhalb unseres SVG zusammen:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800">
	<?php foreach ( $markers as $marker ) : ?>
		<?php
		printf(
			$marker_markup,
			$marker['title'],
			$marker['name'],
			$marker['url'],
			$marker['x'],
			$marker['y']
		);
		?>
	<?php endforeach; ?>
</svg>

Durch die printf() Funktion ist können wir die dynamischen Teile ins Template einfügen. Wenn ihr das in WordPress macht, dann stellt bitte sicher, dass ihr die esc_*() Escape-Funktionen für dynamische Werte verwendet.

Ich habe den gleichen Ansatz auch gewählt, um die „Karten-Grenzpunkte“ mit einem Cyan-gefärbten Kreis zu markieren. Das Resultat sieht dann wie folgt aus (dies hier ist nur ein Screenshot des Resultats):

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1, bearbeitet von Bernhard Kau, CC BY 2.0

Da die Marker recht klein waren, habe ich diese etwas hochskaliert. Da sich der Marker an der Spitze skaliert, kann das mit einer einzelnen Zeile CSS erreicht werden:

.marker path {
	transform: scale(5);
}

Fazit

In einer sehr ähnlichen Art und Weise haben wir ein SVG-Bild erstellt, das anklickbare Marker auf ein Satellitenbild legt. Für das Bild einer Stadt reicht hier wohl ein normales Satellitenbild aus. Für eine größere Region müsst ihr aber eines in der Mercator-Projektion (oder einer anderen von PROJ unterstützten Projektion) finden. Ich habe einige Zeit gebraucht, um ein geeignetes CC-lizensiertes Bild zu finden, das ich als Beispiel für diesen Blogbeitrag verwenden konnte.

Falls ihr auch diesen Code einmal selbst ausprobieren wollt, dann findet ihr einen neuen Branch mit den verschiedenen Teilen in einer kombinierten neuen PHP-Datei auf GitHub. Dort findet ihr auch die verwendeten Satellitenbilder.

Ich hoffe, dass euch dieser Beitrag einen weiteren schönen Weg zeigen konnte, mit dem ihr individuelle Karten erstellen könnt. Wir haben aber recht viel eigenen PHP-Code und eine externe Bibliothek verwenden. Im nächsten (und vermutlich auch letzten) Beitrag dieser Karten-Reihe, möchte ich euch eine Bibliothek vorstellen, die ich vielleicht schon verwendet habt, aber noch nicht in dieser Weise. Bleibt also weiter dabei, denn das Jahr mag bald zu Ende gehen, aber die Kalenderwoche 52 hat noch einen Tag im Jahr 2023! ?