Geo coordinates to pixel positions – build a map with your own images

This blog post will be a start of a little post series. For a project, I recently had to add markers of cities to a map of Germany. I did that manually in the past, but as the maps has grown to 100 markers, I was looking for a way to do that programmatically.

Map projections

Even though some people believe the earth is flat, the earth is round. But it is also not perfectly round. As our monitors are flat (and before that paper maps), there are man different “map projections“. One of the most common ones is the Mercator projection which is used by Google Maps, Bing Maps, OpenStreetMap and many other services. It’s also referred to by identifier EPSG:3857, which we will need a little later.

GPS coordinates

When giving the position of a place on earth, you usually use the World Geodetic System 1984 (WGS 84) with the latitude and longitude values for a specific place. For the Brandenburg Gate in Berlin this might be around 52.5162746 (latitude) and 13.3777041 (longitude). You might have seen coordinates like this before. You might also have seen a location in degrees, minutes and seconds like 52° 30' 58.59" (latitude) and 13° 22' 39.7338" (longitude). But as it’s easier to make calculations with numbers, the first format is easier to continue with.

Getting SVG maps

So now that we have talked a bit about the theory, let’s talk about code. First, you need to find a map. This can be anything, an aerial photo, a satellite image or an image. I like to use SVG images. You can find them on different platforms. Wikipedia is usually a good source, and maps are usually released using a Creative Commons license. We can find a nice map of Germany with borders to all states. Unfortunately, it uses a different projection system. That’s why, for this blog post, I have chosen the free SVG map from amCharts:

Map of Germany
© ammap.com | SVG map of Germany (low detail), code optimized by Bernhard Kau, CC BY-NC 4.0

There are also commercial services and special stock shops for vector maps. Which ever map you use, please make sure to respect the license.

Geocoding markers

We have a map, let’s start to add some markers. As you probably only know the GPS coordinates, we need a way to convert them to “x/y pixel coordinates” on the SVG map. After some research I’ve found the PROJ library and fortunately there is also a PHP variant of PROJ. We can install it using composer:

composer require proj4php/proj4php

Now we have to do some initialize. We have are converting between two projection systems which we have to set up as follow:

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 );

The EPSG:4326 projection system, our source projection, is the WGS 84 system used for GPS coordinates. The EPSG:3857 on the other hand is a “Spherical Mercator” projection system mentioned above and also for the SVG file we are using here. So the first one is defining coordinates on a “globe” (spherical map) while the second one is defining coordinates on a flat map (in one of the many projection systems).

Now that we have this setup, let’s do the first conversion. We take the GPS coordinates of Berlin, Germany and convert them to the Mercator coordinates:

$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
 * )
 */

All right, we have some new numbers now. But how would you use them now? If we want to put them on the SVG map, we have to calculate the pixel coordinates on that image.

Getting the image boundaries

Now we need to get the min and max coordinates on the map. If we are lucky, we would get the coordinates of these points from the provider of the map. Unfortunately for the map from Wikipedia there are no such coordinates given. You can probably find those boundaries on with a search engine or use an open API like the one from OpenStreetMaps and other services. But if you don’t have such data or don’t know where to find them, you can just try to get a GPS coordinate for the points on the edges. So for the map of Germany, we would need to find the places all the way in the North, East, South and West. With these points, we then use the min/max values for the latitude and longitude. Fortunately for the amCharts map, they have it in the source code of the SVG itself:

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

With these, we can just use two reference points (that are not within the country limits of Germany, but that’s not an issue) and do our calculation:

$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)
 * }
 */

With the two “swap” flags, you can indicate that the coordinates are on opposite sites of 0° latitude or 0° longitude, so the maximum value would be used for the left boundary for example and the minimum for the right boundary. For Germany, we have to set $swap_y to true.

Now that we have the boundaries, we can calculate the coordinates on the image. We would also need the size of the image. Then the calculation would be like this:

$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)
 * }
 */

First, we define the boundaries of our SVG map of Germany. Then we use the $dst_berlin point we have transformed before and put it into our calculation. By using $src_boundaries['xmax'] - $src_boundaries['xmin'] we get the “width” of the source projection boundaries (same for the image boundaries). We then subtract the left boundary from the longitude of the point. We divide this point by the width of the source projection and multiply it with the width of the destination projection. This will give us the pixel position on the image. We do the same for the y-axis. To visualize the point, we could simply draw a SVG circle. This would then look like this in code:

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

