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! ?

Geo-Koordinaten zu Pixel-Positionen – Erstelle eine Karte mit deinen eigenen Bildern

Dieser Beitrag soll der Start einer kleinen Beitragsreihe sein. Für ein Projekt musste ich Marker von Städten auf einer Deutschlandkarte platzieren. Am Anfang habe ich das noch manuell gemacht, aber irgendwann ist die Karte auf über 100 Marker angestiegen und es musste eine programmatische Lösung her.

Kartenprojektionen

Auch wenn es einige Menschen gibt, die denken, dass die Erde flach ist, ist die rund. Also nicht wirklich perfekt rund. Da unsere Monitore aber flach sind (genau wie schon Papierkarten zuvor), gibt es verschiedene „Kartenprojektionen„. Eine der am weitesten verbreiteten ist die Mercator projection, welche unter anderem von Google Maps, Bing Maps, OpenStreetMap und vielen anderen Diensten verwendet wird. Es ist auch unter der Bezeichnung EPSG:3857 bekannt, auf die wir später zurückkommen werden.

GPS-Koordinaten

Wenn man eine Position auf der Erde angibt, verwendet man in der Regel das World Geodetic System 1984 (WGS 84) mit Längen- und Breitengraden für den Ort. Für das Brandenburger Tor in Berlin sind diese beispielsweise 52.5162746 (Breitengrad) und 13.3777041 (Längengrad). Vielleicht habt ihr diese Art von Koordinatenangaben schon einmal gesehen. Eventuell kennt ihr aber auch die Angabe von Grad, Minuten und Sekunden, wobei es dann 52° 30' 58.59" (Breitengrad) und 13° 22' 39.7338" (Längengrad) sind. Aber mit der ersten Darstellungsart kann man sehr viel einfacher Berechnungen durchführen.

SVG-Karten finden

Nachdem wir uns nun ein wenig mit der Theorie beschäftigt haben, können wir über Code sprechen. Zuerst einmal müssen wir eine Karte finde. Das kann alles Mögliche sein. Eine Luftaufnahme, ein Satellitenbild oder ein einfaches Bild. Ich verwende sehr gerne SVG Bilder. Diese findet man auf vielen verschiedenen Plattformen. Wikipedia ist hier oft eine gute Quelle für Karten und viele davon sind unter Creative Commons lizenziert. Wir können dort zum Beispiel eine schöne Karte von Deutschland inkl. der Bundesländergrenzen finden. Leider verwendet sie eine andere Kartenprojektion. Ich habe daher für diesen Blogbeitrag eine kostenlose SVG Karte von amCharts verwendet:

Map of Germany
© ammap.com | SVG map of Germany (low detail), Code optimiert von Bernhard Kau, CC BY-NC 4.0

Es gibt auch kommerzielle Anbieter und spezielle Stock-Foto-Shops für Vektorgrafiken. Aber egal welche ihr verwendet, stellt bitte sicher, dass ihr die Lizenz respektiert.

Marker geokodieren

Wir haben eine Map, fangen wir also an, die Marker zu setzen. Ihr werden von den Orten vermutlich nur die GPS-Koordinaten kennen. Diese müssen wir daher zuvor in „x/y Pixel-Koordinaten“ für die SVG-Karte konvertieren. Nach ein wenig Recherche bin ich auf die PROJ library gestoßen und glücklicherweise gibt es hier auch eine PHP Variante von PROJ. Diese installieren wir uns mit Composer:

composer require proj4php/proj4php

Jetzt müssen wir ein paar Initialisierungen durchführen. Wir konvertieren Werte zwischen zwei Projektionssystemen und erzeugen uns diese wie folgt:

require 'vendor/autoload.php';

// Initialize the PROJ library.
$proj = new Proj4php();

// Set the source and destination projection.
$src_projection = new Proj( 'EPSG:4326', $proj );
$dst_projection = new Proj( 'EPSG:3857', $proj );

Beim EPSG:4326 Projektionssystem, unsere Quellprojektion, handelt es sich um das WGS 84 System für GPS-Koordinaten. Bei EPSG:3857 hingegen handelt es ich um die „Sphärische Mercator“ Projektion, die von Diensten wie Google Maps, OpenStreetMap und eben auch unserer SVG verwendet wird. Die erste Projektion definiert also Koordinaten auf einem „Globus“ (einer sphärischen Karte) während die zweite Projektion Koordinaten auf einer flachen Karte (in einem von mehreren Projektionssystemen) definiert.

Nachdem wir dieses Basissetup abgeschlossen haben, können wir unsere erste Konvertierung durchführen. Wir nehmen die GPS von Berlin und konvertieren diese in Mercator Koordinaten:

$src_berlin = new Point( 13.3777041, 52.5162746, $src_projection );
print_r( $src_berlin->toArray() );
/**
 * Array
 * (
 *     [0] => 52.5162746
 *     [1] => 13.3777041
 *     [2] => 0
 * )
 */

$dst_berlin = $proj->transform( $dst_projection, $src_berlin );
print_r( $dst_berlin->toArray() );
/**
 * Array
 * (
 *     [0] => 1489199.2083951
 *     [1] => 6894018.2850289
 *     [2] => 0
 * )
 */

In Ordnung, jetzt haben wir ein paar neue Zahlen. Aber wie genau können wir diese nun nutzen? Wenn wir diese auf der SVG-Karte verwenden möchten, müssen wir sie noch in Pixel-Koordinaten des Bildes umrechnen.

Die Bildgrenzen finden

Wir brauchen jetzt die minimalen und maximalen Koordinaten auf der Karte. Wenn wir Glück haben, dann bekommen wir diese von der Seite, von der wir auch die Karte selbst bekommen haben. Leider liefert Wikipedia so etwas für seine Karten nicht mit. Ihr könnt sie sicher mit einer Suchmaschine herausfinden oder über eine offene API wie etwa die von OpenStreetMaps oder anderer Dienste herausfinden. Aber manchmal gibt es solche Daten nicht oder ihr wisst nicht, wo ihr sie finden könnt. Dann könnt ihr aber einfach versuchen, die Eckpunkte der Karte zu ermitteln. Für die Karte von Deutschland würde man also die Orte ganz im Norden, Osten, Süden und Westen suchen. Mit diesen Punkten können wir und dann die minimalen und maximalen Werte der Längen- und Breitengrade holen. Glücklicherweise liefert und aber die amCharts Karte diese Eckpunkte im Code der SVG direkt mit:

<amcharts:ammap projection="mercator" leftLongitude="5.864765" topLatitude="55.051693" rightLongitude="15.043380" bottomLatitude="47.269299"></amcharts:ammap>

Mit diesen beiden Referenzpunkten (die sich zwar nicht innerhalb der Landesgrenzen von Deutschland befinden, was aber kein Problem ist), können wir die Bildgrenzen wie folgt berechnen:

$swap_x = false;
$swap_y = true;

$dst_points_x = [];
$dst_points_y = [];

$src_points = [
	[ 15.043380, 47.269133 ],
	[ 5.865010, 55.057722 ],
];

foreach ( $src_points as $point ) {
	$src_point      = new Point( $point[0], $point[1], $src_projection );
	$dst_point      = $proj->transform( $dst_projection, $src_point );
	$dst_point_arr  = $dst_point->toArray();
	$dst_points_x[] = $dst_point_arr[0];
	$dst_points_y[] = $dst_point_arr[1];
}

$src_boundaries = [
	'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
	'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
	'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
	'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),
];

