Want to learn object-oriented programmming? Get my free course

Designing a class to generate HTML content in WordPress

As WordPress developers, we often have to generate HTML content for our plugins. (And, if you’re building a theme, that’s pretty much mandatory!) But generating HTML inside a function or method is messy. You have to close your PHP tag, write your HTML, reopen your PHP tag and so on.

This combination of HTML and code isn’t ideal. You’ve coupled your templating to your code in a way that often doesn’t scale well. That’s why it’s common to see such functions or methods grow out of proportion.

The common cause of that is adding conditional logic into the HTML code. This increases the complexity of the code and reduces its readability. So we’d like to avoid that if possible.

It’s worth pointing out that mixing HTML and PHP isn’t bad per se. After all, PHP is also a templating language. We just want to create a proper delimitation between what is regular PHP code and what is PHP template code.

So let’s find a solution to this problem! The twist (well it’s not really a twist for this site…) is that we’re going to use object-oriented programming to do it. We’ll design a class that generates HTML using PHP template.

A shortcode example

So where can you find a lot of HTML code mixed with PHP code in WordPress? Of course, we have themes that mix the two a lot. But using object-oriented programming with themes is a complex topic that deserves more than one article.

So what other WordPress feature mixes HTML code with PHP code? Well, there’s shortcodes! WordPress added shortcodes as macros for HTML content.

Creating a WordPress shortcode often forces you to mix HTML and PHP code together. This is exactly the type of problem we want to solve with the class that we want to design. We can use it to separate the shortcode specific PHP code from the HTML generating code.

Highlighting a comment

Alright, so what’s a small problem that we could solve using a shortcode? Well, let’s say that you have a popular blog. Your blog’s popularity also means that you get a lot of comments on your posts.

But you also love comments! You often want to highlight them in your posts using a block quote. You could do it by hand, but you figure you could write a small plugin to do this. (How convenient for us!)

function myplugin_display_comment_shortcode($attributes)
{
    if (!is_array($attributes) || !isset($attributes['id'])) {
        return '<!-- Missing comment ID -->';
    } elseif (!is_numeric($attributes['id'])) {
        return '<!-- Invalid comment ID -->';
    }

    $comment = get_comment($attributes['id']);

    if (!$comment instanceof WP_Comment) {
        return '<!-- No comment found -->';
    }

    return '<blockquote>'
         . "<div>{$comment->comment_content}</div>"
         . "<div>- {$comment->comment_author}</div>"
         . '</blockquote>';
}
add_shortcode('myplugin_comment', 'myplugin_display_comment_shortcode');

Above is the code in charge of handling our shortcode for our small plugin. We’re using the shortcode API to add the myplugin_comment shortcode to WordPress. When WordPress finds our shortcode, it’ll call the myplugin_display_comment_shortcode function.

Shortcode function explained

The myplugin_display_comment_shortcode function itself is pretty small. It uses a single parameter: attributes. attributes is an array of attributes that the shortcode API passes to our function.

The function starts with two guard clauses. The first one ensures that we have an array and that it has a value set for the id key. The second guard clause checks if the value for the id key is a number or numeric string.

You might have also noticed that the guard clauses also return an error text. We formatted it as an HTML comment. That way you can then find it by viewing the source of an HTML page when debugging a page that uses the shortcode.

Once passed the first two guard clauses, it’s safe for us to get the comment we want to highlight. We do that using the get_comment function. We pass it the id that we validated using our guard clauses and it either returns a WP_Comment object or null.

We assign our fetched comment to the comment variable. We then pass it through another guard clause. This guard clause checks that we got a WP_Comment object back by using the instanceof operator.

If we don’t have a WP_Comment object, we return another error text saying that we couldn’t find the comment. Otherwise, we generate the HTML for the highlighted comment using a string and return it. That’s it!

Creating a generator class

Now that we have a working shortcode function, it’s time to break it in two! We’re going to build a class to handle the last part of the function that returned the HTML of the block quote. To begin, let’s create an empty class for our generator.

class MyPlugin_HighlightedCommentGenerator
{
}

We named our class MyPlugin_HighlightedCommentGenerator. Finding a good name for our class is always an important step. This one does a good job describing what our class does. That way, there’s no doubt about the purpose of the class.

Creating a template

Now, we’re going to put aside our MyPlugin_HighlightedCommentGenerator class for a moment. Instead of continuing to build it, we’re going to convert our earlier HTML code into a PHP template. This takes only a few seconds to do.

<?php
/**
 * Highlighted comment template.
 *
 * Available variables:
 *
 *  WP_Commment $comment
 */