We then add this new node to the original SVG map and get the following:

Map of Germany with a circle on Berlin
© ammap.com | SVG map of Germany (low detail), code optimized and modified by Bernhard Kau, CC BY-NC 4.0

Putting it all together

In order to make this all a little easier, I’ve written a small PixelGeocoder class with the initialization steps, the helper function for the boundaries and a method to do get the coordinates for the image:

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'] ),
		];
	}
}

Now if you want to use this class, this is:

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: use a clickable marker

The circle is easy and nice, but if you want to build a map, you probably want to use a marker, and it should be clickable. Since we are using an SVG image here, we can use a path for the marker and then move it around with the transform="translate(x,y)" attribute:

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

When added to our map, the result would look like this:

Map of Germany with a clickable marker on Berlin
© ammap.com | SVG map of Germany (low detail), code optimized and modified by Bernhard Kau, CC BY-NC 4.0

Conclusion

We all love map, don’t we? But sometimes a Google Maps or OpenStreetMap version just doesn’t look nice. With some custom code and a little set-up work, we can create beautiful maps on top of (our own) images. I did the above for a map of Germany that would get the GPS coordinates from a WordPress custom post type and then dynamically created the SVG markup for the city markers in a shortcode (and later server-side-rendered block).

If you want to test the code yourself, you can find it all on GitHub. It is a “proof of concept” and works, but the PHP class can probably use a better architecture and maybe some more methods, so please feel free to adjust it to your need.

In my next blog post, I will show you how you use a picture instead of an SVG map, so stay tuned, the year is not over, yet! ?

Fatal errors on WordPress with PHP 8+ and an incorrect translation

Last week, I got a report about a broken site, that caught me a bit by surprise. I’ll recreate the issue with a dummy plugin here, so show what went wrong. When navigating to the page with the error, this message was presented to me:

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

I looked at the code and saw something like:

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

Nothing fancy so far. Just a format string with a placeholder for a string, that gets replaced with the post date in the WordPress date format.

Incorrect translation

As I couldn’t see any issue here, I was first a bit confused, what the issue would be. But since the website language was not set to “English (US)”, I’ve checked the translation file. Again, here is an example of a translation with a similar issue:

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

This German translation is translating the “Publish date:” part, but as the placeholder, it is only using a % instead of a %s and this causes the issue. WordPress would translate the original string to German and then pass it to the printf() function, which then exists with a “Fatal error”.

Different error handing with PHP 8+

When you run this code with PHP 7.4 and earlier, you don’t get a fatal error. You don’t even get a PHP notice or warning. It would just not replace the placeholder correctly. The % would just be replaced with an empty string. But as soon as you upgrade to PHP 8+ you will have a broken site.

This was one of the first real issues I have recognized with PHP 8+ on a WordPress site. Maybe I was just lucky, or translation always had all the placeholders translated correctly.

Conclusion

I’ve done some checks for PHP 8+ compatibility with the PHPCompatibilityWP and so far it never failed me. But I would never have imagined, that an incorrect translation would cause a fatal error. When translations are made with GlotPress (used on translate.wordpress.org), you will see a warning, that the translation is missing a placeholder. But tools like Poedit don’t show such a warning. So when you have someone translating a plugin/theme into a language you don’t speak, better make sure that format strings are correct.

The “text-underline-offset” and other lesser known CSS properties for links

Working for a larger agency has many benefits. One of them is that you work with people, that know a lot more about many things then you do. I usually read commits from my colleagues to see how they do things. In one commit, I saw a CSS property I have never seen before: text-underline-offset ?

Styling links

In the early days of the World Wide Web, all links were underlined. With the rise of CSS and more modern designs, people wanted to have different styles for links. As the color and position of the text-decoration: underline was dictated by the used font and its color, it was often replaced with a border-bottom. This whoever caused many different issues, including in many cases a decreased accessibility of the links. With modern CSS there are now many different CSS properties you can use to style links … and which I have never heard of:

On the linked MDN Web Docs pages, you can try many of these properties yourself. I could probably find many very use-case for those properties and show you how they would look like, but I don’t want to spoiler it for you ?

Conclusion