var_dump( $src_boundaries );
/**
 * array(4) {
 *   ["xmin"]=>
 *   float(653037.4250227585)
 *   ["xmax"]=>
 *   float(1668369.9214457471)
 *   ["ymin"]=>
 *   float(5986273.409259587)
 *   ["ymax"]=>
 *   float(7373214.063855921)
 * }
 */

Mit den beiden „swap“ Flags können wir angeben, dass sich die Koordinaten auf der anderen Seite des von 0° Länge oder 0° Breite befindet. Das Maximum würde dann zum Beispiel für die linke Bildgrenze verwendet und das Minimum für die rechte. Im Fall von Deutschland müssen wir $swap_y auf true setzen.

Jetzt können wir endlich mit den Grenzen die Koordinaten auf unserem Bild berechnen. Hierzu müssen wir noch zusätzlich die Größe des Bildes angeben. Die Berechnung sieht dann wie folgt aus:

$image_boundaries = [
	'xmin' => 0,
	'xmax' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,
];

$dst_berlin_arr = $dst_berlin->toArray();
$lng = $dst_berlin_arr[0];
$lat = $dst_berlin_arr[1];

$x_pos = ( $lng - $src_boundaries['xmin'] ) / ( $src_boundaries['xmax'] - $src_boundaries['xmin'] ) * ( $image_boundaries['xmax'] - $image_boundaries['xmin'] );

$y_pos = ( $lat - $src_boundaries['ymin'] ) / ( $src_boundaries['ymax'] - $src_boundaries['ymin'] ) * ( $image_boundaries['ymax'] - $image_boundaries['ymin'] );

var_dump( [ $x_pos, $y_pos ] );
/**
 * array(2) {
 *   [0]=>
 *   float(487.1242093149932)
 *   [1]=>
 *   float(523.9253760603847)
 * }
 */

Zuerst definieren wir also die Größe der SVG Deutschlandkarte in Pixeln. Dann benutzen wir den schon zuvor berechneten $dst_berlin Punkt für den Längen- und Breitengrad. Mit $src_boundaries['xmax'] - $src_boundaries['xmin'] können wir die „Breite“ der Quellenprojektion finden (genau wie auch für die Bildgrenzen). Wir ziehen dann die linke Grenze von unserem Koordinatenpunkt ab. Diesen dividieren wir durch die Breite der Quellprojektion und multiplizieren ihn mit der Breite der Zielprojektion (also der Breite des Bildes). Das Gleiche machen wir für die Y-Achse. Um den Punkt zu visualisieren, können wir einfach einen SVG-Kreis zeichnen. Das könnte im Code dann wie folgt aussehen:

<circle cx="482.18464676347816" cy="273.64009871474894" r="5" stroke="red" stroke-width="2" fill="transparent" />

Wenn wir diesen nun der ursprünglichen SVG-Karte hinzufügen, erhalten wir das Folgende:

Map of Germany with a circle on Berlin
© ammap.com | SVG map of Germany (low detail), Code optimiert und bearbeitet von Bernhard Kau, CC BY-NC 4.0

Alles zusammenfügen

Um mir diese ganzen Schritte zu erleichtern, habe ich mir eine kleine PixelGeocoder Klasse geschrieben, die die Initialisierung übernimmt und auch Hilfsfunktionen für die Berechnung der Grenzen sowie Methoden zum Berechnen der Koordinaten auf dem Bild enthält:

use proj4php\Proj4php;
use proj4php\Proj;
use proj4php\Point;

class PixelGeocoder {
	public $proj;
	public $src_proj;
	public $dst_proj;

	public $src_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,
	];

	public $image_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,
	];

	public function __construct( $src_proj_type = 'EPSG:4326', $dst_proj_type = 'EPSG:3857' ) {
		$this->proj     = new Proj4php();
		$this->src_proj = new Proj( $src_proj_type, $this->proj );
		$this->dst_proj = new Proj( $dst_proj_type, $this->proj );
	}

	public function setDstBoundaries( $points, $swap_x = false, $swap_y = false ) {
		$dst_points_x = [];
		$dst_points_y = [];

		foreach ( $points as $point ) {
			$dst_point      = $this->transformGPStoMapProjection( $point[0], $point[1] );
			$dst_points_x[] = $dst_point[0];
			$dst_points_y[] = $dst_point[1];
		}

		$this->src_boundaries = [
			'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
			'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
			'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
			'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),
		];
	}

	public function transformGPStoMapProjection( $lng, $lat ) {
		$src_point = new Point( $lng, $lat, $this->src_proj );
		$dst_point = $this->proj->transform( $this->dst_proj, $src_point );

		return $dst_point->toArray();
	}

	public function calculateCoordinatesToPixel( $lng, $lat ) {
		return [
			( $lng - $this->src_boundaries['xmin'] ) / ( $this->src_boundaries['xmax'] - $this->src_boundaries['xmin'] ) * ( $this->image_boundaries['xmax'] - $this->image_boundaries['xmin'] ),
			( $lat - $this->src_boundaries['ymin'] ) / ( $this->src_boundaries['ymax'] - $this->src_boundaries['ymin'] ) * ( $this->image_boundaries['ymax'] - $this->image_boundaries['ymin'] ),
		];
	}
}

Wenn ihr diese Klasse nun verwenden wollt, geht das wie folgt:

require_once 'vendor/autoload.php';
require_once 'PixelGeocoder.php';

// 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' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,
];
$pixel_geocoder->setDstBoundaries(
	[
		[ 15.043380, 47.269133 ],
		[ 5.865010, 55.057722 ],
	],
	false,
	true
);

// Calculate the coordinates.
$berlin_lat     = 13.3777041;
$berlin_lng     = 52.5162746;
$dst_berlin_arr = $pixel_geocoder->transformGPStoMapProjection( $berlin_lat, $berlin_lng );
$image_coords   = $pixel_geocoder->calculateCoordinatesToPixel( $dst_berlin_arr[0], $dst_berlin_arr[1] );

var_dump( $image_coords );
/**
 * array(2) {
 *   [0]=>
 *   float(479.2493080704524)
 *   [1]=>
 *   float(273.55748351793665)
 * }
 */

Bonus: einen klickbaren Marker verwenden

Der Kreis ist einfach und schön, aber vermutlich wollt ihr eine Karte bauen, die Marker enthält, die dann auch anklickbar sind. Da wir bereits eine SVG verwenden, können wir uns einen solchen Pfad zeichnen und diesen dann mit dem transform="translate(x,y)" auf der Karte verschieben:

<a xlink:title="Link to berlin.de" target="_parent" xlink:href="https://berlin.de/" transform="translate(479.2493080704524,273.55748351793665)">
	<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>

Wenn wir diesen nun zu unserer Karte hinzufügen, dann sieht es wie folgt aus:

Map of Germany with a clickable marker on Berlin
© ammap.com | SVG map of Germany (low detail), Code optimiert und bearbeitet von Bernhard Kau, CC BY-NC 4.0

Fazit

Wir alle lieben Karten, richtig? Aber manchmal sehen eine Google Map oder OpenStreetMap Version nicht wirklich schön aus. Mit ein wenig eigenem Code und ein wenig Vorbereitung können schöne Karten auf Grundlage (unserer eigenen) Bilder erstellen. Ich habe das oben beschriebene für eine Deutschlandkarte gemacht, die ihre GPS-Koordinaten von einem Custom-Post-Type aus WordPress bekam und dann dynamisch das SVG-Markup der Städte in einem Shortcode (später in einem Server-Side-Rendered Block) generiert.

