WordPress developer, consultant, nerd

Hacking the WordPress Popular Posts Plugin for a Raw Data Array

A popular posts list can be a great way to encourage readers to explore more articles on your site. We see them all over the web in the form of trending articles, and if you spend as much time developing with WordPress as I do, you certainly would have come across this requirement before. As with most things in WordPress, there are plenty of plugins out there that can get you up and running fairly easily, all with varying levels of flexibility and functionality, but finding plugins that offer the flexibility a developer needs can often be a challenge.

As a developer who spends a lot of time building custom WordPress themes, I’m not particularly insterested in plugins that lack flexibility. It is my belief that a solid plugin should have both a stable and flexible API layer, as well as an out-of-the-box layer that builds upon the plugin’s own API. To me, this seems like a sound pattern for building flexible WordPress plugins that suit both ends of the user scale, as it provides a ready solution for non-tech users, whilst also allowing developers to leverage the API for any custom requirements.

Unfortunately, this is a bit of a utopian concept in the WordPress landscape, so we often find ourselves in a position where we either need to pull a few tricks, build something from scratch, or simply make do with what is available.

I recently implemented the WordPress Popular Posts plugin into a site for the first time, and although I found the plugin to be quite flexible from a users point of view, it lacked the mechanism I needed for accessing the raw data. After some reading, I found that I would most likely have to go through a cumbersome process of filtering the output of the plugin to get the layout I needed. My days of extensive hooking and filtering to modify markup have taught me the value and power of using raw data, so I set out to find a way around the problem.

Why not just pull straight from the database?

I had considered this, as it seems like a reasonable possibility. But, the more I thought about it, the less appropiate this solution was. The plugin actually maintains quite a lot of data along with its own table structure, so I would only be writing queries for a potentially unstable database that I don’t control. This scenario comes with a higher probability of breakages down the track, so I needed to find a better way. Ideally, a way that I could leverage existing plugin API methods to ensure maximum future compatibility…

Hacking on the template tags turned out to be the way to go

The plugin does have some template tags (functions) available, which turned out to be part of the answer, but the initial problem with the functions is that they are actually designed to drop markup into a template. What I did find interesting, however, was that the wpp_get_mostpopular() function actually builds a shortcode which dumps the output, and the shortcode that it builds just happens to have an available filter.

Now, I realise I said I wasn’t interested in going crazy with filters, but the thing that caught my eye was the fact that the first argument passed to the filter was the raw data I wanted – I just needed a way to catch that data so that I could repurpose it for my own needs.

The solution was to create a class which I could instantiate whenever I needed a data set of popular posts. To work, the class would need the following;

  1. An invocation of the wpp_get_mostpopular() function that doesn’t actually produce any markup;
  2. A hooked filter to catch the output of the template tag and capture the data in a raw state;
  3. A method that would return the data we need;
  4. The ability to take arguments that would allow us to alter that data set.

Having this meant I could instantiate a list of popular posts – as many times as I required and with varying options on each instance – and use the post data to render any type of view that I needed. This gave me full control of the output.

The Code

Note:

This code was written using version 3.2.1 of the WordPress Popular Posts plugin, running on WordPress version 4.1. I cannot guarantee this will work on different versions.

You are welcome to use this code under the terms outlined in the Creative Commons Attribution 4.0 license.

<?php

/**
 * Class Popular_Posts_Data
 *
 * A class for retrieving an array of popular posts post data. This class is designed for use with the WordPress Popular
 * Posts plugin (@see https://wordpress.org/plugins/wordpress-popular-posts/) and will not work without it.
 *
 * @author 	Phil Smart
 * @license https://creativecommons.org/licenses/by/4.0/legalcode Creative Commons Attribution License
 */
class Popular_Posts_Data
{
    /** @var array $items Property for storing the final data array */
    protected $items = array();

    /**
     * Constructor. This accepts either an integer or an array of query args suitable for the wpp_get_mostpopular()
     * function. If an int is passed, it is treated as the number of posts to get. If an array is passed, it is
     * treated as a plugin-specific query array.
     *
     * @param int|array $args
     */
    function __construct( $args = 10 )
    {
        // Make sure the function is available
        if ( ! function_exists( 'wpp_get_mostpopular' ))
            return;

        // Catch the output of the function to prevent it from displaying and capture the data via the filter
        add_filter( 'wpp_custom_html', array( $this, '_get_posts_data' ), 10, 2 );

        // Ensure our arguments are of a suitable format
        $args = $this->normalise_args( $args );

        // Invoke the API method using our arguments array, and catch the output in the output buffer
        ob_start();
        wpp_get_mostpopular( $args );
        ob_get_clean();

        // Remove the filter so we don't affect other instances of the shortcode
        remove_filter( 'wpp_custom_html', array( $this, '_get_posts_data' ), 10, 2 );
    }

