WordPress popular posts by week code

Currently, I see no way that you can do that as you have no good reference to pull date based statistics.

Notice:
You are storing post views as follows:

add_post_meta($postID, $count_key, '0');

To be able to grab “Most viewed in period”, you would have to do something more complex, probably with an extra database table. Right now, you have no relation between the number of views and any time period.

For example, if you created the following table in your database:

{$prefix} should use the wordpress database table prefix (e.g. wp_ )

CREATE TABLE {$prefix}post_views (
  post_id bigint(20) NOT NULL,
  entry_day datetime NOT NULL,
  num_views int NOT NULL,
  PRIMARY KEY  (post_id,entry_day),
  KEY idx_pv_ed (entry_day)
);

With that table, you can now track highest posts in a period.

Here is a revised version written as a class with logging, error handling, and an example usage script to show what is going on.

The Handler Class

The below code is a small class that includes debugging information so that you can see where any problems are.

I’ve sprinkled it with notes to explain what is going on

class My_Most_Viewed_Posts {
    protected $errors = null;
    protected $notices = null;
    protected $debug = false;

    # This just sets up the errors, notices, and debug variables able
    function __construct($debug = false) {
        $this->clear_all();
        if ( $debug ) {
            $this->debug = true;
        }
    }

    # This lets you add notices about what is going on so you can display them later
    protected function add_notice($msg = '') {
        if ( is_array($msg) || is_object($msg) ) {
            $this->notices[] = print_r($msg, true);
        } else {
            $this->notices[] = $msg;
        }
    }

    # Get an array of the most viewed posts as an array of the form ( post_id => number_of_views )
    # - $oldest_date : should be of the form YYYY-MM-DD
    # - $newest_date : should be of the form YYYY-MM-DD ( if null is provided, it will default to the current day )
    # - $num_to_get  : the number of results to return (e.g. 4 highest by default)
    function get_highest_posts_in_range( $oldest_date, $newest_date = null, $num_to_get = 4 ) {
        # Set up our return value
        $found = array();

        # Debug notice noting what was passed in
        if ( $this->debug ) {
            $this->add_notice( "Starting get_highest_posts_in_range( '{$oldest_date}', '{$newest_date}', {$num_to_get} )" );
        }

        # Do all real processing in a try / catch block to ensure can log problems instead of just throwing errors
        try {
            # Get a handle to the global database connection
            global $wpdb;

            # Verify that the provided $oldest_date is 10 characters long
            if ( 10 !== strlen($oldest_date) ) {
                throw new Exception("Parameter 1: Must be of the form YYYY-MM-DD", 10001);
            } else {
                # Appends time to the oldest date to make any between query include everything on the date
                $oldest_date .= ' 00:00:00';
            }

            # Initialize the $newest_date variable OR validate that any non null value could be in the right format
            if ( is_null( $newest_date ) ) {
                $newest_date = date('Y-m-d') . ' 23:59:59';
            } else if ( 10 !== strlen( $newest_date ) ) {
                # Error
                throw new Exception("Parameter 2: Must be null or in the form YYYY-MM-DD", 10002);
            } else {
                # Appends time to make any between query contain everything entered that day
                $newest_date .= ' 23:59:59';
            }

            # Make sure that the number of records to get is sane
            $num_to_get = (int) $num_to_get;
            if ( $num_to_get <= 0 ) {
                throw new Exception("Parameter 3: Must be a positive integer", 10003);
            }

            # Debug message to note the final values of provided variables after validation
            if ( $this->debug ) {
                $this->add_notice( "After Validation - Oldest: {$oldest_date}, Newest: {$newest_date}, Number to get: {$num_to_get}" );
            }

            # Build our query
            # It will return the post_id and number of views (as views_in_period) for the most viewed items in the given date range
            $query = <<<SQL
SELECT post_id, SUM(num_views) as views_in_period
 FROM {$wpdb->prefix}post_views 
 WHERE entry_day BETWEEN %s AND %s
 GROUP BY post_id
 ORDER BY views_in_period DESC
 LIMIT %d
SQL;

            # Add our variables to the mysql query above safely
            $query = $wpdb->prepare( $query, $oldest_date, $newest_date, $num_to_get );

            # Debug message to note what the final prepared query is
            if ( $this->debug ) {
                $this->add_notice( "Prepared Query:<br />{$query}" );
            }

            # Run the query and get the results
            $results = $wpdb->query( $query );
            if ( false === $results ) {
                $error = $wpdb->last_error;
                throw new Exception("Bad Database query: {$query}, DB Error: {$error}", 10004);
            } else if ( 0 < count($results) ) {
                # There is at least one result. Add a debug message to show what the results are
                $this->add_notice("Results detected:");
                $this->add_notice( $results );

                # Cycle through each result and add it to our return value
                foreach ( $results as $row ) {
                    $found["{$row['post_id']}"] = $row['views'];
                }
            } else if ( $this->debug ) {
                # No results returned, add a notice if in debug mode
                $this->add_notice("Found no results for query");
            }
        } catch ( Exception $e ) {
            # Exception detected, add it to the array of errors
            $this->errors[] = $e;
        }
        return $found;
    }