When developing websites, one thing is always true: you can’t know everything. This is why I can highly recommend reading code from other people and get expired from their works. While these CSS properties might be already familiar to you, they totally blew my mind!

Create a dynamic iCalendar with blog posts

A small local website used a booking calendar plugin. Those bookings were then copied manually to an online calendar, where the team would have an overview on the bookings. Unfortunately, the booking calendar plugin didn’t have any feature to dynamically show the booking on a calendar app. So I was asked if I could help here. To make this a little more useful for some of you, instead of creating a calendar with entries for this specific booking calendar plugin, I’ll show you how to use the same approach to display all your published and scheduled blog posts.

Installing the dependency

We are going to create a dynamic .ical file. This is a text file, and we could just create the “code” ourselves, we are going to use a library to help us with that task. For the project, I have used the spatie/icalendar-generator library, which has everything I needed (and a lot more). We install it using composer into our plugin folder:

composer require spatie/icalendar-generator

We then have to load the necessary files. The easiest way to do this with packages installed with composer is using the composer autoloader, which we just have to require in our main plugin PHP file:

require_once 'vendor/autoload.php';

Creating a calendar

Now we can start to dynamically create our calendar. I will use the most basic code examples here, but in the documentation of the package you can find more complex examples. So let’s create the $calendar object first:

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

The parameter of the create() function is used as the title, but you can also leave it empty. Once the $calendar object is created, we would query our data for the events. Here we just query for the lasted published and scheduled blog posts:

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

$posts = get_posts( $query_args );

Now that we have our blog posts, we can create individual calendar events for all of them:

// 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 );
}

We are using the GMT/UTC times, so the calendar events can adapt to your local time zone. For the “end date”, we just add 15 minutes. By using the post ID as the unique identifier, we make it easier for calendar applications to update/synchronize the event. Finally, we add the event to the previously created $calendar object.

The last step if sending the output. This can be done with the following lines:

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

Import the iCalendar into your calendar app

Now that we can dynamically create the calendar, we probably want to import (and sync) it into a calendar app. For this, we would need to have a URL we can put into the app. I’ve decided to use a custom REST endpoint, so I’ve wrapped everything in a callback function and registered the endpoint like this:

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' );

The calendar can then be accessed using this URL: https://example.com/wp-json/blog-posts-calendar/v1/ical.ics

The permission_callback would allow anyone to subscribe to this calendar. If you want to restrict it, you can implement our own login in your own callback here.

Conclusion

Even if a plugin does not offer some features you might need, you can often write some own custom code to make this available. This is usually a lot easier than replacing the whole plugin, which then might lack some other features. In the case of a booking plugin, it would probably also be quite challenging to migrate all the bookings to another plugin.

If you want to test this code on your website, you can find it as a plugin on GitHub.

Use different Git settings for personal and company projects

If you are working for an agency or for your own clients, but you also work on personal projects, you might want to use different Git settings for these two types of projects. When setting up your machine to work on projects, you configure some basic tools like Git and GitHub. Those configuration settings do include your Git author email address and maybe the associated SSH/GPG key to sign commits. You can normally only define one global author email address, so you might just use your professional email address (from your employer or your own freelancer address). If you also work on personal projects or contribute to open source projects, you might prefer to use your personal GitHub email address instead. This guide will help you to set this up.

Preparation

In this guide, we assume that you usually work with PhpStorm and store all projects you are working on in the ~/PhpstormProjects folder. But the same approach would also work with any other IDE and folder structure.

In order to be able to use two different email addresses with GitHub, you have to verify them first. Then you can select the best one any time you execute something on github.com, like merging a PR. Having those email addresses verified is also essential to be able to sign commits with these addresses.

Splitting up your configuration files and conditionally load them

Usually, you have all your global Git configurations in the file ~/.gitconfig. The file could look like this:

[user]
	name = Jo Doe
	email = [email protected]
	signingkey = ~/.ssh/id_ed25519.pub

Now, in any project you make commits, you would your company.com email address and sign it for this address. If you’d want to use a different email address, you would have to add this manually when doing the commit:

git commit -m"message" --author="Jo Doe <[email protected]>" --gpg-sign=~/.ssh/id_ed25519.pub

This is not really practical, and you would probably forget to add those arguments sometimes. Alternatively, you could add the author settings to the project-specific configuration file, but you would also have to do that for every single cloned repository.

