Output Buffering in PHP

PHP makes it very easy to include everything from interpreting form submissions to calls to the database to rendering HTML in one file. As a project grows, this can make code difficult to test and update.

In this post, I’ll discuss an approach to separating application logic from template files using output buffering, and how that can help to make your code easier to work with.

The Problem

When not using a framework, it’s not uncommon for a single PHP file to include both a lot of logic and a lot of HTML to render the result of the logic. For example, a file could define functions, make queries to the database, and render the page’s body.

But to create a unit-testable application, it’s beneficial to isolate different pieces of the app—e.g. a repository layer for calls to the database, model classes representing data, and view layers that are primarily HTML.

Separating Logic from Templates

My goal here is to separate logic that provides the data to be rendered from a template file that renders the data with HTML. This separation allows for reusability of the template in the event that it could be fed from multiple data sources, and allows for simpler testing of the logic without the HTML being printed. Let’s take a look at a simple example that renders a table of users.

Template

Here’s the code for a template file. Notice that the only logic is a foreach and simple short tags. Otherwise the file is just HTML.


    <div class="user-list">
        <table>
            <tr>
                <th>index</th>
                <th>user name</th>
            </tr>
            <? foreach ($this->getUserNames() as $index => $userName) { ?>
                <tr>
                    <td><?= $index ?></td>
                    <td><?= $userName ?></td>
                </tr>
            <? } ?>
        </table>
    </div>

Backing Class

And here’s the file for the backing class. Notice that it is only a class, with no HTML. It’s easy to write unit tests against the class using a library like phpunit. An interesting peice here is the render method, which makes use of ob_start and ob_get_clean(). That’s the magic that holds things together. We’ll take a look at that next.


    <?

    class UserList {
        public function getUserNames() {
            // hard-coded for simplicity
            // in practice, this could query the database, call into a repository class, etc
            return [
                'Alice',
                'Bob'
            ];
        }
    
        public function __toString() {
            return $this->render();
        }
    
        public function render() {
            ob_start();
            include('path/to/user-list.template.php'); // or wherever your template is located
            return ob_get_clean();
        }
    }
    
    >?

Output Buffering

Output buffering allows for any output (e.g. echo statements and rendered HTML) to be stored to a separate buffer instead of being directly output. There are a number of functions related to output buffering, but the two we’ll use are ob_start and ob_get_clean.

  • ob_srtart begins output buffering. We next include the template file. Its output is rendered to the buffer.
  • ob_get_clean does two things: it gets the contents of the buffer as a string, and it cleans out the buffer. When the string is returned, it can be output elsewhere in the application.

Putting it Together

Next, let’s take a look at including our UserList in another file. Imagine we have a page that renders a bunch of other components, and we wish to include the users list. We could do something like this:


    <?

    require_once 'presenters/user-list.php'; // or whatever file your UserList class is in

    ?>

    <html>
        <body>
            <!-- other html before the user list -->
            <?= new UserList($userRepository) ?>
            <!-- other html after the user list -->
        </body>
    </html>

One cool feature here is that __toString() calls into the render method, so there’s no need to directly call render.

Conversation
  • Arion says:

    Great post Tyler, Yes many times it difficult to distinguish the codes. it often making confuse to me and as I m a noob in this field. It really helpful thanks a lot.

  • Comments are closed.