Modify WP_Query to use custom fields with custom post types

Most if not all of the WordPress sites that I build these days use Custom Post Types with Custom Fields.

I use the excellent CPT UI and Advanced Custom Fields plugins for these. I highly recommend them.

I’ll often want to change the way that WordPress displays these posts, which can be problematic. An easy way would be to add a new query to the page template, and discard the page’s main query object.

Not much of an issue, but it’s an extra and un-needed hit on the database.

A better way is to hook into that main query, and make your adjustments there.

Of course WordPress provides a hook for this, pre_get_posts, which is trivial to implement.

Ordering a custom post by a custom field

Let’s say we want to create an Event custom post. We’ve looked at Event Manager, an excellent plugin but overkill for our situation.

We use CPT UI to create the new Event CPT, and ensure that Has Archive is TRUE to enable the list of events to display.

Next, we use ACF to create a custom field called Event Date which uses a datepicker field type, and set it to display only on posts of type Event. And from ACF’s documentation: “If you intend to use this field to order your posts by, please leave the format as yymmdd.” This is exactly our situation so we leave the defaults.

So far, so good. We can create new events, set the date and view them on the site (after visiting the Permalinks page, of course).

However, WordPress displays the post in the order of publication, where we want them to display in the order of the Event’s date. Step in pre_get_posts.

Before we write the function, we also remember that this should only work on the site’s front end (i.e. not on the admin screen), and for the page’s main query only.

We then want to order the posts by the event_date from earliest to latest, i.e. Ascending.


