WP Google Maps

With 14,000,000 downloads and counting, and almost 500,000 active installations at the time of writing, WP Google Maps (by Code Cabin) is the largest application I’ve been in charge of to date.

For anyone interested to know what I did in my time working on WP Google Maps, I’ve put together this page to illustrate what I brought to this plugin.

Please see my CV for more information about my roles and responsibilities within Code Cabin, this post focuses purely on the engineering and code itself.

Version 6

Whilst version 6 did boast a very impressive feature set, especially compared to it’s competitors, there was a large amount of re-factoring needed to get the code base into a maintainable state before any new features could be worked on.

Monolithic, WET code

Version 6 had a lot of WET code, some blocks were hundreds of lines long. In my first few weeks with Code Cabin this wasn’t immediately evident, linting tools wouldn’t work with the structure as it was, so it wasn’t until this began manifesting itself in bugs that the layout started to become apparent.

For example, the plugin offers a choice of DB (inline JS) marker loading, or XML marker loading. The plugin has marker categorisation, and a radial search. Version 6’s front-end JavaScript was one monolithic file, which looked something like this:

// Function to show markers
function()
{
	if(/* Loading markers from inline JS */)
	{
		foreach(/* JS data */)
		{
			if(/* No search radius, no categories */)
			{
				// Create marker and display
			}
			else if(/* No search radius */)
			{
				if(/* Marker inside selected categories */)
				{
					// Create marker and display
				}
			}
			else if(/* No categories */)
			{
				if(/* Marker inside specified radius */)
				{
					// Create marker and display
				}
			}
			else
			{
				if(/* Marker inside selected categories and marker inside specified radius */)
				{
					// Create marker and display
				}
			}
		}
	}
	else if(/* Loading markers from XML cache file */)
	{
		foreach(/* XML data */)
		{
			if(/* No search radius, no categories */)
			{
				// Create marker and display
			}
			else if(/* No search radius */)
			{
				if(/* Marker inside selected categories */)
				{
					// Create marker and display
				}
			}
			else if(/* No categories */)
			{
				if(/* Marker inside specified radius */)
				{
					// Create marker and display
				}
			}
			else
			{
				if(/* Marker inside selected categories and marker inside specified radius */)
				{
					// Create marker and display
				}
			}
		}
	}
}

This was problematic, because, for example, if a bug emerged in the category filtering code then it would be fixed, then later seem to re-emerge. This was a frequent occurrence at first because fixing a bug in the XML filtering category logic would need to be repeated in the inline JS block, which was easy to overlook for anyone unfamiliar with the code.

WP Google Maps offers a Pro add-on, there was a great deal of code that was repeated from Basic in Pro, where the design had made true extensibility practically impossible, resulting in WET code actually being necessary in some places. The first decision I took as Lead Developer on this plugin was that this needed to be re-arranged so that in the very least, we didn’t have repeated, high maintenance blocks of logic.

Interleaved logic, content and presentation

Another problem with the earlier versions was a lot of mixed logic (PHP/JS), content (HTML) and presentation (CSS).

Mixed PHP and JavaScript

For an experienced developer, escaping and switching between these “in real time” is do-able, however there are other problems that arise from this approach, most notably,

  • Unexpected characters from user input being outputted from PHP could produce malformed HTML, or worse JS syntax errors. For example, the end users could edit raw polygon data if they wished to do so, entering anything outside of the specified format would usually produce a JavaScript error.
  • Fields being serialized in an inconsistent way, for instance, some fields would use stripslashes when saving. Others would not. This would not emerge until a user started adding quotes into that field, and would quickly find they would accumulate and balloon in the database. This lead to hundreds of different calls to stripslashes being added with no apparent pattern.
  • No templating or dynamic handling of settings means that if you want to add a new control to the interface, you will have to create PHP to render the control in the appropriate state,
  • Also because of the above, you would have to write PHP to receive and write the value of the control back into the database explicitly, each time you add a new control.
Explicit handling of control display states

The plugin literally has hundreds of different settings, some with inconsistencies in naming, inconsistencies in the way values are used to represent settings and lack of standardisation in general, so refactoring this particular aspect of the plugin, in a manner which keeps the very substantial user base happy, was an absolutely mammoth task.

Suboptimal filtering

The plugin supports various forms of filtering, for example, filtering by an address and radius, keywords, or categories.

Historically, this filtering would be performed by hand-rolled JavaScript which would take care of the filtering. I took the decision later on to move this onto the database, which does obviously move load onto the server, however the filtering queries are very simple compared to the legacy JavaScript, don’t stall the browser, and are an appropriate use of the database rather than using our own JavaScript which required maintenance and debugging.

Legacy JavaScript category filtering code

Lack of CRUD and other interfaces

