Skip to main content

WordPress: Taking the Hack out of Multiple Custom Loops

By Lyza Gardner

Published on September 14th, 2009

Topics

Consider this a looking-forward-to WordCamp Portland post! WordCamp is this weekend in Portland, Sept. 19-20. It is utterly sold out!

This post is aimed at those comfortable with WordPress hackery and PHP programming.

Making things work for our customers often–nay, all the time–ends up with the need for multiple WordPress loops on pages

On a given day, one of our enterprise or otherwise-crafty-and-complex clients may request, say, the three most recent posts about foo in the right sidebar, “upcoming events” (e.g. date-sensitive posts in some events category) in another box, a couple of embedded and editable WordPress page components, a random post or two, and then the “traditional” post loop itself. Whew.

I have long tried to find the best way to handle custom loops. For sake of definition, custom loop here means querying and displaying posts and/or pages within a page that are not that page’s “natural” queried posts (that is, the landing page of a WordPress blog by default “naturally” queries the most recent posts, and a category page “naturally” queries for posts in a given category).

There are a number of plugins and even entire themes keyed toward custom-loop-heavy development and modular formatting of posts in loops, including the Carrington JAM theme. But I find that most solutions either are a bit too baked–the Carrington theme feels like it would require a lifestyle change and an adoption of a new outlook (not that I’m ragging on the Carrington stuff; I’ve actually heard good stuff about it)–or a little bit too under-baked and soggy in the middle (“Here’s a two-line code snippet to call query_posts! I think? I mean, this should work, probably.”).

Problems I keep running into:

  • The dreadful tendency to forget to restore the “natural”, appropriate global post and wp_query objects after one is done messing about. Those are sort of inviolate and should not be sullied, but it’s wretchedly easy to lose track.
  • Yet, if one does not pollute the global namespace with posts in the custom loop, one cannot access certain template tags and the like.
  • The mishmash of logic and formatting that often happens when one munges together a custom query in a page or post template is totally aesthetically unpleasing and betrays a certain lack of character/mettle.
  • Duplicated markup across a site that would better be modular and re-used.

It occurred to me recently that there is a straightforward way to deal with this. By creating a custom function (in a given theme’s functions.php file) of relatively low complexity, one can solve some of these problems once and for all.

It’s prefaced with lyzadotcom because I initially wrote it for my own site, but you can call it whatever.

This is what it looks like, and we’ll talk about it more below:

/**
 * To use when a loop is needed in a page. Use $args to call query_posts and then
 * use the template indicated to render the posts. The template gets included once
 * per post.
 *
 * @param Array $args               WordPress-style arguments; passed on to query_posts
 *                                  'template' => name of post template to use for posts
 * @return Array of WP $post objs   Matching posts, if you should need them.
 */
function lyzadotcom_custom_loop($args)
{
    global $wp_query;
    global $post;
    $post_template_dir          = 'post_templates';
    /* The 'template' element should be the name of the PHP template file
       to use for rendering the matching posts. It should be the name of file,
       without path and without '.php' extension. e.g. the default value 'default'
       is $post_template_dir/default.php
    */
    $defaults                   = Array('template' => 'default' );

    $opts = wp_parse_args($args, $defaults);

    // Bring arguments into local scope, vars prefixed with $loop_
    extract($opts, EXTR_PREFIX_ALL, 'loop');

    // Preserve the current query object and the current global post before messing around.
    $temp_query = clone $wp_query;
    $temp_post  = clone $post;

    $template_path = sprintf('%s/%s/%s.php', dirname(__FILE__), $post_template_dir, $loop_template);

    if(!file_exists($template_path))
    {
        printf ('<p class="error">Sorry, the template you are trying to use ("%s") in %s() does not exist (%s).',
            $template,
            __FUNCTION__,
            __FILE__);
        return false;
    }
    /* Allow for display of posts in order passed in post__in array
       [as the 'orderby' arg doesn't seem to work consistently without giving it some help]
       If 'post__in' is in args and 'orderby' is set to 'none', just grab those posts,
       in the order provided in the 'post__in' array.
    */
    if($loop_orderby && $loop_orderby == 'none' && $loop_post__in)
    {
        foreach($loop_post__in as $post_id)
            $loop_posts[] = get_post($post_id);
    }
    else
        $loop_posts = query_posts($args);

    /* Utility vars for the loop; in scope in included template */
    $loop_count             = 0;
    $loop_odd               = false;
    $loop_even              = false;
    $loop_first             = true;
    $loop_last              = false;
    $loop_css_class         = '';                               // For convenience
    $loop_size = sizeof($loop_posts);
    $loop_owner = $temp_post;       /* The context from within this loop is called
                                       the global $post before we query */

    foreach($loop_posts as $post)
    {
        $loop_count += 1;
        ($loop_count % 2 == 0) ? $loop_even = true : $loop_even = false;
        ($loop_count % 2 == 1) ? $loop_odd  = true : $loop_odd  = false;
        ($loop_count == 1) ?     $loop_first = true : $loop_first = false;
        ($loop_count == $loop_size) ? $loop_last = true : $loop_last = false;
        ($loop_even) ? $loop_css_class = 'even' : $loop_class = 'odd';
        setup_postdata($post);
        include($template_path);
    }
    $wp_query = clone $temp_query;  // Put the displaced query and post back into global scope
    $post = clone $temp_post;       // And set up the post for use.
    setup_postdata($post);
    return $loop_posts;
}
Code language: PHP (php)
  • By default, this function will look for loop templates in a directory called ‘post_templates’ in your theme’s template directory. Also by default, it will look for a file called ‘default.php’ in this directory. You can change the directory for templates if this bugs you.
  • Templates are PHP files and you can do what you will in them. All template tags are available.
  • You can create as many template files as you want and use them for different chunks of content.