Falls ihr den Code einmal selbst testen wollt, findet ihr alles auf GitHub. Es ist ein „Proof of Concept“ und funktioniert, aber die PHP-Klasse könnte sicher eine bessere Architektur und vermutlich auch ein paar mehr Hilfsfunktionen gebrauchen. Ihr könnt den Code also gerne für eure Bedürfnisse anpassen.

In meinem nächsten Blogbeitrag werde ich euch zeigen, wie mein eine Pixelgrafik anstelle einer SVG-Karte verwenden kann, bleibt also weiter dabei, das Jahr ist noch nicht zu Ende! ?

Fatale Fehler in WordPress mit PHP 8+ und einer fehlerhaften Übersetzung

Letzte Woche habe ich die Meldung bekommen, dass eine Seite kaputt war, die mich ein wenig überrascht hat. Ich habe hier den Fehler mal in einem Dummy-Plugin reproduziert, um euch zeigen zu können, was schiefgelaufen ist. Beim Aufruf der defekten Seite wurde ich mir die folgende Fehlermeldung präsentiert:

Fatal error: Uncaught Error: Missing format specifier at end of string
in /var/www/html/wp-content/plugins/broken-format-string/broken-format-string.php on line 15

Also habe ich mir den Code dazu angesehen und dort stand Folgendes:

printf(
	__( 'Publish date: %s', 'broken-format-string' ),
	date_i18n( get_option( 'date_format' ), get_post_datetime() )
);

Nichts wirklich spannendes. Nur ein Format-String mit einem Platzhalter für einen String, der mit dem Veröffentlichungsdatum im WordPress-Datumsformat ersetzt wird.

Fehlerhafte Übersetzung

Ich konnte also nicht wirklich nachvollziehen, was genau hier der Fehler war. Aber da Sprache der Seite nicht „English (US)“ war, habe ich mir mal die Übersetzungsfunktion angesehen. Hier wieder ein einfaches Beispiel mit einem ähnlichen Fehler:

#: broken-format-string.php:14
msgid "Publish date: %s"
msgstr "Veröffentlichungsdatum: %"

Die deutsche Übersetzung des „Publish date:“ ist korrekt, aber der Platzhalter wird hier nur ein % verwendet und nicht %s, was dann zu diesem Fehler führt. WordPress würde hier also den Original-String ins Deutsche übersetzen und an die printf() Funktion weitergeben, was dann zu dem „Fatal error“ führt.

Unterschiedliche Fehlerbehandlung in PHP 8+

Wenn ihr diesen Code in PHP 7.4 oder früher ausführt, dann bekommt ihr keinen fatalen Fehler. Ihr bekommt noch nicht einmal eine PHP Warning oder Notice. Es würde einfach der Platzhalter nicht korrekt ersetzt. Das % würde einfach durch einen leeren String ersetzt werden. Aber sobald ihr auf PHP 8+ aktualisiert, habt ihr eine defekte Seite.

Das war das erste Mal, dass ich wirklich ein Problem mit PHP 8+ auf einer WordPress-Website festgestellt habe. Vielleicht hatte ich einfach Göück, oder aber die Übersetzungen hatten die Platzhalter immer korrekt übersetzt.

Fazit

Ich habe häufiger mal Prüfungen auf PHP 8+ Kompatibilität mit PHPCompatibilityWP gemacht und bisher hat es immer funktioniert. Aber ich hätte nie erwartet, dass eine fehlerhafte Übersetzung mal einen fatalen Fehler auslösen würde. Wenn man Übersetzungen mti GlotPress macht (was auf translate.wordpress.org verwendet wird), dann wird einem eine Warnung angezeigt, wenn in einer Übersetzung ein Platzhalter fehlt. Aber Tools wie Poedit zeigen hier leider keine Warnung an. Wenn ihr also jemanden damit beauftragt euer Plugin/Theme in einer Sprache zu übersetzten, die ihr selbst nicht sprecht, dann prüft am besten sehr genau, ob alle Format-String korrekt sind.

Die „text-underline-offset“ und viele weitere weniger bekannte CSS-Eigenschaften für Links

Die Arbeit für eine große Agentur hat viele Vorteile. Einer davon ist es, dass man mit vielen Menschen zusammenarbeitet, die sehr viel mehr über sehr viele Dinge wissen. Ich lese daher oft Commits von anderen und schaue mir an, was sie so machen. In einem Commit habe ich eine CSS-Eigenschaft entdeckt, sie ich noch nie zuvor gesehen hatte: text-underline-offset ?

Links gestalten

In den frühen Tagen des World Wide Web waren alle Links unterstrichen. Mit dem Aufkommen von CSS und modernen Design wollten Leute dann aber andere Styles für Links haben. Die Farbe und Position der text-decoration: underline wurde aber von der Schrift und ihrer Farbe vorgegeben und daher wurde sie oft durch einen border-bottom ersetzt. Das hatte aber mehrere negative Folgen, nicht zuletzt in vielen Fällen eine Verschlechterung der Barrierefreiheit von Links. Mit modernem CSS gibt es nun aber viele verschiedene CSS-Eigenschaften, um Links zu stylen … und von vielen davon habe ich noch nie gehört:

Auf den verlinkten MDN Web Docs Seiten könnt ihr viele der oben erwähnten CSS-Eigenschaften direkt selbst ausprobieren. Ich könnte euch sicher auch viele Bespiele nennen, wie man diese einsetzen kann und wie es dann aussehen würde, aber ich möchte spoilern und euch den Spaß nehmen, es selbst herauszufinden ?

Fazit

Bei der Entwicklung von Websites gibt es eine unumstößliche Wahrheit: Du kannst nicht alles kennen. Daher kann ich es nur allen empfehlen, den Code von anderen zu lesen und sich von deren Arbeit inspirieren zu lassen. Diese CSS-Eigenschaften mögen für eine von euch bereits bekannt sein, für mich waren die überragend!

Erstellen eines dynamischen iCalendar mit Blog-Beiträgen

Eine kleine lokale Website verwendet ein Terminbuchungs-Plugin. Diese Buchungen wurden anschließend manuell in einen Online-Kalender übertragen, damit das Team einen Überblick über die Buchungen hatte. Leider unterstützte das Plugin keine Funktion, mit der die Buchungen dynamisch in einer Kalender-App angezeigt werden konnten. Daher wurde ich gefragt, ob ich hier helfen kann. Um das Gezeigte aber für euch aber etwas nützlicher zu machen, werde ich nicht zeigen, die ich das für dieses spezielle Terminbuchungs-Plugin umgesetzt habe, sondern wie man den gleichen Ansatz verwenden kann, um alle eure veröffentlichten und geplanten Beiträge anzuzeigen.

Installieren der Abhängigkeiten

Wir werden eine dynamische .ical Datei erstellen. Das ist eine Textdatei und wir könnten den „Code“ einfach selbst erzeugen, aber wir verwenden eine kleine Bibliothek, die uns dabei helfen wird. Für das Projekt habe ich die spatie/icalendar-generator Library verwendet, welche alles hatte, was ich brauchte (und noch vieles mehr). Wir installieren sie einfach mit Composer in unseren Plugin-Ordner:

composer require spatie/icalendar-generator

