# 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](https://secure.php.net/manual/en/language.basic-syntax.phptags.php), write your HTML, reopen your PHP tag and so on. This combination of HTML and code isn't ideal. You've [coupled](https://carlalexander.ca/coupling-cohesion-wordpress/) 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](https://carlalexander.ca/mastering-php-conditionals/) 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](https://codex.wordpress.org/Shortcode)! WordPress added shortcodes as [macros](https://en.wikipedia.org/wiki/Macro_(computer_science)) 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](https://codex.wordpress.org/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](https://en.wikipedia.org/wiki/Guard_(computer_science)). The first one ensures that [we have an array](https://secure.php.net/manual/en/function.is-array.php) and that [it has a value set](https://secure.php.net/manual/en/function.isset.php) for the `id` key. The second guard clause checks if the value for the `id` key is [a number or numeric string](https://secure.php.net/manual/en/function.is-numeric.php). You might have also noticed that the guard clauses also return an error text. We formatted it as an [HTML comment](https://en.wikipedia.org/wiki/HTML_element#Comments). 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`](https://codex.wordpress.org/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`](https://secure.php.net/manual/en/language.operators.type.php) 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](https://carlalexander.ca/php-string-formatting/) 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](https://carlalexander.ca/importance-naming-programming/). 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`](https://codex.wordpress.org/Function_Reference/esc_html_e) function. You should always ensure to [escape your output](https://codex.wordpress.org/Validating_Sanitizing_and_Escaping_User_Data#Escaping:_Securing_Output) like this. If you expect your output to contain HTML, you should use the [`wp_kses`](https://codex.wordpress.org/Function_Reference/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](https://secure.php.net/manual/en/book.outcontrol.php). 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`](https://secure.php.net/manual/en/function.ob-start.php) 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`](https://secure.php.net/manual/en/function.include.php) 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`](https://secure.php.net/manual/en/function.ob-get-clean.php) 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](https://codex.wordpress.org/Glossary#Filter). This would let a developer change the template that our class includes using the [plugin API](https://carlalexander.ca/wordpress-adventurous-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`](https://developer.wordpress.org/reference/functions/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`](https://secure.php.net/manual/en/function.is-readable.php). 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`](https://codex.wordpress.org/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](https://codex.wordpress.org/Child_Themes) 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`](https://secure.php.net/manual/en/function.empty.php) 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);
    }
}
```