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

Last modified 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 for the content.


The Solution

Here is a solution that you can use if you want to just use one loop to return multiple sets of data, using a method called string concatenation.

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

        //format the title so it's able to be linked to
        $urlize_title = sanitize_title($title);
        $formatted_title = "<a href='#{$urlize_title}' title='{$title}'>{$title}</a>";

        //format the content into a php/html string
        $formatted_content = "<section id='{$urlize_title}' class='tab-content'><h2>{$title}</h2>{$content}</section>";

        //concatenate the strings into new variables for future display
        $titles .= $formatted_title;
        $contents .= $formatted_content;
        ?>
    <?php endwhile; ?>
<!-- Now that we're out of the loop, let's put the compiled data inside our wrappers -->
    <div class="cell panel-links">
        <?php echo $titles;?>
    </div>

    <div class="cell panel-content">
        <?php echo $contents; ?>
    </div>

<?php endif; ?>

This might look like a lot to take in but all we’re doing is storing data as the loop runs, and using that saved data to display it after the loop has finished running.

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

Each time the loop runs, it’ll concatenate the title and content onto inside the declared variables.

Once the loop has finished running through all the rows, you’re free to use the declared variables outside the loop as you wish.

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 namespaced or specific to this loop only, since it could inadvertently be used later on by other PHP expressions accidentally.

And if you have a lot of markup?? No problem!! You can use heredoc to concatenate multiline HTML and PHP with ease:

<?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>";

    // using heredoc allows you to intersperse markup with php variables over multiple lines, while keeping indentation intact
    $languages .= <<<EOT
			<img src="$img_src"
			alt="$alt"
			class="icon"
			loading="lazy">
			<div class="title">$item->title</div>
			EOT;
    $tools .= $formatted_tools;
}
// display the collated data
echo '<section id="languages">' . $languages . '</section>';
echo '<section id="tools">' . $tools . '</section>';

Seriously, how awesome is that?

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 too - for anything more complex two loops may be a better option for readability’s sake. But as with anything, only you know what will work best for long-term readability. In any case, I hope you find this method helpful.

Jaclyn Tan

I'm Jaclyn and I make websites. I aim to make the web a more fun and accessible place, sharing what I learn along the way.

My blog has no ads or sponsors and I plan on keeping it that way. If you like what I do please consider supporting me.