While it would also be technically possible to change the author afterwards, it’s far from easy and requires an interactive rebase which rewrites the history and must not be done with pushed commits. So how can it be achieved differently?

Load configuration files conditionally

Within a Git configuration file, you can use includeIf to load the file conditionally. The easiest way to do that is by having different folders for your projects. We would store personal and open source projects in ~/PhpstormProjects and company projects in the sub-folder ~/PhpstormProjects/company. We can then add the following to our global Git configuration:

# file: ~/.gitconfig

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

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

Then we would move all specific settings into these files. This would probably be the whole [user] settings as well as some other ones. The general file might look like this:

# file: ~/.gitconfig-general

[user]
	name = Jo Doe
	email = [email protected]
	signingkey = ~/.ssh/id_ed25519.pub

In this code snippet, I am using the GitHub “noreply” email address you can set up instead of your real personal email address to keep it private and prevent it from being used to send you spam.

Now, for the company sub-folder, we use this separate configuration file.

# file: ~/.gitconfig-company

[user]
	name = Jo Doe
	email = [email protected]
	signingkey = ~/.ssh/id_ed25519_company.pub

Those two files are stored in your home directory alongside the global .gitconfig file. If you want to overwrite more settings conditionally, just add them to these files.

On the Git documentation about includes, you can even learn how to conditionally load files due to different things than the gitdir. You could for example also load config files based on the remote URL, so even if you clone a company repository to a folder outside ~/PhpstormProjects/company/ it would conditionally load the .gitconfig-company file. But it’s probably easier to understand which configuration is loaded based on the parent folder.

Bonus: Managing GitHub notifications for multiple email addresses

When you get invited to the github.com organization of your agency, you are usually automatically subscribed to notifications for all repositories. As you have probably used GitHub only for private projects before, you will receive all these notifications to your private email address. Fortunately, you can route emails for notifications based on the organization.

To define those routing rules, navigate to “Settings | Notifications”. Here you click on the “Custom Routing” Button and then on the “Add new route” button. Then pick the organization, select the email address and click “Save”. If you have been invited to multiple organizations, you can add multiple rules.

If you are using gitlab.com for your company/personal work, you can also set a notification email per group or even projects in your profile in the “User Settings | Notifications” setting.

Conclusion

Working on company and private projects at the same time on one device can really mess up the Git author data in your commits. But with the conditional loading of configuration files, you can use settings specifically for different types of projects.

Run your custom code only once in an action

WordPress offers a lot of hooks you can use to interact with code. Filters are used to change a value of a passed variable, and you usually want to change this value every time this filter is applied. But for actions you might want to run them only once, especially when thy run side effects like sending an email.

Check, if an action has been triggered

With the function did_action() you can check how many times an action has already been called. If you want to run your custom code only when the action is run the first time and then not a second time, you can do the following:

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

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

When the the_hook_name action is being called and then running did_action( 'the_hook_name' ), the return value will be 1, as the action has just been triggered. Therefore, you can’t simply use its return value as a boolean, but you have to check if the return value is greater than one, to stop the execution of your custom code.

Use your own action to avoid multiple runs of your code

Sometimes you cannot simply break check if the action runs for the first time, but you have to check for additional things. You can add them all to the one condition. Alternatively, you can use your own action, which then you use in the early return condition:

function do_this_only_once( $hook_parameter ) {
	// If the custom code has been run already, return.
	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 this code example, we use a custom action in our first condition. Then we perform some other checks, to prevent execution of our custom code, unless we really want to run it. Then we finally run our code. At the very end of the function, we run our custom action, so we can use it on the next call to prevent a second execution. This early return makes sure we not only run the code once, but also that we don’t run the additional conditions again.

Conclusion

There are different ways to make sure to run an action only once. It’s usually best to just use the did_action() function which will tell you how many times an action was run. And if you have some side effects in your code, like sending an email, you really should make sure you don’t run them more times than necessary.

Update all composer dependencies to their latest versions

If you code in PHP, changes are very high that you are using composer to manage your dependencies. At one point, you will add a dependency. The version you’ve first installed can be updated with a single command, but it won’t update to a new major version. If you have a lot of dependencies in an old project, and you want to try, if all the newest versions of them would work (with a current PHP version), you would usually have to update every package separately, by adding it as a dependency again:

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

Now you could just copy/paste every package name from your composer.json file, but for many dependencies, that’s quite a task, and you might miss some. So I was searching for a way to do it in just one command.

Updating all packages at once

For this blog post, I will use the wp-cli/wp-cli package as an example. It has a number of required and dev-required dependencies.

Getting all packages as a list

The first step is to find a command that gives us a list of all composer packages used in the project. We can use the composer show command for this:

$ 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

Here you’ll find sections with the “requires” and “requires (dev)”. But this output is rather hard to parse for the names only. Fortunately, you can get the output as a JSON object as well, by adding the --format argument:

$ 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"
    }
}