function theme_event_cpt_orderby_event_date_custom_field($query)
    if(!is_admin()                                       // don't run on admin pages
       && $query->is_main_query()                        // target the main query only
       && $query->query_vars['post_type'] == 'event'     // run only for the Event post type
        $query->set('meta_key','event_date');            // choose this custom field
        $query->set('orderby', 'meta_value_num');        // order by the custom field specified
        $query->set('order', 'ASC');                     // order Ascending (low to high)
add_action( 'pre_get_posts', 'theme_event_cpt_orderby_event_date_custom_field' );

Voila, our Event archive page now sorts events by the event date, making much more sense for our users.

WordPress provides more methods for even finer grained control. Thes methods and their arguments are detailed on the WP_Query documentation page.

WordPress Plugin for

Last week an email from Matt at BugMuncher dropped in my inbox.

BugMuncher is a tool for website owners to get quality feedback from their users. It places a small tab on the page, and users can send back information and even screengabs.

Matt wanted to make integrating BugMuncher in a WordPress site a simple and easy task.

I provided a simple configuration screen. It asked for the public API key, and users can also customize the text, colours and screen position of the widget. As long as the API key appears and is valid, the widget ‘just works’.

I also uploaded it into the WordPress plugin SVN repository for him.

It’s available now (and free!) on the WordPress plugin directory.

Family Fortunes: Saving and Updating Laravel Relations

Although I’ve been using Laravel heavily for over two years, I still find things there that I didn’t really know about.

Today I took some time to really understand all the different ways of saving and associating related models. I had misunderstood the docs at first, but after some searching and playing with test code I’ve come to a better understanding.

One-To-Many and Belongs-To Relationships

To illustrate I’ll use an example Parent and a Child class. The docs used Post, Comment, User and Roles which threw me off, so I hope that my naming convention is clearer.

class Parent extends Model
  public function children()
    return $this->hasMany(App\Child::class);

class Child extends Model
  public function parent()
    return $this->belongsTo(App\Parent::class);

I always use plurals for hasMany() relationships as a reminder to myself how it goes.

The first example is saving a Child object to an existing Parent:

// Find an existing Parent record
$parent = App\Parent::find(2);

// Creates a new Child instance 
$child = new App\Child(['name' => 'mei']);

// effectively sets $child->parent_id to 2 then saves the instance to the DB

If called repeatedly, the above code will continue to save Child records with the same Parent.

So far so good. We now want to do the reverse of the above.

Let’s say we want a new parent for our child (programmatic adoption?):

// a new Parent object
$parent = App\Parent::find(31);

// updates $child->parent_id to 31

Our Child had parent_id of 2. Above, associate set the parent_id property of $child to 31, then $child->save() persists the change to the database.

Of course, it’s possible to for an object to have more than one parent relation (for example, a Comment can belong to a Post and an Author). As the association is only confirmed when save() is called, we can simply call associate with another parent object.

$parent = App\Parent::find(31);
// a new parent! 
$anotherParent = new App\AnotherParent(['name' => 'steve']);

// associate with our parent, as above

// set the second relationship 

Assuming that foreign keys are set, calling $child->save() before setting the second relationship would throw an error.

Now that the relationship is set, it’s also possible to sever it.

// sever the parent-child relationship

In this example $child->parent_id is set to null. The save() method must be called to persist the change to the database.

Again, if foreign keys have been set an error would be thrown here. This code would only work if the foreign key (parent_id) is nullable.

Many-To-Many Relationships

Let’s move on from Parent and Child.

To illustrate the Many-To_-Many relationship we’ll use Post and Tag. These two are connected with the belongsToMany() method.

class Post extends Model
  public function tags()
    return $this->belongsToMany(AppTag::class);

class Tag extends Model
  public function posts()
    return $this->belongsToMany(AppPost::class);

A Post can have zero or more tags, and a Tag can have zero or more posts (in theory anyway, an unused tag is mostly pointless).

Such a relationship allows us to get all tags attached to a Post, and conversely get all posts attached to a Tag.

Unlike one to many relations, both records must be persisted before the following to work. The pivot table must have both ids.

In this example, a tags are added to a post.

$post = App\Post::find(3);

$tag = App\Tag::find(47);


The first argument of attach() is an id, but an Eloquent model can also be passed:


What happens above is that the pivot table gets a new record, with post_id set to 3 and tag_id to 47.

If your pivot table has extra fields, these can be set by passing an array as the second argument. The parent (in this case Post) model’s timestamp can be touched by passing in true as a third argument:

$post->tags()->attach($tag, ['expires' => date('Y-m-d H:i:s')], true);

To sever the relationship, detach() is used.

A single id or an array can be passed to detach(), or called without any arguments which detaches all of the parent’s relations.

// detaches $tag from $post

// detaches tags with the following ids from $post
$post->tags()->detach([41, 52, 54]);


The first and second methods deletes rows corresponding to the $post with the given tag ids. The last method deletes all rows from the pivot table with that $post’s id.

Finally, the sync() method is provided to make life just a bit easier for us.

Let’s assume we’ve edited the post, and have remove one tag and attached another.

$post = App\Post::find(4);

// $post has tags with ids: 32, 45, 47

// tag 32 is no longer required, and we need to add tag with id 86

// so instead of this:

// we do this
$post->tags()->sync([45, 47, 86]);

Here the new tags [45, 47, 86] will be persisted, and tag 32 will be deleted from the pivot table. This also removes any logic for finding out which tags already exist for the post which are not meant to be removed.


Having spent time playing with this I have a much better appreciation of how it works.

Again, Laravel and Eloquent provide an elegant interface to the database, and lets us write readable code.

I’ve picked up a few points here myself, and I’ll be taking a look back at a few of my own projects to see where I can improve them.

Fixing a WordPress pagination 404 error

I’ve recently had a load of WordPress work.

Much of these jobs are similar, so I’ve had a good chance to try and learn more about better theme and plugin design.

One of these sites had a strange error when paginating the posts page. Permalinks were set to Day and Name, with the category base set to news and blog index set to the news page.

This creates the illusion of a standard site with all news namespaced under /news.

Calling gave the first page of posts, using the index.php template file. Calling gave the latest posts within that category, and pagination worked correctly when calling

But, annoyingly, calling gave a 404 error.

I googled far and wide but could not find an exact match to my problem (although it seems to be common with custom post types).

Changing permalinks back to the default setting worked, so I left it at that so I could dig deeper.

After some googling I found a recommendation to use the Debug This plugin. It gives detailed breakdowns of debugging data on any page or post. I was only interested in the rewrite rules so this is as far as I got with it.

Using Debug This to list the rewrite rules, I could see which one was being matched and what the redirect was doing in the background. It’s done with the Debug This menu item, and selecting Query > Rewrites.

When I called this was the output:

Matched Rule: (.?.+?)(/[0-9]+)?/?$
Matched Query: pagename=news&page=

Calling gave:

Matched Rule: news/(.+?)/?$
Matched Query: category_name=my-category

And gave:

Matched Rule: news/(.+?)/page/?([0-9]{1,})/?$
Matched Query: category_name=my-category&paged=2

Strangely did not match any rules. So the quickest option was to add my own rule to get things going again.

The third matched rule above is close to what I want, just without the category part of the url. So news/page/?([0-9]{1,})/?$ should work OK, provided that it was checked before the other rules.

In functions.php I added the following code:

function mg_news_pagination_rewrite() {
  add_rewrite_rule(get_option('category_base').'/page/?([0-9]{1,})/?$', 'index.php?pagename='.get_option('category_base').'&paged=$matches[1]', 'top');
add_action('init', 'mg_news_pagination_rewrite');

This simply adds a rewrite rule for pagination of the category_base value.

I added top as a third argument to insert the rule at the beginning of the list. It would otherwise match news/(.+?)/?$ (as it did originally) where it would throw a 404 error after failing to find a page category.

I’ve purposely used get_option('category_base') instead of news here too, so you can copy and paste into your own functions.php file.

The Source of the Problem

After writing this post I tried to replicate this on a fresh install, to see if it’s a problem within WordPress or not.

Setting the blog index to a static page, e.g. blog, works fine. Posts are viewed under /blog and pagination works automatically, following the standard blog/page/2 pattern.

The problem arose as I had the category base set to news.

The WordPress rewrite rule list has the category rewrites at the beginning, followed by tags, comments, author, posts by date etc, then finally the wildcards for pages. You can view a sample full list of rules on the WordPress plugin API pages.

In this case news was right at the top, so the match was made before reaching the page wildcard rules.

This is how it’s meant to happen, and my trick to imitate a sub directory of news would not work by design.

However, I think that the solution above is valid.

Javascript 6 Nations’ Sweepstake Calculator

I do a bit of voluntary work for CR Caernarfon, my local rugby club.

In 2014 we organised a 6 Nations sweepstake to a bit of fun and raise some money for charity. If you’re not aware of the 6 Nations championship, it’s a springtime rugby competition between England, France, Ireland, Italy, Scotland and Wales. They play each other once and a winner is decided at the end.

Players are invited to guess the score for each match plus the number of tries. They’re also asked to predict the final table.

There are six point-scoring rules for the sweepstake, and with 15 matches (n(n-1)/2) it can take a few hours to calculate the points for each person. It was therefore a ripe programming challenge.

I coded a simple solution in javascript last year, but didn’t get used as there was no easy way for the organizer (hi, mum!) to input results. So I updated it for this year and it works well. Checkout the latest results.

The rules for individual matches are:

  1. 1 point for either predicting the correct winning team or predicting a draw for each match.
  2. 1 point for the correct score of each team in each match (max of 2).
  3. 1 point for the correct points difference between the teams in each match.
  4. 2 points for predicting the total number of tries (including penalty tries) in each match.

Maximum available points are 6 points for each match.

What I Did

(Note: the code is on github.)

First off I asked the organizer to input all data into a spreadsheet. Participants’ details were listed, along with their predictions for each match. Each match has a home score, away score and try count. This sheet was then exported as a CSV file.

D3.js is used to read in this data. I’ve used D3 on the Wales Yearbook so have a good grasp of its features. It has a csv method, d3.csv(url[[, accessor], callback]) which issues a GET request for the file, processes each row and then passes the result to a callback.

Using this method I turned the CSV data into a JSON structure for each participant:

return {
    // enw = name 
    // and anyone who's been involved in rugby will know the importance of nicknames!
    person : { name: d.enw1+' '+d.enw2, nickname : d.nickname, email : d.ebost },
    rankings : [d.finalFirst, d.finalSecond, d.finalThird, d.finalFourth, d.finalFifth, d.finalSixth],
    games : [
            home: +d.game1Home,
            away: +d.game1Away,
            tries: +d.game1Tries
        /* another 13 match results */
            home: +d.game15Home,
            away: +d.game15Away,
            tries: +d.game15Tries

This object is added to an array which is then passed to the callback.

But before I discuss that, I’ll demonstrate the API created to calculate the scores.

A Sweepstake javascript object holds all the data and does the calculations. Briefly:

var Sweep = {

    // array of participants
    players : [],

    // array of match results
    results : [],

    // a singleplayer object
    singleplayer : {},

     * Add a player to the object
    addPlayer : function(name)
        this.singleplayer = { name: name, matches: [] };
        return this;

     * Add a match prediction. This must be chained to a player via addPlayer()
    addScore : function(home, away, tries)
        this.singleplayer.matches.push( {
            home : home,
            away : away,
            tries : tries

        return this;

     * Add a match result
    addResult : function(name, home, away, tries)
            name: name,
            home : home,
            away : away,
            tries : tries

        return this;

     * Caculate and store the scores
    calculate : function()
        // loop through each player, calculate and store the points for each match

     * Display the scores in a table
    showResult : function()
        // format the results in a table


Using this object we can pass in each participant’s predictions and the final match results. Using rows passed by the D3.csv callback:

for(var i in rows)
    var player = rows[i];

    var result = Sweep.addPlayer(;
    for(var j in

Each participant is now stored in the Sweepstake.players array.

Next up is to add the match results:

    // results from the 2015 first weekend
    Sweep.addResult('Cym v Llo', 16,21,3) // Wal v Eng
            .addResult('Eid v Iwe',3,26, 2) // Ita v Ire
            .addResult('Ffr v Alb',15,8,1) // Fra v Sco

And then still within the callback we call Sweepstake.calculate() and Sweepstake.showResult().

The calculate method loops through the players and checks their scores with the results:

// for each match
// individual points breakdown of points allocation
player.matches[j].points = {winner:'',score_home:'',score_away:'',diff:'',tries:'',total:0}

// match points difference
result.pd = Math.abs(result.home - result.away);

// match winner prediction
// Home Win
if( (player.matches[j].home > player.matches[j].away) && (result.home > result.away) 
||  // OR away win
(player.matches[j].home < player.matches[j].away) && (result.home < result.away) 
|| //  OR draw
(player.matches[j].home == player.matches[j].away && result.home == result.away)
player.matches[j].points.winner = '1'; // a string to be outputted in html
player.matches[j]; // final score as an integer

// compare with player's predictions

// home score
if(result.home == player.matches[j].home) 
player.matches[j].points.score_home = '1';
// away score
if(result.away == player.matches[j].away) 
player.matches[j].points.score_away = '1';

// prediction points difference
player.matches[j].pd = Math.abs(player.matches[j].home - player.matches[j].away);             
if(result.pd == player.matches[j].pd)
player.matches[j].points.diff = '1';

// try count, two points
if(result.tries == player.matches[j].tries)
player.matches[j].points.tries = '2';
player.matches[j] = player.matches[j] + 2;

// total player score
player.points = player.points + player.matches[j];

The Sweepstake.showResult() is simply a method to output the above in a table row, and then render the full table on the page.

In order for participants to check their individual results, I added a click event to their name which opens a Bootstrap modal window. It gives a breakdown of their points for each match.


This is a simple jQuery click() method to listen for the click on the player name, and then show the table of results in the modal.


What took hours now takes seconds to calculate, and results are updated on the club website after each round’s final game.

My main issue is that this is updated by editing the HTML file (with Sweepstake.addResult()) and uploading via FTP. I’d love to provide a way so that I can add scores off my smartphone, but this would call for back end programming, data stores and authentication etc. I will probably look at this for next year.

Finishing Off

As usual time constraints has stopped me in adding the rules for the final table, but as the competition has still not ended (in three weeks’ time, as of time of writing) I still have time for this. I’ll update this post accordingly.

All the code is available on my github repo.

WordPress Bootstrap Plugin: Bootstrap Column widget

Download Bootstrap Column from WordPress Plugins, or fork Bootstrap Column widget on Github.

WordPress Bootstrap Plugin

Although I’ve built a few custom WordPress plugins over the years, this is the first I’ve released publicly.


Bootstrap Column simply adds a widget which nicely works with Bootstrap v3 based themes.

It’s not dissimilar to the standard WordPress textbox widget, but it offers extra Bootstrap v3 layout functionality.

Those familiar with Bootstrap will quickly understand how it works.

Bootstrap offers a 12 column grid within four break points at which to change the layout. These breakpoints start from mobile first:

  • Extra Small – for smartphones at less than 768px screen width
  • Small – for tablets between 768px and 992px
  • Medium – for larger tablets and small desktops between 992px and 1200px
  • Large – for everything else above 1200px

Bootstrap provides a set of classes which not only control column width, but also their behaviour at these different screen sizes.

For example, take the following <div class="col-lg-4 col-md-6">.

Both Small (sm) and Extra Small (xs) are left out, so Bootstrap will default this div to a full width layout, i.e. 12 columns.

At the Medium (md) breakpoint the div will stretch to 6 columns, or half the page width. At the Large (lg) breakpoint it will only be four columns in width.

Ideally it would be partnered with another div with this class value <div class="col-lg-8 col-md-6">. At Large this gives you two columns at 1/3 and 2/3 page widths. At Medium it gives two equally sized columns.

These CSS features are very powerful and enables very quick layouts and prototyping.

Hopefully my plugin will make things a bit easier for other Bootstrappers.

Poor Code

Working on the plugin has really highlighted the standard of coding in WordPress. Although it’s a quick way of adding widgets, the code’s structure is not to be copied.

The widget framework is a quick and easy way to create plugins, but very messy and has little in common with the SOLID principles of programming.

For example, the single responsibility principle is ignored here. The class that extends the plugin class is responsible for saving and updating the widget’s contents and settings, and also rendering the HTML for both the front and back ends.

A nice templating system would be a tidy addition to the current way of doing things.

Developing with Symfony2

On June 22nd the website officially launched.

UPDATE: was nominated for the Kieran Hegarty Award for Innovation at the 2014 Celtic Media Festival!

Dyma is a website which gives a platform to young people between the ages of 13 and 18. They can upload videos and share a part of their lives as part of the Children’s Commissioner for Wales’ Dyma Fi/See Me project. The aim of the project is to give a realistic picture of young people in Wales today – in their own words.

Phil Stead from Cwmni Da brought me on the project to head up the technical side.

This is the biggest project that I have worked on, and possibly the most stressful. But I’m extremely proud of what was produced and how it worked.

Symfony2 was built as a custom bundle on the Symfony2 framework. Although I’d read much on S2 beforehand, it was the first time I’d actually had the chance to use it for real.

We did briefly discuss using Drupal or WordPress, but I was sure that neither these would not have been the correct solution. I’ve used much of both over the years, and ploughing through their ageing codebases did not attract me.

I was pretty confident that Symfony2 would be straightforward to pick up, but it took me much longer than I had anticipated. While frustrating that I knew what I wanted to do, just not the way Symfony2 wanted to do it (!), learning the ‘proper’ way of developing software was a worthwhile journey. Having a much better understanding of how software is structured make developing new projects much easier.

What I really came to appreciate about Symfony2 was the way in which database tables are defined as entities, and their relationships are hardcoded into the files. Whilst difficult to understand at first, I now miss the solid relationships between the entities, forms and the data they controlled. One of my recent projects was a simple plugin for WordPress, and not having a solidified relationship between database and code is limiting and feels so old fashioned.


The front end was designed by the hugely talented Iestyn Lloyd, using the Bootstrap (version 2.x) CSS framework. I chipped in with some of the HTML and CSS, but my main work on the frontend was the coding. jQuery is in there as well as Three.js for controlling the zooming videos.

Vimeo API Development

As the site was for uploading and showing video, we decided to use Vimeo’s platform. Each uploaded video was sent to Vimeo via their API for encoding. It was then simple to use their player to showcase the submitted videos on the site. I used Vimeo’s PHP library on Github, but did end up making small improvements.

Version Control and Deployment

Github was used for versioning and deployment. I wrote a bash script on the server for deployments, and this was ran manually whenever we needed an update. Me and Iestyn have overwritten each other’s files on many projects, So Github was a welcome change. Iestyn even managed to overcome his deep rooted fear of the command line!


The site was hosted on Digital Ocean’s servers. The site handled all out uploading tests admirably, and this is no doubt thanks to Symfony2’s solid architecture (although Digital Ocean’s solid state hard drives helped!).

To my shame no unit test were designed. This was my first SYmfony2 app, and I had often read that unit test were not advised on first projects.


I would have liked to develop a simple Page CMS, but time did not allow it. Static pages were updated through the usual deployment process. One of Symfony2’s main problems is that although Composer works brilliantly, finding the correct version numbers for the dependencies was a pain.

I also struggled with getting Sonata Admin to work, although when it finally caved in it worked well.

Unit Tests would have improved development, but again time was against me.

In redoing the project, I would like to have had a proper development server and a better way of deployment. But with having to engineer all levels of functionality (server install/config, backend, frontend) on the project there simply wasn’t time.

All in all I enjoyed the learning experience and look forward to using Symfony2 again. This time the learning curve will not be quite as steep.

Better Marker Controls with LeafletJS

This is a follow up post to Custom Marker Controls with Mapbox and LeafletJS.

As with many projects, things changed and I was asked to add more controls to the map.

Each marker had it’s own category, and now each had its own language, Welsh or English.

Adding another custom control to show or hide markers based on two different taxonomies means a proper system for checking if a marker should be displayed or not. Much of the code I previously wrote was refactored.

First of all the language had to be added to the GeoJSON object.

language = { language : plentyn.language, code : plentyn.lang_code };

If the video had no language (i.e. deemed to be bilingual) it was an empty object. This property was added just like the category.

// the marker. all values are for demonstration
var geoObj = {
    type: 'Feature',
    geometry : { type : 'Point', coordinates : [ 53, -4 ] },
    properties : { 
        title : 'Video Title',
        video : '',
        category : category,
        language : language

To keep track of which markers should be shown or hidden, I had two arrays – categoryHide and langHide – to store them.

When a category or language checkbox was clicked, the code would add or remove the marker from either array.

// each checkbox had a data-df-type attribute to store the category or language

// value of the checkbox
var Tvalue = input.value;

// for the category marker...
if(input.dataset.dfType == 'category')
        categoryHide.splice(categoryHide.indexOf(Tvalue), 1); // remove (to show)
        categoryHide.push(Tvalue); // add (to hide)

After each array was prepared, I called a toggleMarker() function. This function would loop through each marker in the main array. If the marker was hidden it would be shown, then hidden if it was in the either of the hide arrays.

// pass the main map object
toggleMarker = function(map) 
    // loop through each marker
    for(var i in Dymafi.marks)
         var mrkr = Dymafi.marks[i];

        // show all hidden markers

        // check if marker language is in hide array
        if(languageHide.indexOf( != -1)
            // hide this marker

        // check if marker category is in hide array
        if(categoryHide.indexOf( != -1)
            // hide this marker

This is an improvement on the old solution, and ready for any more checkboxes that may need to be added.


Although it worked well, a new problem was created. The control box was growing and was getting in the way on smaller screens.

I looked at Leaflet’s documentation and tutorials for ideas. It had a nice built in show hide ability when controls were created for its own layers, but were unavailable (for whatever reason) for custom controls.

So with a bit of HTML, jQuery and CSS I had my own system working:

I added an anchor with a background icon to the control <div>:

<a href="#" class="menuclick"><i class="icon-align-justify"></i></a>

The jQuery adds or removes a class from the parent <div>:

// set a variable if touch is enabled
var clickevent = 'mousedown';
if ('ontouchstart' in document.documentElement) {
    clickevent = 'touchstart';

// show/hide the categories on hover
$('a.menuclick').on('mouseenter', function(){
$('.cats').on('mouseleave', function(){
// the same for touchscreens
if(L.Browser.touch) // LeafletJS utility 
    $('a.menuclick').on(clickevent, function(){

    // hide the div if the map is moved
    map.on('movestart', function(){

And the CSS:

/* .cats is the main wrapper */
/* show hide on touch/mouseover */
.cats .options{
.cats.expanded .options{
min-width: 130px;
.cats a{
.cats.expanded a{

This need some clearing up, but works great.

You can see the finished result on the Dyma Fi map page.

Custom Marker Controls with Mapbox and LeafletJS

I’m working on a simple mapping application to display user videos on a map. I’ve opted to use Mapbox (which is built on LeafletJS) mainly because of the styling options available.

My problem was showing and hiding markers based on their categories. Each marker has this geoJson structure:

// the marker's category object
category = { id : 1, name: 'category name' };

// the marker. all values are for demonstration
var geoObj = {
    type: 'Feature',
    geometry : { type : 'Point', coordinates : [ 53, -4 ] },
    properties : { 
        title : 'Video Title',
        video : '',
        category : category

Each category was in it’s own array, each an object with an id and name attribute as above:

maincats = [category1, category2, category3];

As far as I could google, I couldn’t find a method of adding map controls to show or hide these markers based on a property. So I rolled my own.

This will create a div of checkboxes for each category, which will show or hide all markers of that category.

Here I have the on ready event for the marker layer, which then loops through each of its markers (layers, in this case). After adding each marker to an array I build the control structure.

map.markerLayer.on('ready', function(e) 
    marks = []; // array of markers
    this.eachLayer(function(marker) // loop
        marker.bindPopup('pop up content etc');
        marks.push(marker); // add the marker to the array

     var cats = L.control();

     cats.onAdd = function (map) {
         this._div = L.DomUtil.create('div', 'cats'); // create the control div

         // create the html for the checkboxes
         var html = '<h6>Categories</h6>';
         for(var i in maincats)
             html += '<div><input type="checkbox" name="category-'+maincats[i].id+'" value="'+maincats[i].id+'" checked />'+maincats[i].name+'</div>';

         // add the html to the div
         this._div.innerHTML = html;

         // add the click event listener
         L.DomEvent.addListener(this._div, 'click', function(e){

            var input = e.srcElement;
            var catid = input.value;

            if(typeof(input.value) == 'undefined') return false; // ignore random clicks on the div

            // loop through each marker, show or hide if in category
            for(var i in marks)
                if(marks[i] == catid)
                    // check if box is checked, then show or hide accordingly
                    (input.checked) ? map.addLayer(marks[i]) : map.removeLayer(marks[i]); // each marker is a single layer


This works well enough, although I am yet to browser test it properly.

I suspect there may be better ways of doing this with LayerGroups, but I couldn’t get them to work at all. But this method is as good as any.