For example, this might be your default.php template file.

<div class="post <?php echo $loop_css_class ?>" id="post-<?php the_ID(); ?>">
        <h4><?php echo $loop_count; ?><a href="<?php the_permalink(); ?>" title="<?php the_title(); ?>">
            <?php the_title(); ?>
        </a></h4>
        <em><?php the_date(); ?></em><br />
        <?php the_excerpt(); ?>
    </div>
Code language: HTML, XML (xml)

You can see that, in addition to standard WordPress template tags, I’ve used a couple of the batching variables made available by the function.

Available variables in loop templates are:

$loop_count                         // The count of the current post within the loop
$loop_odd                           // True if we're on an odd item (e.g. $loop_count = 1, 3, 5...)
$loop_even                          // True if we're on an even item (e.g. $loop_count = 2, 4, 6...)
$loop_first                         // True if this is the first post/page in the loop
$loop_last                          // True if this is the last post/page in the loop
$loop_css_class                     // 'even' if $loop_even, 'odd' if $loop_odd
$loop_size = sizeof($loop_posts);   // Total number of posts/pages in loop
$loop_owner = $temp_post;           // The post in global scope before this loop was entered
Code language: PHP (php)
  • The use of clone means you need to have PHP5.
  • I have not tested this on any versions of WordPress 2.8. I imagine it would be OK, but YMMV.
  1. Edit your themes functions.php file and stick this function in it.
  2. Create at least one loop template file. By default (unless you tell it otherwise, which likely you will) this function looks for a template in
    /post_templates/default.php

    We’ll talk about templates shortly.

  3. Call the function from any WordPress template file you might need it from. You talk to it with WordPress query-string-style arguments. This function takes any argument that the query_posts() function takes.
  4. It also takes an additional argument, ‘template’. This is how you tell it which loop template to use.

In the desired WordPress template:

<?php
        $args = Array('pagename'         => 'embed-this-page',
                      'template'         => 'embed');
    ?>
    <?php lyzadotcom_custom_loop($args); ?>
Code language: PHP (php)

This is going to look for the template ’embed.php’ in your $post_templates directory. This template might look like this:

<div class="embedded" id="embedded-page-<?php the_ID(); ?>">
        <strong><?php the_title(); ?></strong><br />
        <?php the_content(); ?>
        <?php edit_post_link(); ?>
    </div>
Code language: PHP (php)

In the desired WordPress template:

<ul>
    <?php
        // This assumes you have category IDs.
        // Getting them is beyond the scope of this pos
        $args = Array('category__in'        => Array(5,6,4),
                      'showposts'           => 5
                      'template'            => 'list_with_link');
        lyzadotcom_custom_loop($args);
    ?>
    </ul>
Code language: PHP (php)

list_with_link.php might look like:

<li id="link-<?php the_ID(); ?>" class="<?php echo $list_css_class>">
        <a href="<?php the_permalink()" title="<?php the_title(); ?>"><?php the_title(); ?></a>
    </li>
Code language: PHP (php)

In the desired WordPress template:

<h3>March's Bananas</h3>
    <?php
        $args = ('tag'              => 'banana',
                 'showposts'        => -1, //show all posts
                 'monthnum'         => 3,
                 'year'             => 2009,
                 'template'         => 'month_archive_list');
        $banana_posts = lyzadotcom_custom_loop($args);
        /* Hint: You might used the returned $banana_posts to
           populate another call to lyzadotcom_custom_loop(),
           e.g. 'post__not_in' => [ids from $banana_posts]
           to avoid duplicating posts!
        */
    ?>
Code language: HTML, XML (xml)

month_archive_list.php might look like:

<div class="archive_post" id="archive_post-<?php the_ID(); ?>">
        <?php echo $loop_count; ?> of <?php echo $loop_size; ?> Bananas:
        <strong><?php the_title(); ?></strong><br />
        <?php the_content(); ?>
        <?php edit_post_link(); ?>
    </div>
Code language: HTML, XML (xml)
Will this work with pages as well as posts?
Yes. Sure. Just fine! Anything that query_posts() can do, I can do too!
Why isn’t this a plugin?
It took me hours just to write this post; I’ve got stuff to do! If there’s a lot of interest, maybe I’ll consider it.
I found a bug/horrible security hole/idiocy?
Please let me know! Gently! Comments to this post is a fine method to do so.
How is this bad boy licensed?
GPL. Do with it what you will!
OMG! Don’t you know about XYZ plugin? It totally does this, but better!
Nope! But I’d like to!

I’ve gone ahead and zipped up some stuff for you:

  • a functions.php file with the function in it (add it to yours)
  • Several example files in a post_templates directory
  • Example.php with several calls to the function

Download ZIP

Comments

Michael Fields said:

Awesome article, glad to see that someone else thinks this way too. I’ve been working on a theme for the past two months that will integrate functionality close to this. I bookmarked this page for reference. Thanks!
-Mike