Now we need to parse the JSON. On my Linux system, I had the jq command available, which allows you to parse a JSON file or output. I’ve found a nice cheat sheet with some useful argument and was able to just the get the requires key:

$ 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"
}

This is great! But we only need the names of the packages, so in the next step, we get the object keys:

$ 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"
]

In order to use it in another command, we usually want the names in a single line, so we use add to achieve this:

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

Not really what we want. We need a space after every package name. We can add it with 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 "

We are almost there, we just need to remove the quotes around the string with the -r argument:

$ 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

And there we have it. Now we can put it into a sub command and finally update all required dependencies at once:

$ 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
...

That it! If your project also have dev requirements, you’ll need to run a second command updating them as well by adding --dev to the composer command and using devRequires instead of requires in the filter:

$ 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
...

Summary

I hope this blog post explained, how you can use composer and another command to get a task like this done in a single command. These are the two commands again you will probably need:

For required packages:

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

For development packages:

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

I really like the power of command line tools, but finding a one-liner is sometimes a bit more tricky. After searching for a solution to this very problem, I finally took the time to come up with something, and fortunately I’ve found a solution.

Work and travel – or how coding on a train can be a challenge

Today I want to share a different type of story. When I travel in Germany, I usually always take the train. If some of you ever travelled from Berlin westbound will probably know, that as soon as you leave Berlin, the internet connection gets really bad, no matter if you use the wifi on board an ICE or your mobile as a hotspot. But those of you using Linux and being developer might not even be able to connect to the internet using the wifi at all.

WIFIonICE

When you take an ICE high speed train, even in the 2nd class you will be able to use free wifi. It’s decent enough for most things, even streaming and video calls, if you really need to do them. But please not in a quite zone! For streaming, at iceportal.de you will even find some movies and series to watch, just like in a plane.

That sound nice, right? But you might not be able to use it at all. Usually, you would connect to either WIFIonICE or WIFI@DB and your browser will pop up to ask you to accept the terms. If that’s not happening, open a browser and navigate to LogIn.WIFIonICE.de to see that page. You did that and nothing happens? Then the FAQ section on the DB website might help you. But you wouldn’t be here it that worked, right?

OK, let me take a guess: you might be using a Linux laptop, but you are definitely using for local development you are using Docker. Got it? Then welcome to my uncommon blog post to a strange topic ?

Docker networks and WIFIonICE

The issue is one, you will unfortunately not find on the DB website. And it’s also one I ran into and couldn’t find out, why I was not able to see the page to accept the terms. Any request to any page just failed. Nothing happened in the browser. The reason for that is the following: the ICE on board network uses the 172.17.0.0/16 IP range. And not take a guess what Docker is using as the default IP range for its networks! You are a clever one ?

Solving the issue

In order to be able to connect, you have to remove the network attached to the IP address 172.17.0.1 from your Docker networks. This could be done with the OS tools (like ip link delete) or using the docker network rm command. This would fix it, but it might break things within Docker, as the network you have just deleted is probably needed. Usually its the primary bridge network. And when you board an ICE in some months time, the IP address might again be taken by another Docker network. So I needed a more robust solution.

Change the IP range

After some research, I’ve found the documentation of an optional configuration file you can have on your system to set some variables for the Docker daemon. This page also contains a full example for a Linux configuration. From this, we would only need a small part. First, you open up the configuration file (or create it):

sudo vim /etc/docker/daemon.json

Now you add (at least) the following lines and save the file:

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

I have defined two alternative IP ranges. One would probably have been enough. After changes on this file, you have to restart the Docker daemon. For me, this command did it (on Manjaro Linux):

systemctl restart docker