Anschließend müssen wir die notwendigen Dateien laden. Der einfachste Weg, das mit Paketen zu tun, die mit Composer installiert wurden, ist die Verwendung des Composer-Autoloaders. Hierzu fügen wir diesen der PHP-Datei des Plugins hinzu:

require_once 'vendor/autoload.php';

Erstellung eines Kalenders

Jetzt können wir damit loslegen, einen dynamischen Kalender zu erstellen. Ich werde hier sehr einfache Codebeispiele verwenden, in der Dokumentation des Pakets findet ihr aber auch einige komplexere. Erstellen wir also erst einmal das $calender Objekt:

$calendar = Calendar::create( 'example.com' );

Der Parameter der create() Funktion wird für den Titel verwendet, er kann aber auch leer gelassen werden. Nachdem wir nun unser $calender Objekt haben, würden wir uns die Daten für die Events holen. Hier führen wir lediglich eine Query aus, die uns die letzten veröffentlichten und geplanten Blog-Beiträge holt:

// Get all blog posts.
$query_args = [
	'post_type'      => 'post',
	'post_status'    => [
		'publish',
		'future',
	],
	'posts_per_page' => - 1,
];

$posts = get_posts( $query_args );

Nachdem wir alle Beiträge geladen haben, können wir für jeden davon ein individuelles Event erstellen:

// Create an event per blog post.
foreach ( $posts as $post ) {
	$start_date = new DateTime( $post->post_date_gmt, new DateTimeZone( 'UTC' ) );
	$end_date   = ( clone $start_date )->add( new DateInterval( 'PT15M' ) );

	$event = Event::create();
	$event->name( $post->post_title );
	$event->startsAt( $start_date );
	$event->endsAt( $end_date );
	$event->uniqueIdentifier( $post->ID );

	$calendar->event( $event );
}

Wir verwenden hier GMT/UTC Zeiten, um es dem Kalender zu ermöglichen, sich an die lokale Zeitzone anzupassen. Für das „Enddatum“ addieren wir 15 Minuten dazu. Durch die Verwendung der Post-ID als „unique identifier“ kann eine Kalender-Applikation einfacher einzelne Events aktualisieren/synchronisieren. Zum Schluss fügen wir dann noch das Event zum zuvor erstellten $calendar Objekt hinzu.

Im letzten Schritt müssen wir dann noch sie Ausgabe senden. Das wird mit folgenden Zeilen erreicht:

// Print the calendar output.
header( 'Content-Type: text/calendar; charset=utf-8' );
echo $calendar->get();
exit;

Importieren des iCalendar in eine Kalender-Anwendung

Den fertigen dynamischen Kalender wollen wir jetzt vermutlich in eine Kalender-App importieren (und synchronisieren). Dazu muss er über eine URL aufrufbar sein, die wir in der Anwendung eintragen müssen. Ich habe mich dazu entschieden, hierfür einen eigenen REST-Endpoint zu erstellen. Den bisherigen Code habe ich also in eine Callback-Funktion verschoben und dann den Endpoint wie folgt registriert:

function blog_posts_calendar_register_rest_route() {
	register_rest_route(
		'blog-posts-calendar/v1',
		'/ical.ics',
		[
			'methods'             => 'GET',
			'callback'            => 'blog_posts_calendar_generate_ical',
			'permission_callback' => '__return_true',
		]
	);
}
add_action( 'rest_api_init', 'blog_posts_calendar_register_rest_route' );

Der Kalender kann dann über folgende URL aufgerufen werden: https://example.com/wp-json/blog-posts-calendar/v1/ical.ics

Der permission_callback würde es hier allen erlauben, den Kalender zu abonnieren. Wenn ihr etwas restriktiver sein wollt, könnt ihr hier eure eigene Callback-Funktion implementieren.

Fazit

Selbst wenn ein Plugin eine Funktion mal nicht anbieten, kann man meistens etwas eigenen Code schreiben, um es verfügbar zu machen. Das ist oft auch einfacher als das ganze Plugin durch ein anderes zu ersetzen, was dann vielleicht eine andere Funktion nicht mehr hat. Im Falle eines Terminbuchungs-Plugins kann es vermutlich besonders schwer sein, dies durch ein anderes zu ersetzen, da man auch alle Daten zum andern Plugin migrieren muss.

Wenn ihr den Code mal selbst mit eurer Website testen wollt, dann findet ihr in als Plugin auf GitHub.

Verwendung von unterschiedlichen Git-Einstellungen für persönliche und berufliche Projekte

Wenn ihr für eine Agentur oder selbstständig tätig seid, gleichzeitig aber auch persönliche Projekte habt, dann möchtet ihr vielleicht unterschiedliche Git-Konfigurationen für diese beiden Arten von Projekten verwenden. Wenn ihr euren Rechner aufsetzt, um an Projekten zu arbeiten, dann konfiguriert ihr einige grundlegende Tools wie Git und GitHub. Diese Einstellungen beinhalten in der Regel die Git-Autor E-Mail-Adresse und eventuell auch einen zugehörigen SSH/GPG-Key für die Signierung von Commits. Man kann normalerweise nur eine globale E-Mail-Adresse definieren, ihr verwendet hier dann also vermutlich die professionelle E-Mail-Adresse (des Unternehmens). Wenn ihr aber auch an privaten oder Open-Source-Projekten arbeiten wollt, dann bevorzugt ihr hier vielleicht eher eure persönliche GitHub E-Mail-Adresse. Diese Anleitung soll euch dabei helfen, das einzustellen.

Vorbereitung

In dieser Anleitung gehen wir davon aus, dass ihr PhpStorm verwendet und all eure Projekte, an denen ihr arbeitet, im Ordner ~/PhpstormProjects gespeichert sind. Derselbe Ansatz funktioniert aber natürlich auch mit einer anderen IDE und einer anderen Ordnerstruktur.

Damit ihr zwei verschiedene E-Mail-Adressen mit GitHub verwenden könnt, müsst ihr diese erst einmal verifizieren. Anschließend könnt ihr bei jeder Aktion, wie etwa dem Merge eines PR auf github.com, auswählen, welche ihr verwenden möchtet. Die Verifizierung ist weiterhin notwendig, damit ihr sie für die Signierung von Commits verwenden könnt.

Aufteilung euer Konfiguration und bedingtes Laden der Dateien

Normalerweise speichert ihr alle Git-Konfigurationen in der Datei ~/.gitconfig. Diese Datei könnte wie folgt aussehen:

[user]
	name = Jo Doe
	email = j.doe@company.com
	signingkey = ~/.ssh/id_ed25519.pub

In jedem Projekt, in dem ihr nun einen Commit macht, würde die company.com E-Mail-Adresse verwendet und der Commit mit dieser signiert. Wenn ihr eine andere E-Mail-Adresse verwenden wollt, müsstet ihr diese manuell bei jedem Commit angeben:

git commit -m"message" --author="Jo Doe <1234567+jo.doe@users.noreply.github.com>" --gpg-sign=~/.ssh/id_ed25519.pub

Das ist natürlich nicht wirklich praktikabel und ihr vergesst diese zusätzlichen Parameter vielleicht manchmal. Alternativ könnt ihr auch die Autor-Einstellungen in der projektspezifischen Konfigurationsdatei angeben. Aber das müsstet ihr in jedem einzelnen geklonten Projekt tun.

Es ist zwar ebenfalls technisch möglich, die Autor-Daten nachträglich zu ändern, aber das ist alles andere als leicht und erfordert auch einen interaktiven Rebase, der die Historie überschreibt und daher nicht für bereits gepushte Commits verwendet werden darf. Wie können wir es also sonst erreichen?