?>
<blockquote>
    <div><?php esc_html_e($comment->comment_content); ?></div>
    <div>- <?php esc_html_e($comment->comment_author); ?></div>
</blockquote>

Above is what you’ll find in the PHP template file. It’s worth pointing out that this is a standalone PHP file. Our generator will use it to generate the highlighted comment HTML.

We also put a small comment block at the beginning. That’s because it’s not always obvious what variables are available in a PHP template file. But, with this comment block, a developer knows that they have access to the comment variable. (Aren’t we nice!?)

Another thing worth noting is the use of the esc_html_e function. You should always ensure to escape your output like this. If you expect your output to contain HTML, you should use the wp_kses instead.

Generating a highlighted comment

Once we have our PHP template, we can look at how to use it to generate our highlighted comment HTML. To do this, we’re going to have to leverage a PHP feature called output buffering. If you’re not familiar with output buffering, it’s a way to control the output of a PHP script. This lets you manipulate the output in various ways.

class MyPlugin_HighlightedCommentGenerator
{
    /**
     * Generates the highlighted comment HTML for the given comment.
     *
     * @param WP_Comment $comment
     *
     * @return string
     */
    public function generate(WP_Comment $comment)
    {
        ob_start();

        include __DIR__ . 'highlighted-comment.php';

        return ob_get_clean();
    }
}

Here’s one of those ways. We updated our MyPlugin_HighlightedCommentGenerator class above and added the generate method. The generate method uses output buffering to convert the HTML output of our PHP template into a string.

The method starts by calling ob_start which starts a new output buffer. It’s worth mentioning that output buffers are stackable. So this works even if there’s already an output buffer active.

With the output buffer active, we can now process our PHP template. We do this by using the include statement. This statement tells PHP to include and evaluate the highlighted-comment.php file. (This is the name that we gave to our PHP template.)

Including our template in this way will cause PHP to generate the highlighted comment HTML. But, since output buffering is on, it’ll get stored in it instead. The only thing left is to convert the HTML in the output buffer into a string.

That’s what the ob_get_clean function does. It fetches all the content stored in the current output buffer and returns it as a string. It then deletes the output buffer. (You always need to delete the output buffer once you’re done with it.)

Improving our generator class

Now, we could just stop here and leave our MyPlugin_HighlightedCommentGenerator class as is. It does solve the problem that we had identified earlier. In practice, we wouldn’t need to do anything else.

That said, our current MyPlugin_HighlightedCommentGenerator class is a bit barebones. It might not feel like we improved things in a meaningful way by creating this class. And you wouldn’t be wrong to feel that way either.

So let’s look at some improvements that we can make to our MyPlugin_HighlightedCommentGenerator class. There are a few things that we can do. And this should help make the class feel more useful.

Custom PHP templates

Earlier we created a PHP template that we use to generate the HTML of our highlighted comment. But a developer might want to make changes to it for the project that they’re working on or for some other reason. How can we help them do this?

Adding a filter

Well, we could add a filter. This would let a developer change the template that our class includes using the plugin API. Here’s one way we could do it:

class MyPlugin_HighlightedCommentGenerator
{
    /**
     * Generates the highlighted comment HTML for the given comment.
     *
     * @param WP_Comment $comment
     *
     * @return string
     */
    public function generate(WP_Comment $comment)
    {
        $template_path = apply_filters('myplugin_highlighted_comment_template_path', __DIR__ . 'highlighted-comment.php');

        if (!is_readable($template_path)) {
            return sprintf('<!-- Could not read "%s" file -->', $template_path);
        }

        ob_start();

        include $template_path;

        return ob_get_clean();
    }
}

As you can see, we made a few changes to our generate method. First, we added a template_path variable. The include statement now uses it to include and evaluate a PHP template for our highlighted comment.

The template_path variable gets assigned the value returned by the call to the apply_filters function. We pass two values to it. There’s the myplugin_highlighted_comment_template_path string which is the name of our filter. And then there’s the path to highlighted-comment.php which is our default template.

With this filter, another developer can now modify the template that we use. But this comes with increased responsibility. Someone might replace our default template path with an invalid template path. They might not mean to (we all make mistakes), but we should still plan for this eventuality.

That’s why we also added this guard clause after our call to the apply_filters function. It checks if we can read the template path stored in the template_path variable using the is_readable. We return a special HTML comment block if the file isn’t readable.

Using a theme template file

But adding a filter isn’t the only thing that we can do to support custom PHP templates. We can also allow theme developers to customize the HTML our highlighted comment. The best part is that they don’t even need to use the filter that we created.