Version 6 worked directly on the plugins database tables, with no exceptions. All the code that dealt with reading, serializing and writing data from the client (browsers) to the database and back worked exclusively with non-modular, purely procedural code. Many code points addressed the database directly, with explicit sanitization and escaping (as opposed to prepared statements) and little use of WordPress’ database helper functions.

This became especially problematic because adding code to one aspect of the plugin would often require a developer to add code elsewhere in a seemingly unrelated section of code.

For example, the process of adding a new field to the marker table would require one to add additional code to transmit that data to the browser, but would also require one to add code to the data exporter. Because of a lack of documentation on this, it could be quite easy to make avoidable mistakes here.

The lack of classes, interfaces and generally standardised code made generating meaningful, structured documentation impossible with version 6.

Lack of extensibility

Version 6’s JavaScript engine was built almost entirely around about half a dozen very large functions.

Inconsistently named functions, and polluting the global scope

Such a design leads to lack of extensibility, because it was impossible to customise atomic parts of the plugins functionality without overriding the entire function – which would inevitably lead to users editing code themselves, which would ultimately either lock them out of updates, or cause the overrides to fail as the code base changed.

Version 6’s PHP was built entirely on functions in the global scope, with almost no use of meaningful WordPress hooks.

Inconsistently named functions, and pollution of the global scope

I refactored both the server side and client side code into modules which use the Factory Design method, which not only allowed the plugin to be fully extensible, but would also pave the way for making the plugin and Premium add-on share a great deal of code, leading to a DRYer code base.

Version 7

Mostly leaving the plugins feature set to one side, I’m focusing on the software architecture itself in this post, however I made two very significant changes to my first major release with WP Google Maps.

REST API

I started building a REST API for WP Google Maps in version 7. The initial purpose of this was to facilitate asynchronous, paginated marker listings for our “power users” with a large number of markers.

The client-side REST API module, handles compression, nonces and other tasks

Version 6 would output the HTML for the entire listing directly into the page body, which could cause high output times server side, and high parse and render times client side. This became especially problematic for users with many thousands of markers, as it would lead to browser stalling, or worse, the PHP execution time limit being hit during rendering.

Version 7 deprecated all marker listing code and introduced REST based modules to handle all this functionality.

Base class for the new marker listing classes

This had to be done very carefully, so as not to break JavaScript and CSS associated with the marker listings – both internal code, and snippets we’d given out to the customers over time.

A child of the base marker listing class, representing one of many styles

This was hugely beneficial for the plugin in general, but especially for the marker listings. Having a very large number of markers would no longer stall the browser on parsing HTML, or cause long load times, or even worse – a blank page due to PHP hitting it’s execution time limit.

The REST based marker listing system is asynchronous, features server-side pagination rather than transmitting all items, and extends from base classes which provide functions for searching and sorting the list items in a consistent way across all child classes, where previously this was not the case.

Query builder and server-side filtering

The REST API would later (7.11.*) go on to be used to facilitate marker filtering, with all the JavaScript that previously performed this removed.

The filtering components (store locator, category filter, custom field filters), which are now fully modular and extend from an event dispatcher module, will emit an event when filtering parameters change.

The filtering module will listen for these events, and when the filtering parameters change, a REST request is sent out to the server.

An outgoing filtering request for all markers in category 5

This request is received by the markers REST endpoint, which instantiates a marker filter and passes these parameters to the filter.

The base class for the marker filter

The marker filter uses these parameters to build a query, which is then used to fetch the ID’s of all markers which fit the specified parameters. The ID’s are returned to the browser via REST, which can then show and hide markers based on the returned ID’s.

The response from the above request

The server-side marker filtering massively outperforms the legacy JavaScript, it’s handled by a couple of dozen lines of code rather than hundreds of lines, so it’s more maintainable and easier to work with. It also means that the results are cachable via REST cache plugins or a CDN.

JavaScript modules

Version 7 was the first version to introduce modules for JavaScript. This was mainly done to facilitate OpenLayers support – by creating wrappers instead of using Google’s modules directly, and by using the Factory Method design pattern, I was able to achieve engine independence for our legacy core code.

This meant that the logic in the core code could stay without a total refactor, but wrappers with methods and properties matching Google’s modules could facilitate OpenLayers support through the plugin.

Marker wrapper with method names matching Google, used for OpenLayers support

This also meant that JavaScript snippets we had given out to customers to implement custom behaviour on, for instance, markers, would continue to work seamlessly with our new wrappers.

This also meant that the end user could have their own modules which subclassed our wrappers, which allowed for further, fine tuned and external customisation of many aspects of the plugin.

Factory Method design pattern

DOMDocument HTML handling

Build pipeline and minification

To be continued…

Leave a Reply

Your email address will not be published. Required fields are marked *