Bedingtes Laden einer Konfigurationsdatei

Innerhalb einer Git-Konfigurationsdatei kann man mit includeIf eine Datei aufgrund einer Bedingung laden. Der einfachste Weg ist es hierbei, dies abhängig vom Projektordner zu tun. Wir speichern hierzu einfach alle persönlichen und Open-Source-Projekte direkt im ~/PhpstormProjects Ordner und alle für das Unternehmen im Unterordner ~/PhpstormProjects/company. Wir können dann die folgenden Zeilen in unsere globale Git-Konfiguration einfügen:

# file: ~/.gitconfig

[includeIf "gitdir:~/PhpstormProjects/"]
	path = .gitconfig-general

[includeIf "gitdir:~/PhpstormProjects/company/"]
	path = .gitconfig-company

Wir verschieben dann alle spezifischen Eisntellungen in diese beiden Dateien. Das werden in der Regel mindestens die gesamten [user] Einstellungen sein, so wie eventuell noch ein paar mehr. Die allgemeine Datei könnte dann wie folgt aussehen:

# file: ~/.gitconfig-general

[user]
	name = Jo Doe
	email = 1234567+jo.doe@users.noreply.github.com
	signingkey = ~/.ssh/id_ed25519.pub

In diesem Codeschnippsel habe ich die GitHub „noreply“ E-Mail-Adresse verwendet, die ihr anstelle eurer persönlichen E-Mail-Adresse einstellen könnt, damit diese private belibt und nicht für den Versand von Spam missbraucht wird.

Für den beruflichen company Unterordner sieht die separate Konfigurationsdatei dann etwa so aus:

# file: ~/.gitconfig-company

[user]
	name = Jo Doe
	email = j.doe@company.com
	signingkey = ~/.ssh/id_ed25519_company.pub

Diese beiden Dateien werden in eurem Homeverzeichnis neben der globalen .giconfig Datei gespeichert. Wenn ihr weiter Einstellungen überschreiben möchtet, fügt sie einfach der jeweiligen Datei hinzu.

Auf der Git-Dokumentationsseite zu den includes findet ihr auch andere Bedingungen für das Laden von Dateien, unabhängig vom gitdir. Ihr könnt beispielsweise auch eine Konfigurationsdatei abhängig von der Remote-URL laden. Damit wäre es dann auch möglich ein company Repository in einen anderen Ordner als ~/PhpstormProjects/company/ zu klonen un trotzdem die .gitconfig-company Datei zu laden. Aber es ist vermutlich einfacher zu verstehen, welche Datei geladen wird, wenn man nach dem Ordner geht.

Bonus: Verwaltung von GitHub-Benachrichtigungen für mehrere E-Mail-Adressen

Wenn ihr eine Einladung zu einer GitHub-Organisation erhaltet, dann werden ihr in der Regel automatisch alle Benachrichtigungen von allen Repositories abonniert. Da ihr GitHub vermutlich zuvor nur für private Projekte verwendet habt, erhaltet ihr diese Benachrichtigungen dann auch alle an die private E-Mail-Adresse. Glücklicherweise kann man ein Routing für Benachrichtigungen basierend auf der Organisation einstellen.

Um eine solche Routing-Regel einzustellen, navigiert ihr zu „Settings | Notifications“. Dort klickt ihr dann auf den „Custom Routing“ Button und dann auf den „Add new route“ Button. Anschließend wählt ihr die Organisation und die E-Mail-Adresse aus und klickt auf „Save“. Wenn ihr Einladungen zu mehreren Organisationen bekommen habt, könnt ihr auch mehrere Regeln anlegen.

Falls ihr gitlab.com für private/berufliche Projekte verwendet, dann könnt ihr pro Gruppe oder Projekt eine E-Mail-Adresse für die Benachrichtigungen auswählen. Diese findet ihr in eurem Profil unter „User Settings | Notifications“.

Fazit

Die gleichzeitige Arbeit an privaten und beruflichen Git-Projekten auf einem Gerät kann die Git-Autor-Daten eurer Commits ziemlich durcheinanderbringen. Aber mit dem bedingten Laden von Konfigurationsdateien kann spezifische Einstellungen für die verschiedenen Arten von Projekten festlegen.

Führe deinen eigenen Code innerhalb einer Action nur einmal aus

WordPress bietet sehr viele Hooks an, mit denen man mit dem Code interagieren kann. Filter ändern dabei den Wert einer übergebenen Variablen und in der Regel möchte man diesen Wert jedes Mal ändern, wenn der Filter angewendet wird. Bei Actions möchte man den Code aber vielleicht nur einmal ausführen, gerade wenn er Seiteneffekt, wie etwa den Versand einer E-Mail auslösen.

Prüfe, ob eine Action bereits ausgeführt wurde

Mit der Funktion did_action() kann man prüfen, wie häufig eine Action bereits aufgerufen wurde. Wenn ihr euren eigenen Code also nur dann ausführen wollte, wenn die Action zum ersten Mal ausgeführt wird, aber kein zweites Mal, dann könnt ihr das folgende tun:

function do_this_only_once() {
	// If the action we are hooking in was called more than once, stop execution of the custom code.
	if ( did_action( 'the_hook_name' ) > 1 ) {
		return;
	}

	// Run your custom code
}
add_action( 'the_hook_name', 'do_this_only_once' );

Wenn die the_hook_name Action ausgeführt wird, dann wird beim Ausführen von did_action( 'the_hook_name' ) der Wert 1 zurückgegeben, da die Action gerade eben ausgeführt wurde. Daher könnt ihr nicht einfach auf einen boolschen Wert testen, sondern ihr müsst prüfen, ob der Wert größer als Eins ist, um die Ausführung eures Codes zu stoppen.

Verwendet eure eigene Action, um mehrfache Ausführungen eures Codes zu verhindern

Manchmal könnt ihr nicht einfach prüfen, ob die Action zum ersten Mal ausgeführt wird, sondern ihr müsst noch weiter Dinge prüfen. In einem solchen Fall könnte man diese alle in eine Bedingung schreiben. Alternativ könnt ihr hier aber auch eine eigene Action nutzen, die ihr dann für das frühe Abbrechen der Funktion nutzen könnt:

function do_this_only_once( $hook_parameter ) {
	// If the custom code has been run already, stop execution of custom code.
	if ( did_action( 'do_this_only_once' ) ) {
		return;
	}

	// A second check on a hook parameter.
	if ( 'something' !== $hook_parameter ) {
		return;
	}

	// Run your custom code

	// Call our custom action, so we can check, if it has been called already.
	do_action( 'do_this_only_once' );
}
add_action( 'the_hook_name', 'do_this_only_once' );

In diesem Beispiel prüfen wir in der ersten Bedingung auf unsere eigene Action. Dann führen wir eine weitere Bedingung aus, die die weitere Ausführung abbrechen könnte. Nur wenn auch diese Bedingung die Funktion nicht verlässt, wird der eigene Code schließlich ausgeführt. Ganz am Ende der Funktion führen wir dann unsere eigene Action aus. Die erste Bedingung in der Funktion stellt beim nächsten Aufruf der Funktion nicht nur sicher, dass wir den eigenen Code nicht erneut ausführen, es verhindert auch, dass die weiteren Bedingungen nicht erneut geprüft werden müssen.

Fazit