    /**
     * Normalises args for the wpp_get_mostpopular() function. This ensures we pass the function a correctly formatted
     * args array, but also allows us to define some default arguments specific to our needs.
     *
     * @param int|array $args
     *
     * @return array
     */
    protected function normalise_args( $args )
    {
        // If an integer is passed, treat it as a limit
        if (is_int( $args ))
            $args = array( 'limit' => $args );

        // Allows passing of an array of categories by reformatting into a string
        if (isset( $args['cat'] ) and is_array( $args['cat'] ))
            $args['cat'] = implode( ',', $args['cat'] );

        // Merge parameters with defaults. Set whatever defaults you like here.
        return wp_parse_args( $args, array(
            'range'     => 'weekly',
            'freshness' => 1,
            'orderby'   => 'views',
            'limit'     => 10,
            'post_type' => 'post'
        ) );
    }

    /**
     * The hooked filter that intercepts the shortcode output and receives a list of posts.
     *
     * @param $data
     * @param $instance
     */
    public function _get_posts_data( $data, $instance )
    {
        $this->items = (array) $data;
    }

    /**
     * Public accessor for posts array.
     *
     * @return array
     */
    public function get_posts()
    {
        return $this->items;
    }
}

Usage

<?php

// Simply get the five most popular posts
$query = new Popular_Posts_Data(5);
$array = $query->get_posts();

// More advanced query
$query = new Popular_Posts_Data(
      array(
            'range' => 'monthly',
            'limit' => '15',
            'cat'   => '234, 12, 54'
      ) );
$array = $query->get_posts();

Conclusion

Using this technique, we are able to add a new layer of flexibility to the WordPress Popular Posts plugin. Be mindful, however, that plugins change and those changes are not always fully backwards compatible. By writing your own code that relies on third party plugins, you will most likely need to maintain and adapt your code at some point. My decision to leverage existing inputs/outputs built into the plugin was purely to mitigate the potential for ongoing maintenance, but I don’t expect this to last forever.

  • Lucas

    Thank you for this code and explainations / Regards Lucas

  • http://www.themesrefinery.net/ Rehman Ali

    Nice plugins.But sometimes plugins create big hurdles for website in loading.I have created a best way to show Popular Posts by Views in WordPress without plugin.
    Popular Posts by Views in WordPress

    • http://philkurth.com.au Phil Smart

      Thanks for sharing Rehman.

      Of course, it is simple to implement a basic counter that counts your post views, but there are a few issues with your suggestion that spring to mind;

      1. Caching

      Server-side driven counting assumes that PHP is building every single request that hits the page. What happens when a page is cached? This is a very important consideration if we are going to talk performance, as caching will bypass the PHP for a vast number of visitors, throwing the count right out.

      I should also point out – for clarification – that the WordPress Popular Posts plugin (not my plugin, by the way; I just use it) counts via AJAX after the page has loaded, which means; 1.) it isn’t wedging in another database call before page load, and 2.) it bypasses any server-side cache ensuring a more accurate count.

      2. Maintenance

      I’ve spent many years of my life reinventing the wheel and the main lesson I’ve learned is the following: Coding everything out yourself, whilst highly educational, only adds more responsibility and cost to your plate in the form of maintenance. If there is a third party option that does the job well, then it is well worth considering for many reasons, including;

      1. The cost of maintaining a third party integration will be limited by the extent of your integration. The core is looked after by the developer/s who manage the project, so you don’t need to worry about it.

      2. An externally developed module will, on most occassions, have more features than we might be willing to build from scratch;

      3. If a project has been around for some time, the team will have undoutedly dealt with many of the same pitfalls and caveats that you will hit along the way.

      4. By using well-developed third party plugins, you free yourself up to focus on the more unique requirements that a project may have.

      A note on performance

      It’s important to remember that the general performance hit that plugins might introduce are easily mitigated with good optimisation; concatenating and minifying resources will generally take care of the most common hold-ups that plugins will introduce.

      • http://www.themesrefinery.net/ Rehman Ali

        Thanks Phil.

  • http://www.plennevaux.be/alexandre pixeline

    Hello Phil,
    Thank you for your write up. This is just to let you know that I “forked” your wrapper class to get closer to the raw results. More info here https://github.com/cabrerahector/wordpress-popular-posts/issues/122 Thank you. Alex.