Using a single PHP loop to output data into multiple wrapping elements

Published March 12, 2019
.* :☆゚

When you’re iterating over arrays in PHP and wanting to display the returned data into different wrappers, it can get messy pretty quickly, especially when HTML markup is thrown into the mix. I often run into this issue when I am building things like accordions, or tabbed data in WordPress.

Naturally, after running into this problem many times over working on various different sites, I just had to ask, isn’t there a better way to display all this content iteratively using just one loop instead of having to repeat myself?!


The Scenario

As a real world example, today I had to create a page layout similar to Bootstrap’s javascript tabs.

A standard workflow might look like this:

  1. Create a custom repeater field with ACF, with each row containing a title and content
  2. Input data in the backend
  3. Write a loop in the page template to iterate through the repeater field, grab the title fields, and build the navigation.
  4. Copy that same loop to get each row’s tab content, and build the tabs.

The reason you would use two of the same loops to output the data is because there isn’t an obvious way to separate the data in each row into different wrappers without restarting the loop over again.

As I quickly found out, conditionals are useless in this scenario. Since both the title and content are together in the same row, it is impossible to check when they are both queried. A counter is also useless for the same reason- the title and content will have the same iteration count as they are in the same row.

<?php if( have_rows('tab') ): ?>

	<div class="tabs">

	<?php while( have_rows('tab') ): the_row();

        $title = get_sub_field('title');
        $content = get_sub_field('content');
        $formatted_title = sanitize_title($title); //makes the text url friendly, see here: https://codex.wordpress.org/Function_Reference/sanitize_title
        ?>

        <!-- the navigation -->
        <div class="tab-nav"> 
            <a href="#<?php echo $formatted_title; ?>"><?php echo $title; ?></a>
        </div>

        <!-- the content -->
        <div class="tab-content">
            <div id="<?php echo $formatted_title; ?>">
                <?php echo $content; ?>
            </div>
        </div>

        <!-- the above works great if you have one row!
        It just won't do for the rows after that though because.... -->
	<?php endwhile; ?>

	</div>

<?php endif; ?>

If you run a loop that is set up like above, you’ll get markup like this:

<div class="tab-nav">
    <a href="#shipping">Shipping</a>
</div>
<div class="tab-content">
    <div id="shipping">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est l
    </div>
</div>

<div class="tab-nav">
    <a href="#sizing">Sizing</a>
</div>

<div class="tab-content">
    <div id="sizing">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est l
    </div>
</div>

...

Can you see the problem? There are multiple .tab-nav and .tab-content divs when we only want one each as wrapping divs.


The Solution

This is my solution for iterating over multiple rows of data and putting said data into separate wrapping containers, with one loop only.

<?php if(have_rows('tabs')) :
    $titles = ''; //first, declare the variables outside the loop
    $contents= '';
    ?>
    <?php while(have_rows('tabs')) : the_row();
        // vars
        $title = get_sub_field('title'); // store the data as variables
        $content = get_sub_field('content'); // store the data as variables

        //format the title so it's able to be linked to
        //for more see here: https://codex.wordpress.org/Function_Reference/sanitize_title
        $urlize_title = sanitize_title($title);
        $formatted_title = '<a href="#' . $urlize_title . '" title="' . $title . '">' . $title . '</a>';

        //format the content into a php/html string
        //note you can't break out of the php to write normal html with vars sprinkled in, as the string needs to be stored as a php variable for future use
        $formatted_content = '<section id="' . $urlize_title . '" class="tab-content"><h2>' . $title . '</h2>' . $content . '</section>';

        //concatenate the strings into new variables for future display
        //note this is php string concatenation - not forming an array
        $titles .= $formatted_title;
        $contents .= $formatted_content;
        ?>
    <?php endwhile; ?>
<!-- Now that we're out of the loop, let's bring the data inside our wrappers -->
    <div class="cell panel-links">
        <?php echo $titles; // this is the concatenated string we created above ?>
    </div>

    <div class="cell panel-content">
        <?php echo $contents; // this is the concatenated string we created above ?>
    </div>

<?php endif; ?>

This might look like a lot to take in but all it’s doing is passing data and storing it for future use once the loop has finished.

Firstly, we declare variables outside the loop to store the data in.

Each time the loop runs, it’ll store the title and content as a formatted string inside these declared variables.

Once the loop has finished running through all the rows, you’re free to use the declared variables and echo it with the wrapping divs outside the loop.

This isn’t limited to while loops either! You can use this method for any loop in PHP.

For example, a regular PHP foreach loop might look something like this:

<?php
$array = [...]; // the array to iterate over
//declare the vars
$languages = '';
$tools = ''
foreach ($array as $item) {
    //store the data
    $language = $item['language'];
    $tool = $item['tool'];

    //format the data
    $formatted_language .= '<div class="language">'.$language.'</div>';
    $formatted_content =  '<div class="item">'.$tool.'</div>';

    $languages .= $formatted_language;
    $tools .= $formatted_tools;
}
// display the collated data
echo '<section id="languages">' . $languages . '</section>';
echo '<section id="tools">' . $tools . '</section>';

There are a few things to note.

You must declare any variables you are using to store data in before and outside the loop starts. For this reason, it’s a good idea to make sure these variables are specific to this loop only, since it could inadvertently be used later on by other PHP expressions accidentally.

Also, importantly, this method stores the formatted string in PHP, so you can’t just break out of the PHP loop to write regular html and PHP thrown in. I know, it’s annoying- I hate mixing HTML inside PHP and only use it as a last resort. I think for me, this is the only downside to this method.

I think in this case the pay off is worth it though - you get better overall readability*, and you don’t have to repeat the same loop twice, meaning you don’t have to request the same data twice.

*(of course that depends on the kind of data you are displaying - for anything more complex honestly I’d just create two of the same loops with regular HTML inside. Developers always talk about DRY, but I’ve found sometimes readability actually does mean repeating yourself.)