The get_query_template function is what makes this possible. It’s a function that retrieves the location of a template file in a theme without the need of an extension. It can even detect if you’re using a child theme and locate the correct template file.

class MyPlugin_HighlightedCommentGenerator
{
    /**
     * Generates the highlighted comment HTML for the given comment.
     *
     * @param WP_Comment $comment
     *
     * @return string
     */
    public function generate(WP_Comment $comment)
    {
        $template_path = get_query_template('myplugin-highlighted-comment');

        if (empty($template_path)) {
            $template_path = __DIR__ . 'highlighted-comment.php';
        }

        ob_start();

        include $template_path;

        return ob_get_clean();
    }
}

Above is the generate method from our MyPlugin_HighlightedCommentGenerator class. We removed the code using the apply_filters function from the previous section. (But don’t worry, it’ll be back soon!) We replaced it with code that uses the get_query_template function.

We start by calling the get_query_template function. We pass it myplugin-highlighted-comment as the argument for the template name. This tells the get_query_template function to look for a template named myplugin-highlighted-comment.

We assign the return value of the call to the get_query_template function to the template_path variable. If it couldn’t find a template using our template name, the get_query_template function will return an empty string. That’s why added a guard clause after our call to the get_query_template function.

It checks if the template_path variable is an empty string by using the empty function. If the empty check is true, we assign the path of our default template to the template_path variable. This is the highlighted-comment.php template file that we saw earlier.

Combining the two

At this point, we can look at combining the code that uses a filter with the code that uses a theme template file. That way, both a plugin developer and a theme developer can customize the output of our MyPlugin_HighlightedCommentGenerator class. Let’s look at one way that we can do this.

class MyPlugin_HighlightedCommentGenerator
{
    /**
     * Generates the highlighted comment HTML for the given comment.
     *
     * @param WP_Comment $comment
     *
     * @return string
     */
    public function generate(WP_Comment $comment)
    {
        $template_path = $this->get_template_path();

        if (!is_readable($template_path)) {
            return sprintf('<!-- Could not read "%s" file -->', $template_path);
        }

        ob_start();

        include $template_path;

        return ob_get_clean();
    }

    /**
     * Get the path of PHP template that the comment generator will use.
     *
     * @return string
     */
    private function get_template_path()
    {
        $template_path = get_query_template('myplugin-highlighted-comment');

        if (empty($template_path)) {
            $template_path = __DIR__ . 'highlighted-comment.php';
        }

        return apply_filters('myplugin_highlighted_comment_template_path', $template_path);
    }
}

The biggest change from what we’ve seen so far is that we created a new method called get_template_path. Its job is to get the template path for our generate method. It’s also where we combine most of the code from the earlier two examples.

The method starts with a call to the get_query_template function to check for a custom theme template file. We store the path to that custom template file in the template_path variable.

We then check if the get_query_template function returned an empty string. If it did, we assign the path to our default template to the template_path variable. The get_template_path method finishes by returning the template_path variable. But not without passing it through the apply_filters function first.

Meanwhile, the generate method is a bit simpler now. It starts by making a call to our new get_template_path method. It assigns the value that the get_template_path method returns to the template_path variable.

The generate method kept the is_readable guard clause from earlier. But why did we leave it in the generate method instead of putting it the get_template_path method? It’s because we still want to return the error text when we can’t read the file that our template_path variable points to.

The rest of the generate method hasn’t changed from all our other examples. We call ob_start function to start the output buffering. We then include the template that’s in the template_path variable. We finish by returning the HTML returned by the ob_get_clean function.

Generic comment generator class

Our MyPlugin_HighlightedCommentGenerator class is starting to look quite nice! Plugin developers can customize the highlighted comment template by using a filter. Theme developers can use a theme template file.

But, if we look at the code that we have right now, it’s pretty generic. There isn’t that much of it that’s specific to generating a highlighted comment. So what can we do about that?

Well, we could try to embrace that idea and make our class more generic. That way we could use it to generate HTML from WP_Comment object in different scenarios. So let’s do that!

What’s specific to highlighted comments?

So what do we have in the MyPlugin_HighlightedCommentGenerator class that’s specific to highlighted comments? We can narrow it down to three things. You have the:

  1. myplugin-highlighted-comment string passed to the get_query_template function.
  2. default template_path if the get_query_template function returns an empty string.
  3. myplugin_highlighted_comment_template_path string passed to the apply_filters function.

We can make these three things dynamic within our class. The easiest way to do it is by using a constructor. We pass these values to it and assign them to internal variables like this:

class MyPlugin_CommentGenerator
{
    /**
     * Path to the default template used by the highlighted comment generator.
     *
     * @var string
     */
    private $default_template_path;