Es gibt mehrere Wege, um zu verhindern, dass euer Code für eine Action nur einmal ausgeführt wird. Dabei empfiehlt es sich in der Regel, dass man hierzu die did_action() Funktion verwendet. Und wenn eurer Code Seiteneffekte hat, wie etwas das Versenden einer E-Mail, dann solltet ihr wirklich sicherstellen, dass er nur genau so oft aufgerufen wird, wie unbedingt notwendig.

Alle Composer-Pakete auf einmal auf die neueste Version aktualisieren

Wenn ihr in PHP programmiert, dann stehen die Chancen sehr hoch, dass ihr Composer zur Verwaltung der Abhängigkeiten verwendet. Irgendwann möchtet ihr die sicher einmal aktualisieren. Die Hauptversion, die ihr beim ersten Mal installiert habt, könnt ihr mit einem einzigen Befehl aktualisieren, allerdings nicht auf die nächste Hauptversion. Wenn ihr also viele Abhängigkeiten in einem alten Projekt habt, und dann testen wollt, ob diese auch in der neusten Version (mit einer aktuellen PHP-Version) funktionieren würden, müsst ihr normalerweise jedes Paket einzeln aktualisieren, indem ihr die Abhängigkeit erneut hinzufügt:

composer require wp-cli/mustangostang-spyc
composer require wp-cli/php-cli-tools
...

Jetzt könntet ihr natürlich einfach alle Paketnamen aus der composer.json Datei kopieren, aber bei vielen Abhängigkeiten ist das recht aufwändig und man vergisst schnell mal einige. Daher habe ich nach einem Weg gesucht, alle Pakete in nur einem Befehl zu aktualisieren.

Alle Pakete auch einmal aktualisieren

Für diesen Blogbeitrag nehme ich das wp-cli/wp-cli Paket als Beispiel. Es hat eine ganz Reihe an Abhängigkeiten und Dev-Abhängigkeiten.

Alle Pakete als Liste erhalten

Im ersten Schritt müssen wir einen Befehl finden, der uns alle Composer-Pakete unseres Projekts als Liste liefert. Hierzu können wir den composer show Befehl verwenden:

$ composer show -s
name     : wp-cli/wp-cli
descrip. : WP-CLI framework
keywords : cli, wordpress
versions : * 2.7.x-dev
type     : library
license  : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage : https://wp-cli.org
source   : []  a5336122dc45533215ece08745aead08af75d781
dist     : []  a5336122dc45533215ece08745aead08af75d781
path     : 
names    : wp-cli/wp-cli

support
issues : https://github.com/wp-cli/wp-cli/issues
source : https://github.com/wp-cli/wp-cli
docs : https://make.wordpress.org/cli/handbook/

autoload
psr-0
WP_CLI\ => php/
classmap
php/class-wp-cli.php, php/class-wp-cli-command.php

requires
php ^5.6 || ^7.0 || ^8.0
ext-curl *
mustache/mustache ^2.14.1
rmccue/requests ^1.8
symfony/finder >2.7
wp-cli/mustangostang-spyc ^0.6.3
wp-cli/php-cli-tools ~0.11.2

requires (dev)
roave/security-advisories dev-latest
wp-cli/db-command ^1.3 || ^2
wp-cli/entity-command ^1.2 || ^2
wp-cli/extension-command ^1.1 || ^2
wp-cli/package-command ^1 || ^2
wp-cli/wp-cli-tests ^3.1.6

suggests
ext-readline Include for a better --prompt implementation
ext-zip Needed to support extraction of ZIP archives when doing downloads or updates

In der Ausgabe gibt es zwei Abschnitte mit „requires“ und „requires (dev)“. Allerdings ist diese Ausgabe nur schwer nach den Namen der Pakete zu parsen. Glücklicherweise können wir die Ausgabe auch als JSON-Objekt erhalten, indem wir einfach das --format Argument anhängen:

$ composer show -s --format=json
{
    "name": "wp-cli/wp-cli",
    "description": "WP-CLI framework",
    "keywords": [
        "cli",
        "wordpress"
    ],
    "type": "library",
    "homepage": "https://wp-cli.org",
    "names": [
        "wp-cli/wp-cli"
    ],
    "versions": [
        "2.7.x-dev"
    ],
    "licenses": [
        {
            "name": "MIT License",
            "osi": "MIT",
            "url": "https://spdx.org/licenses/MIT.html#licenseText"
        }
    ],
    "source": {
        "type": "",
        "url": "",
        "reference": "a5336122dc45533215ece08745aead08af75d781"
    },
    "dist": {
        "type": "",
        "url": "",
        "reference": "a5336122dc45533215ece08745aead08af75d781"
    },
    "suggests": {
        "ext-readline": "Include for a better --prompt implementation",
        "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates"
    },
    "support": {
        "issues": "https://github.com/wp-cli/wp-cli/issues",
        "source": "https://github.com/wp-cli/wp-cli",
        "docs": "https://make.wordpress.org/cli/handbook/"
    },
    "autoload": {
        "psr-0": {
            "WP_CLI\\": "php/"
        },
        "classmap": [
            "php/class-wp-cli.php",
            "php/class-wp-cli-command.php"
        ]
    },
    "requires": {
        "php": "^5.6 || ^7.0 || ^8.0",
        "ext-curl": "*",
        "mustache/mustache": "^2.14.1",
        "rmccue/requests": "^1.8",
        "symfony/finder": ">2.7",
        "wp-cli/mustangostang-spyc": "^0.6.3",
        "wp-cli/php-cli-tools": "~0.11.2"
    },
    "devRequires": {
        "roave/security-advisories": "dev-latest",
        "wp-cli/db-command": "^1.3 || ^2",
        "wp-cli/entity-command": "^1.2 || ^2",
        "wp-cli/extension-command": "^1.1 || ^2",
        "wp-cli/package-command": "^1 || ^2",
        "wp-cli/wp-cli-tests": "^3.1.6"
    }
}

Jetzt müssen wir das JSON noch parsen. Auf meinem Linux-System steht mir hierzu der Befehl jq zur Verfügung, mit dem man eine JSON-Datei oder Ausgabe parsen kann. Ich habe auch eine kleine Übersicht an nützlichen Argumenten gefunden, mit dem es mir dann möglich war, an den requires Schlüssel zu gelangen:

$ composer show -s --format=json | jq '.requires'
{
  "php": "^5.6 || ^7.0 || ^8.0",
  "ext-curl": "*",
  "mustache/mustache": "^2.14.1",
  "rmccue/requests": "^1.8",
  "symfony/finder": ">2.7",
  "wp-cli/mustangostang-spyc": "^0.6.3",
  "wp-cli/php-cli-tools": "~0.11.2"
}

Das ist schon mal toll! Aber wir brauchen lediglich die Namen der Pakete. Daher lesen wir im nächsten Schritt nur die Schlüssel des Objekts aus:

$ composer show -s --format=json | jq '.requires | keys'
[
  "ext-curl",
  "mustache/mustache",
  "php",
  "rmccue/requests",
  "symfony/finder",
  "wp-cli/mustangostang-spyc",
  "wp-cli/php-cli-tools"
]

Um diese Liste nun in einem anderen Befehl verwenden zu können, brauchen wir sie in einer Zeile. Hierzu können wir add verwenden:

$ composer show -s --format=json | jq '.requires | keys | add'
"ext-curlmustache/mustachephprmccue/requestssymfony/finderwp-cli/mustangostang-spycwp-cli/php-cli-tools"