    # This adds a new row to the post views table OR updates an existing row's num_views by 1
    # The record is automatically added as viewed on the current day
    function add_post_view( $post_id ) {
        # Log how we were called
        if ( $this->debug ) {
            $this->add_notice("Called add_post_view( {$post_id} )");
        }
        # Initialize our return value
        $added = 0;

        try {
            # Get hold of the database
            global $wpdb;

            # Add a new record. If there is a key violation, update the number of views instead.
            # ( the unique key on the table is on the post_id and entry_day )
            $query = <<<SQL
INSERT INTO {$wpdb->prefix}post_views (
    post_id, entry_day, num_views
 ) VALUES (
    %d, UTC_DATE(), 1
 ) ON DUPLICATE KEY UPDATE num_views = num_views + 1
SQL;

            # Add our variables to the query in a safe manner
            $query = $wpdb->prepare( $query, $post_id );

            # Log the query to be ran so we can look at it if needed
            if ( $this->debug ) {
                $this->add_notice("Prepared Query: {$query}");
            }

            # Determine our results
            $result = $wpdb->query( $query );
            if ( false === $result ) {
                # Error
                $error = $wpdb->last_error;
                throw new Exception("Bad Query: {$query}, Database Claims: {$error}", 10001);
            } else if ( 0 === $result ) {
                # Should never happen - would be an error as result is numer affected and there should be at least 1 row affected
                throw new Exception("Bad Query: {$query}, Database claims no records updated!", 10002);
            } else {
                # Note how many changes were made (anything over 1 is fine)
                if ( $this->debug ) {
                    $this->add_notice("Query completed with {$result} results");
                }
                $added = $added + $result;
            }
        } catch ( Exception $e ) {
            # Make note of the exception
            $this->errors[] = "Exception Ahoy!";
            $this->errors[] = $e;
        }
        if ( $this->debug ) {
            $this->add_notice("Leaving add_post_view");
        }
        return $added;
    }

    # Get the list of all errors as an array
    function get_errors() {
        if ( is_null( $this->errors ) ) {
            $errors = array();
        } else {
            $errors = $this->errors;
        }
        return $errors;
    }

    # Get the list of all notices as an array
    function get_notices() {
        if ( is_null( $this->notices ) ) {
            $notices = array();
        } else {
            $notices = $this->notices;
        }
        return $notices;
    }

    # Clear all errors and notices
    # Used on initialization and between calls
    function clear_all() {
        $this->notices = array();
        $this->errors = array();
    }
}

To make use of this class, you will need to do the following:

  1. Option 1: Turn it into a plugin
  2. Option 2: Turn it into a library file (see below)
  3. Option 3: Add it to the bottom of your themes functions.php file.
  4. Option 4: Add it to the top of the template you want to use it in.