    /**
     * Name of the filter used to filter the template path.
     * 
     * @var string
     */
    private $filter_name;

    /**
     * Template name used by the `get_query_template` function.
     * 
     * @var string
     */
    private $query_template_name;

    /**
     * Constructor.
     *
     * @param string $default_template_path
     * @param string $filter_name
     * @param string $query_template_name
     */
    public function __construct($default_template_path, $filter_name, $query_template_name)
    {
        $this->default_template_path = $default_template_path;
        $this->filter_name = $filter_name;
        $this->query_template_name = $query_template_name;
    }

    // ...
}

First, we renamed our class to MyPlugin_CommentGenerator to represent its new generic nature. We also added a constructor and three internal variables: default_template_path, filter_name and query_template_name. We then use these three values in the get_template_path method:

class MyPlugin_CommentGenerator
{
    // ...

    /**
     * Get the path of PHP template that the comment generator will use.
     *
     * @return string
     */
    private function get_template_path()
    {
        $template_path = get_query_template($this->query_template_name);

        if (empty($template_path)) {
            $template_path = $this->default_template_path;
        }

        return apply_filters($this->filter_name, $template_path);
    }
}

As you can see above, these three variables replace the string values that we were using earlier. in the get_template_path method. This is what allows for the MyPlugin_CommentGenerator to be dynamic. To replicate the MyPlugin_HighlightedCommentGenerator class that we had earlier, you just need to instantiate the MyPlugin_CommentGenerator class like this:

$comment_generator = new MyPlugin_CommentGenerator(
    __DIR__ . 'highlighted-comment.php', 
    'myplugin_highlighted_comment_template_path', 
    'myplugin-highlighted-comment'
);

Going back to our shortcode function

The only thing left is to go back and fix our initial myplugin_display_comment_shortcode function. We want to use our MyPlugin_CommentGenerator class to generate the HTML. This means replacing our string with a call to our generate method like this:

function myplugin_display_comment_shortcode($attributes)
{
    if (!is_array($attributes) || !isset($attributes['id'])) {
        return '<!-- Missing comment ID -->';
    } elseif (!is_numeric($attributes['id'])) {
        return '<!-- Invalid comment ID -->';
    }

    $comment = get_comment($attributes['id']);

    if (!$comment instanceof WP_Comment) {
        return '<!-- No comment found -->';
    }

    $comment_generator = new MyPlugin_CommentGenerator(
        __DIR__ . 'highlighted-comment.php',
        'myplugin_highlighted_comment_template_path',
        'myplugin-highlighted-comment'
    );

    return $comment_generator->generate($comment);
}
add_shortcode('myplugin_comment', 'myplugin_display_comment_shortcode');

Mission accomplished!

At this point, our MyPlugin_CommentGenerator class is looking pretty good! You can use it for our initial use case of generating HTML for a highlighted comment. But you can also use it to generate HTML for other types of comments as well.

On top of that, we also managed to achieve what we’d set out to do at the beginning of the article. We separated the regular PHP code from the PHP template code. This reduced the coupling that we had in our code at first. You can find the complete code below.

class MyPlugin_CommentGenerator
{
    /**
     * Path to the default template used by the highlighted comment generator.
     *
     * @var string
     */
    private $default_template_path;

    /**
     * Name of the filter used to filter the template path.
     *
     * @var string
     */
    private $filter_name;

    /**
     * Template name used by the `get_query_template` function.
     *
     * @var string
     */
    private $query_template_name;

    /**
     * Constructor.
     *
     * @param string $default_template_path
     * @param string $filter_name
     * @param string $query_template_name
     */
    public function __construct($default_template_path, $filter_name, $query_template_name)
    {
        $this->default_template_path = $default_template_path;
        $this->filter_name = $filter_name;
        $this->query_template_name = $query_template_name;
    }

    /**
     * Generates the highlighted comment HTML for the given comment.
     *
     * @param WP_Comment $comment
     *
     * @return string
     */
    public function generate(WP_Comment $comment)
    {
        $template_path = $this->get_template_path();

        if (!is_readable($template_path)) {
            return sprintf('<!-- Could not read "%s" file -->', $template_path);
        }

        ob_start();

        include $template_path;

        return ob_get_clean();
    }

    /**
     * Get the path of PHP template that the comment generator will use.
     *
     * @return string
     */
    private function get_template_path()
    {
        $template_path = get_query_template($this->query_template_name);

        if (empty($template_path)) {
            $template_path = $this->default_template_path;
        }

        return apply_filters($this->filter_name, $template_path);
    }
}
Creative Commons License