Das ist nicht wirklich, was wir wollen, dann nach jeden Paketnamen sollte ein Leerzeichen kommen. Das erreichen wir mit map:

$ composer show -s --format=json | jq '.requires | keys | map(.+" ") | add'
"ext-curl mustache/mustache php rmccue/requests symfony/finder wp-cli/mustangostang-spyc wp-cli/php-cli-tools "

Jetzt haben wir es fast geschafft. Wir müssen nur noch die Anführungsstriche um den String entfernen, was uns mit dem -r Parameter gelingt:

$ composer show -s --format=json | jq '.requires | keys | map(.+" ") | add' -r
ext-curl mustache/mustache php rmccue/requests symfony/finder wp-cli/mustangostang-spyc wp-cli/php-cli-tools

Damit haben wir es also. Jetzt können wir das Ergebnis in einem Subbefehl verwenden und endlich mit einem einzelnen Befehl alle Abhängigkeiten aktualisieren:

$ composer require $(composer show -s --format=json | jq '.requires | keys | map(.+" ") | add' -r)
Using version * for ext-curl
Info from https://repo.packagist.org: #StandWithUkraine
Using version ^2.14 for mustache/mustache
Using version ^7.4 for php
Using version ^2.0 for rmccue/requests
Using version ^5.4 for symfony/finder
Using version ^0.6.3 for wp-cli/mustangostang-spyc
Using version ^0.11.15 for wp-cli/php-cli-tools
./composer.json has been updated
...

Das war’s! Wenn ihr in eurem Projekt auch Dev-Abhängigkeiten habt, dann müsst ihr noch einen zweiten Befehl ausführen, bei dem ihr dann an den composer Befehl noch das --dev Argument anhängt und nach devRequires statt requires filtert:

$ composer require --dev $(composer show -s --format=json | jq '.devRequires | keys | map(.+" ") | add' -r)
Using version dev-latest for roave/security-advisories
Using version ^2.0 for wp-cli/db-command
Using version ^2.2 for wp-cli/entity-command
Using version ^2.1 for wp-cli/extension-command
Using version ^2.2 for wp-cli/package-command
Using version ^3.1 for wp-cli/wp-cli-tests
./composer.json has been updated
...

Zusammenfassung

Ich hoffe, dass ich euch in diesem Beitrag erklären konnte, wie ihr mit Composer und einem weiteren Befehl eine solche Aufgabe in einem einzelnen Befehl erledigen könnt. Das hier sind noch einmal die zwei Befehle, die ihr vermutlich benötigt:

Für Abhängigkeiten:

composer require $(composer show -s --format=json | jq '.requires | keys | map(.+" ") | add' -r)

Für Dev-Abhängigkeiten:

composer require --dev $(composer show -s --format=json | jq '.devRequires | keys | map(.+" ") | add' -r)

Ich mag die Möglichkeiten von Kommandozeilen-Tools wirklich sehr, aber es ist manchmal nicht ganz leicht, solche Einzeiler zu finden. Nachdem ich einige Jahre nach einer Lösung für genau dieses Problem gesucht hatte, habe ich mir dann doch endlich mal die Zeit genommen, um eine Lösung zu finden, und zum Glück war ich erfolgreich dabei.

Arbeiten und Reisen – oder wieso Programmieren im Zug eine Herausforderung sein kann

Heute möchte ich mal eine etwas andere Geschichte teilen. Wenn ich in Deutschland reise, dann nehme ich fast immer den Zug. Falls manche von euch schon einmal Berlin Richtung Westen verlassen haben, dann werdet ihr vermutlich auch wissen, dass die Internetverbindung extrem schlecht wird, sobald man Berlin verlassen hat, egal ob über das WLAN im ICE oder euren mobilen Hostpot. Aber wenn ihr Linux verwendet und auf dem Laptop auch entwickelt, dann seid ihr vielleicht gar nicht in der Lage, das Internet über WLAN zu verwenden.

WIFIonICE

Wenn ihr im ICE unterwegs seid, dann gibt es selbst in der 2. Klasse kostenloses WLAN. Es ist ausreichend stark, um die meisten Dinge zu erledigen. Selbst Streaming und Videotelefonate sind möglich, wenn ihr diese wirklich machen müsst. Aber bitte nicht im Ruhebereich! Für das Streaming würde ich euch aber das iceportal.de empfehlen, wo ihr einige Filme und Serien ansehen könnt, ähnlich wie im Flugzeug.

Das klingt alles nicht schlecht, oder? Normalerweise müsst ihr euch dazu nur zu WIFIonICE oder WIFI@DB verbinden und dann öffnet sich euer Browser und fordert euch dazu auf, die AGB zu akzeptieren. Passiert das nicht, könnt ihr einen Browser und auf LogIn.WIFIonICE.de navigieren, wo ihr das dann tun könnt. Ihr habt das getan, und nichts passiert? Dann können euch vielleicht die FAQ auf der DB Seite weiter helfen. Aber ihr würdet diesen Beitrag vermutlich nicht lesen, wenn das funktioniert hätte, richtig?

OK, lasst mich raten: Ihr verwendet sehr wahrscheinlich einen Linux-Laptop, aber auf jeden Fall setzt ihr bei der lokalen Entwicklung Docker ein. Richtig? Dann willkommen zu meinem etwas anderen Thema für einen Blogbeitrag ?

Docker-Netzwerke und WIFIonICE

Das Problem ist eines, zu dem ihr leider auf der DB Seite keine Hilfe finden werdet. Es ist auch eines, was ich jetzt schon mehr als einmal hatte und was dazu führte, dass ich die Seite mit den AGB nicht sehen konnte. Es war überhaupt nicht möglich, irgendeine Seite zu öffnen. Nichts passierte im Browser. Der Grund dafür ist der folgende: im ICE wird der IP-Adressbereich 172.17.0.0/16 für das WLAN verwendet. Und jetzt ratet mal, welchen IP-Adressbereich Docker standardmäßig für seine Netzwerke verwendet? Ihr seid ja richtig clever ?

Lösung des Problems

Um euch wieder mit dem WLAN verbinden zu können, müsst ihr das Netzwerk, das die IP-Adresse 172.17.0.1 verwendet entfernen. Das könnt ihr mit den Betriebssystem-Werkzeugen machen (etwa mit ip link delete) oder aber ihr verwendet den docker network rm Befehl. Es würde das Problem lösen, aber es würden eventuell einige Dinge nicht mehr funktionieren, da Docker dieses Netzwerk ja vermutlich braucht. Es ist auch meisten das primäre Bridge-Netzwerk. Und wenn ihr euch in ein paar Monaten erneut in einen ICE setzt, dann ist die IP-Adresse vielleicht schon wieder von einem neuen Docker-Netzwerk belegt. Daher brauchte ich eine bessere Lösung.

Ändern des IP-Adressbereichs

Nach einiger Recherche konnte ich eine Dokumentation zu einer optionalen Konfigurationsdatei finden, mit der ihr einige Variablen für den Docker-Daemon auf eurem System setzen könnt. Auf dieser Seite gibt es auch ein komplettes Beispiel für eine Linux-Konfiguration. Hiervon braucht ihr nur einen kleinen Teil. Zuerst öffnet (oder erstellt) ihr eine Konfigurationsdatei:

sudo vim /etc/docker/daemon.json

Nun fügt ihr (mindestens) die folgenden Zeilen ein und speichert die Datei:

{
  "default-address-pools": [
    {
      "base": "172.30.0.0/16",
      "size": 24
    },
    {
      "base": "172.31.0.0/16",
      "size": 24
    }
  ]
}

Ich habe hier zwei alternative IP-Adressbereiche definiert. Vermutlich würde sogar einer ausreichen. Nach Änderungen an dieser Datei müsst ihr den Docker-Daemon noch neu startet. Für mich war das mit diesem Befehl möglich (unter Manjaro Linux):

systemctl restart docker

Jetzt solltet ihr in der Lage sein, die Seite mit den AGB zu öffnen und endlich mit einer produktiven Coding-Session im Zug anzufangen … oder aber einen Film oder eine Serie aus der Mediathek des ICE genießen ?

Fazit

Netzwerkprobleme und Probleme mit (öffentlichen) Hotspots kennen wir wohl alle und wir können viele Geschichten darüber erzählen. Aber ich hätte nie gedacht, dass der Einsatz von Docker zu so einem Fehler führen könnte, der anscheinend der DB nicht bekannt ist. Zumindest findet man hierzu nichts in den FAQ zu diesem Problem und ich bin sicher nicht der einzige, der es schon hatte. Wenn euch dieser Blogbeitrag also helfen konnte und ihr nun endlich online seid, dann könnt ihr doch sicher auch kurz einen Kommentar schreiben, oder nicht?☺️

Hinzufügen eines formularspezifischen Gravity Forms Hook zu deinem Code

Wer meinem Blog regelmäßig liest, wird sicher festgestellt haben, dass ich in vielen Projekten Gravity Forms verwende. Ich mag die große Vielfalt an Funktionen des Plugins (und seiner Erweiterungen) sehr, aber vielleicht noch sehr viel mehr die Möglichkeit, viele Dinge anzupassen. Ich habe auch dabei geholfen eine Erweiterung zu schreiben, mit er es möglich ist, alle Dateien eines Eintrags (oder mehrerer Einträge) auf einmal herunterzuladen. Diese Woche haben wir eine neue Version veröffentlicht, die eine kleine aber wichtige Neuerung enthält: die Möglichkeit, Hooks nur für bestimmte Formular zu verwenden.

Hooks in Gravity Forms verwenden

Wenn man Gravity Forms Funktionalitäten anpassen möchte, dann funktioniert das genau wie im Core oder in Plugins/Themes. Ihr verwendet die Funktionen add_action() oder add_filter() für euren eigenen Code. Und Gravity Forms hat sehr viele Actions und Filter.

Sehen wir uns als Beispiel die gform_pre_submission Action an. Ihr könnt diese verwenden, um einen Eintrag zu verändern, bevor Benachrichtigungen versendet wurden und vor der Speicherung des Eintrags in der Datenbank. Ein Anwendungsfall könnte wie folgt aussehen:

function my_gform_pre_submission( $form ) {
    $_POST['input_1'] = strtolower( $_POST['input_1'] );
}
add_action( 'gform_pre_submission', 'my_gform_pre_submission' );

Dies würde den Wert vom Formularfeld mit der ID 1 (input_1) nehmen und den Textwert in Kleinbuchstaben ändern. Dies würde allerdings für jedes einzelne Formular angewendet werden. Wenn ihr es nur für ein Formular anwenden wollt, dann müsst ihr es für die Action so schreiben, dass sie nur für dieses eine Formular greift. Glücklicherweise ist bei Gravity Forms genau das möglich, denn ihr könnt Hook-Namen dynamisch verwenden. Ersetzt hierzu den add_action Aufruf wie folgt:

add_action( 'gform_pre_submission_5', 'my_gform_pre_submission' );

Dieser Action-Name hat einen Suffix _5 und wird daher nur für das Formular mit der ID 5 aufgerufen. Ich hatte das bereits im Blogbeitrag Dynamische Formular-Hooks für GravityForms Anfang des Jahres erklärt und dabei auch beschrieben, wie man mit diesen statischen Werten für die IDs der Formulare in den Hook-Namen umgehen kann.

Nachdem wir also in dieser kleinen Einführung noch einmal die Grundlagen wiederholt habe, können wir in das eigentliche Thema einsteigen und uns ansehen, wie ihr Hooks nur für bestimmte Formulare in eurem eigenen Code verwendet.

Hooks in eurem Code anbieten

Wenn ihr eine Veränderung von Teilen eures Codes erlauben möchtet, dann müsst ihr dazu eine diese beiden Funktionen verwenden:

  1. apply_filters()
  2. do_action()

Ihr könnt euch sicherlich schon denken, welche Funktion hier jeweils für welche Art von Hook verwendet wird. Der Unterschied besteht darin, dass ein Filter einen Wert zurückgibt, eine Action aber nicht. Wenn ihr also einen Filter verwenden möchtet, dann sieht es in etwa wie folgt aus:

$value = apply_filters( 'filter_name', $value, $arg1, $arg2 );

Ihr definiert also einen Namen für den Filter und übergebt die Variable, die verändert werden soll. Optional könnt ihr weitere Argumente übergeben, mit deren Hilfe ihr dann innerhalb der Callback-Funktion entschieden könnt, wie ihr den Wert verändert.

Für eine Action sieht es ziemlich ähnlich aus. Ihr speichert aber keinen Rückgabewert, da die Funktion auch nichts zurückgibt:

do_action( 'action_name', $arg1, $arg2 );

Und weil die Funktion keinen Wert verändert, müsst ihr auch nicht zwingend eine Variable übergeben, es sind also alle Argumente optional. Viele Actions bekommen gar keinen Wert übergeben, wie etwa die wp_head Action.

Den Hook formularspezifischen machen

Nachdem wir nun also gelernt haben, wie man Hooks zum eigenen Code hinzufügt, sehen wir uns jetzt an, wie man diese nur für bestimmte Formular ausführen lässt. In der offiziellen Dokumentation von Gravity Forms findet sich nur eine Seite zu Actions, aber leider ohne viel Erklärung. Die Veränderungen, die ihr machen müsst, sind aber für Actions und Filter identisch. Alles, was ihr tun müsst, ist die Funktionen mit einem gf_ Präfix zu versehen und als ersten Parameter ein Array mit dem Hook-Namen und der Formular-ID zu übergeben. Für einen Filter sieht es dann wie folgt aus:

$value = gf_apply_filters( array( 'filter_name' $form_id ), $value, $arg1, $arg2 );

In diesem Beispiel gehen wir davon aus, dass die Formular-ID in der Variablen $form_id gespeichert ist, sie könnte aber auch in $form['id'], $entry['form_id'] oder etwas Ähnlichem enthalten sein. Ihr müsst also nur sicherstellen, dass ihr sie an die Funktion übergebt. Für eine Action sieht der Funktionsaufruf dann wie folgt aus:

gf_do_action( array( 'action_name' $form_id ), $arg1, $arg2 )

Das war auch schon alles, was ihr tun müsst!

Fazit

Wenn ihr eine Erweiterung, ein Plugin oder eigenen Code für Gravity Forms schreibt, ist es immer gut, wenn ihr euch überlegt, an welchen Stellen jemand eventuell euren Code anpassen möchte. Hierfür dann Hooks zur Verfügung zu stellen, kann euren Code sehr viel besser nutzbar machen. Und wenn ihr diese Hooks hinzufügt, dann verwendet dabei immer die Gravity Forms Funktionen, damit sie spezifisch für individuelle Formulare verwendet werden können.