Turning a class into a simple library file

If you want to use this as needed, you could create a new file ( say class-my-most-viewed-posts.php ) and copy the code into it with <?php on the first line of the file

Drop it into your theme directory and you can then call it as follows from within your template:

# Load the class file so that the class is made available
$found = locate_template('class-my-most-viewed-posts.php', true);
if ( empty($found) ) {
    echo "<p>Failed to locate template class-my-most-viewed-posts.php</p>";
} else {
    # NOTE - If you copy this into functions.php or into the template (instead of creating its own file):
    # - copy from the line below to the line further down labeled INLINE DECLARATION METHOD END

    if ( ! class_exists('My_Most_Viewed_Posts') ) {
        echo "<p>Failed to find class My_Most_Viewed_Posts</p>";
    } else {
        # Create an instance of the class in debug mode
        $Popularity = new My_Most_Viewed_Posts( true );
        if ( !is_object( $Popularity ) ) {
            echo "<p>Failed to create My_Most_Viewed_Posts object</p>";
        } else {
            # NOTE - the $post_id here should be the post to add it for!
            $entries_added = $Popularity->add_post_view( $post_id );
            if ( 0 === $entries_added ) {
                # Failed, show problems.
                echo "<p>Notices:</p><pre>" . print_r( $Popularity->get_notices(), true ) . "</pre>";
                echo "<p>Errors:</p><pre>" . print_r( $Popularity->get_errors(), true ) . "</pre>";
            } else {
                # Clear notices so far
                $Popularity->clear_all();

                # Tell user we are making progress - comment out when done debugging
                echo "<p>It seems to have worked. Added {$entries_added} record(s).</p>";
                echo "<p>Checking counts</p>";

                # Get the highest counts now that we have added a record (from the first of the year to tonight at midnight)
                $highest_counts = $Popularity->get_highest_posts_in_range( '2015-01-01', null, $num_to_get = 4 );
                if ( ! is_array($highest_counts) ) {
                    echo "<p>Bad return value for highest posts in range</p>";
                    echo "<p>Notices:</p><pre>" . print_r( $Popularity->get_notices(), true ) . "</pre>";
                    echo "<p>Errors:</p><pre>" . print_r( $Popularity->get_errors(), true ) . "</pre>";
                } else {
                    # We have some highest counts in the form post_id => views

                    # Get the ids of the posts (see note 2 below)
                    # see: http://php.net/manual/en/function.array-keys.php
                    $wanted_ids = array_keys( $highest_counts );

                    # Build arguments for [WP_Query] (see note 3 below)
                    # - posts_per_page : -1 means to show them all
                    # - post_type : any will show all but those flagged exclude from search
                    # - post_parent__in will require that the results have keys in the provided array
                    # see http://codex.wordpress.org/Class_Reference/WP_Query
                    $wanted_args = array(
                        'posts_per_page' => -1,
                        'post_type' => 'any',
                        'post_parent__in' => $wanted_ids
                    );
                    $wanted = new WP_Query( $wanted_args );
                    if ( $wanted->have_posts() ) {
                        while ( $wanted->have_posts() ) {
                            $most_viewed = $wanted->next_post();
                            echo "<p>Post Found:</p><pre>" . print_r( $most_viewed, true ) . "</pre>";
                        }
                        unset( $wanted );
                    } else {
                        echo "<p>No posts found for wp_query using args:</p><pre>" . print_r( $wanted, true ) . "</pre>";
                    }
                }
            }
        }
    }
    # INLINE DECLARATION METHOD END
}

Obviously, the code could be a lot shorter without all of the debug messages, but they should be helpful in tracking down the problem.

If you choose to place the class structure in your themes functions.php or in your template, copy the section just between INLINE DECLARATION METHOD for running the code. (e.g. you do not use locate_template )

Leave a Comment