Now you should be able to open up the page to accept the terms and finally start some productive coding session on the train … or enjoy a movie from the onboard media library ?

Conclusion

Network issues and issues with (public) hotspots is probably something we all can tell many stories about. But I’d never imagined that using Docker would cause an issue, not even DB is aware of. Or at least they don’t have anything in their FAQ about this specific issue, which probably not only I had more than once now. So is this blog post helped you to get online, why not leave a little comment? ☺️

Adding form specific Gravity Form hooks to your code

Those of you who read my blog regularly will probably know, that I use Gravity Forms in many projects. I really like the huge feature set of the plugin (and it’s add-ons), but even more than that, the possibilities to customize things. I’ve also helped to build such an extension, which allows you to bulk download all files of one (or many) entries. This week we have released a new version introducing a small but important feature: the possibility to have form specific hooks.

Hooking into Gravity Forms

When customizing Gravity Forms functionalities, it works just as with Core and plugins/themes. You would use add_action() or add_filter() for your custom code. And Gravity Forms has a lot of actions and filters!

Let’s take the gform_pre_submission action as an example. You can use this action to change the entry before notifications are sent, and before it is saved to the database. A use case might look like this:

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

This would take the value of the form field with ID 1 (input_1) and change the text to lower case. This however would do it for every single form you have created. If you just want to do it for one form, you have to use the action specifically for this form. Fortunately, that’s possible by using the dynamic form specific hook names. Just replace the add_action call with this line:

add_action( 'gform_pre_submission_5', 'my_gform_pre_submission' );

This action name has a _5 suffix and will therefore only be called for the form with the ID 5. I’ve already covered this in the blog post Dynamic form specific hooks for GravityForms earlier this year, where I’ve also explained on how to deal with those static form ID values in the hook names.

Now that we have repeated this little introduction, let’s dive into adding such a form specific hook into your own code.

Providing hooks in your custom code

When you want to allow parts of your code being changed by hooks, you have to use one of these two functions:

  1. apply_filters()
  2. do_action()

You can probably guess which one to use for which type of hook. The difference is, that filters return a value while actions do not. So when using a filter, it looks like this:

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

You define the filter name, then you pass the value to be filtered. You can also optionally pass more arguments to the filter, which you can then use in your callback function, to decide on how to change the value.

For an action, it looks pretty much the same. But you won’t save the return value, as the function does not return anything:

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

As an action does not change a value, it does not necessarily need a value being passed, so all arguments are optional. Many actions take no values at all, like wp_head for example.

Make hooks form specific

Now that we know how we can add hooks to our code, let’s see how we can make them form specific. In the official Gravity Forms documentation, you only find a page for actions, but without too much explanation. But the modifications you have to do are the same for actions and filters. All you have to do is prefixing the two function with gf_ and passing an array with the hook name and the form ID as the first argument. For filters, it looks like this:

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

In this example, we would need to have the ID of the form stored in a variable $form_id, but you might get it from $form['id'], $entry['form_id'] or similarly. You just have to make sure you pass it. For an action, the new function call would look like this:

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

That’s really all you have to do!

Conclusion

When you write add-ons/plugins or custom code for Gravity Forms, it’s always nice to think about parts of your code others might want to change. Providing hooks for those parts can make your code so much more useful. And if you add those hooks, always use the Gravity Forms functions, so the hooks can be targeted specifically for individual forms.

Adding some options to a block

After we have seen in the last block post, how we can use modern JavaScript to write a block, I want to show why we would really want to do that by adding some options to our block. I’ll show you only the changes we have to do.

Creating an “edit” component

As a first step, we remove the edit() function from the index.js file and place it in its own file. The index.js file also have the following content left:

import { registerBlockType } from '@wordpress/blocks';
import './editor.scss';
import './style.scss';
import Edit from './edit';
import metadata from './block.json';

registerBlockType(
	metadata.name,
	{
		edit: Edit
	}
);

We import the new Edit component and assign it to options when registering the block. We also remove the import for the ServerSideRender component, as it’s no longer needed in the index.js file.

Implementing the component

Not that we have our new file imported, let’s add the ServerSideRender component again and all the things we would need for our option. The result would look like this:

import ServerSideRender from '@wordpress/server-side-render';
import { InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, RangeControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';

export default function Edit( props ) {
	const {
		attributes,
		setAttributes,
	} = props;
	const {
		postsToShow,
	} = attributes;

	return (
		<>
			<InspectorControls>
				<PanelBody
					title={ __( 'Settings', 'rpssrb' ) }
				>
					<RangeControl
						label={ __( 'Number of items', 'rpssrb' ) }
						value={ postsToShow }
						onChange={ ( value ) =>
							setAttributes( { postsToShow: value } )
						}
						min="1"
						max="100"
					/>
				</PanelBody>
			</InspectorControls>
			<div { ...useBlockProps() }>
				<Disabled>
					<ServerSideRender
						block="rpssrb/random-posts"
						attributes={ attributes }
					/>
				</Disabled>
			</div>
		</>
	);
}

OK, this is a whole lot more than we had before. But let’s look at each part to understand what’s happening here.

Step 1: Importing components

In our new code, we need some React components. We will get them from different WordPress packages. We import all of them in the lines 2-5.

Step 2: Creating a function and getting the component properties

We create a function Edit and also export it as the default function right away, so it can be used in our index.js file, as we have seen in the first code snippet. The “edit” and “save” function automatically get some properties. We can “extract” those properties we need. The attributes property is an object with the attributes to the specific block. It uses a React state to live update the UI in the block editor every time a value changes. The setAttribute property is a function that is used in combination with attributes to update the state object.

As attributes is an object as well, we can also get some values from it and assign it to separate variables. We will do that with the postsToShow attribute, we will use later.

Step 3: Implementing the inspector controls

Now that we have the attribute we want to change, we can implement the controls to adjust them. The first thing that might look a little weird is that “empty tag” <> at line 12. This is necessary as the function can only have one root tag in its return statement. We basically just wrap the rest of the JSX tags wight this empty tag.

Next, we open the <InspectorControls> component, which holds all the item in the block sidebar. Inside of that, we create a <PanelBody> to have a nice title for our option, and then we add a <RangeControl> component. The result will look like this:

The InspectorControls with a PanelBody and the title "Settings" as well as a RangeControl with the label "Number of item" with a selected value of 5.

Now we have a nice little “range slider” with which we can select the number of posts to show. With the min and max attributes of the component, we define the limits of the control. The initial value will be retrieved from the previously defined postsToShow attribute and on the onChange event, we use the setAttribues function to update the value in the state. If some of these things don’t make sense at first, you’ll get familiar with them the longer you use them.

Step 4: Rendering the block

Now that we have our controls to adjust the number of posts, we need to render them in our new Edit component. We will again use the <ServerSideRender> component, but this time we add the attributes to the tag, so that the component will refresh, every time we change the value of the posts to show.

We also wrap the component inside a <div>, so we can attach all the blog properties, and we also use a <Disabled> components, so we are able to select our block in the block editor (and not opening the permalinks instead).

Step 5: Registering the attribute for the block

Any time you use an attribute for a block, you have to register it. This is done in the block.json file. The new file looks like this:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "rpssrb/random-posts",
	"version": "0.1.0",
	"title": "Random Posts",
	"category": "widgets",
	"icon": "smiley",
	"description": "An example for a ServerSideRender block using minimal ES6 code.",
	"supports": {
		"html": false
	},
	"textdomain": "random-posts-server-side-render-block-es6",
	"attributes": {
		"postsToShow": {
			"type": "number",
			"default": 5
		}
	},
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css"
}

Step 6: Using the options in the PHP callback function

Now that we can set the option and save it, we have to use it in our PHP callback function. But fortunately that is really easy, as it is passed automatically into the function:

function rpssrb_render_callback( $atts ) {
	$args = [
		'post_type'      => 'post',
		'orderby'        => 'rand',
		'posts_per_page' => (int) $atts['postsToShow'],
	];

	// Run the query and render the posts as in the previous blog posts ...
}

This was it! I’d suggest you to look into the different components you can use in the block editor handbook. There are many you may find really useful to build your block options. Just make sure you register those new attributes as well.

Conclusion

I know this was a lot more code than in the previous two blog posts. But once you have the basics set up for the “inspector controls” you can add new controls as you want.

As I’ve explained in the first blog post to this small series, you can also split up files as much as you want. If the inspector controls get really complex, you might want to move them to their own file. But for this example I’ve wanted to keep it simple again and only use the one edit.js file.