Software Engineering in WordPress, PHP, and Backend Development

Author: Tom (Page 11 of 426)

Using Custom Templates with Rewrite Rules in WordPress Plugins

Assume you’re working on a custom WordPress plugin that includes its own template for rendering data. The difference in this template is that it’s not one that you’ll apply to a page or custom post type in WordPress, but it will instead be its own page with access to the WordPress core functions.

This works well enough for a plugin, right? 🙂. Credit.

For example, say you’re generating a report for a given user ID and you want the page to incorporate fonts from Google, perhaps a third-party library, and it look as if it’s not even part of the WordPress core application.

The head element of the page may look something like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><?php echo getName($userId); ?></title>
    <link rel="stylesheet" href="<?php echo getAsset("/lib/reset.css"); ?>">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Sans">
  </head>

But you there are two other requirements:

  1. You want to access the report through a pretty link that includes the $userId (perhaps something like /information/42
  2. You want to use core functions for getting user metadata through functions like get_user_meta.

To do this, you need to have custom templates with rewrite rules in your WordPress plugin. Again, this is not a custom template in the WordPress sense and you’ll only have as many rewrite rules as you do templates.

Custom Templates with Rewrite Rules

1. The Plugin Structure

This can be structured however you want, but I find it easiest to have the rules set in their own file that’s included in the plugin’s bootstrap file or in the core plugin’s file.

I also think it’s easiest to keep the template in its own template directory where you can also house other templates. The main reason for this being that if you have custom rewrite rules, each rule can map to its own template.

In the example I’m going to give throughout this article, I’m going to have the rules located in the plugin bootstrap file which I’m calling plugin.php and I’m going to have the template in a directory called templates. The template will be called information.php so that it maps to the permalink structure I’m going to set up.

So aim to have:

  • plugin.php in the root of your plugin
  • templates/information.php in the root of your plugin

Note that the information.php template does not need to include the standard WordPress template headers because this isn’t that type of template.

2. The Template

This template can be as involved and complicated or as simple as you like. For the purposes of this post and to keep the code easy to follow, I’m just going to render user’s first name and last name from the user metadata table.

For example:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><?php echo getName($userId); ?></title>
    <link rel="stylesheet" href="<?php echo getAsset("/lib/reset.css"); ?>">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Sans">
    <link rel="stylesheet" href="<?php echo getAsset("index.css"); ?>">
  </head>
  <body>
    <div id="container">
        <header>
            <h1>Hello World</h1>
        </header>
        <main>
            <?php echo get_user_meta($userId, 'first_name', true) . ' ' . get_user_meta($userId, 'last_name', true); ?>
        </main>
        <footer>
            <p>&copy; <?php echo date('Y'); ?> Tom McFarlin</p>
        </footer>
    </div><!-- #container -->
  </body>
</html>

Obviously, though, you can mark this up however you find necessary. You can even incorporate additional functions that are in plugin.php or whatever file you want to incorporate additional logic to add functionality to the template.

You may see that I have a getAsset function in the head element of this page. This is what that function looks like in plugin.php:

function getAsset(string $filename)
{
    if (0 < stripos($filename, '.css')) {
        return trailingslashit(plugins_url('acme-info')) . "/assets/css/$filename";
    }

    if (0 < stripos($filename, '.js')) {
        return trailingslashit(plugins_url('acme-info')) . "/assets/js/$filename";
    }
}

This is useful for that I’m doing in a template like this but additional functionality and the reason behind doing this is outside the scope of this article. I’m showing that it can be done.

3. The Rewrite Rules

To set up the custom rewrite rules, you’ll need three functions:

First, set up the rewrite rule whenever the plugin is activated so that it persists as long as the plugin is activated. To do this, set up a function that looks like this:

register_activation_hook(__FILE__, function () {
    add_rewrite_rule(
        '^information/([0-9]+)/?',
        'index.php?user_id=$matches[1]',
        'top'
    );

    flush_rewrite_rules();
});

This will add a rewrite rule whenever the plugin is activated to that you can use information/42 and have it passed to the WordPress core application with the digit representing the ID for the user account you want to retrieve.

We also flush the rewrite rules when the plugin is activated so the new rules take affect.

Next, set up the deactivation hook so the custom rewrite rule is removed and is flushed from the WordPress application so no lingering functionality from the plugin works:

register_deactivation_hook(__FILE__, function () {
    flush_rewrite_rules();
});

Finally, set up the functionality so that it does the following:

  1. Filters the REQUEST_URI and sanitizes the URL,
  2. Separates the URL into parts by running explode on the URL
  3. Verifying the URL is properly structured
  4. Redirecting to the custom information.php template
  5. Handling errors if the file doesn’t exists, the user ID is invalid, or the URL is invalid.

It’s a lot of code for stronger functionality (hence my post on using ChatGPT for writing more secure code) but it should be clear enough given the notes above.

add_action('template_redirect', function () {
    $requestUri = filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL);
    $requestUriParts = array_values(array_filter(explode('/', $requestUri)));

    if (
        count($requestUriParts) === 2 &&
        strtolower($requestUriParts[0]) === 'report' &&
        ctype_digit($requestUriParts[1])
    ) {
        $userId = intval($requestUriParts[1]);

        // Validate $userId and ensure it's within an appropriate range
        if ($userId > 0 && $userId <= 1000000) { // Adjust the upper limit as needed
            $templatePath = plugin_dir_path(__FILE__) . 'templates/information.php';

            // Check if the template file exists before including it
            if (file_exists($templatePath)) {
                include $templatePath;
                exit;
            } else {
                // Handle the case where the template file is missing
                die('Report template not found.');
            }
        } else {
            // Handle invalid user IDs
            die('Invalid user ID.');
        }
    } else {
        // Handle invalid URLs
        die('Invalid URL.');
    }
});

At this point, assuming you’re trying to access https://yourEnvironment.com/information/42 then you’ll be redirected to the custom template outlined in the second step above.

Notice that you’re still able to read the userId and you’re able to use built-in WordPress functions to render whatever information you deem necessary for your solution.

References

Is It Worth Using ChatGPT to Help Secure Source Code?

To cut straight to the point, I’m not going to say we need to rely on AI tools to help us to write truly secure code. This isn’t to say we can use tools like ChatGPT to help secure source code, but given how these utilities are trained via their LLMs, there’s only so much they can provide. In short, don’t short change security analysts who are, by nature, trained in this very thing.

That said, I’ve been using tools such as ChatGPT and other AI developer tools to help make recommendations on making code more secure. I’ll give a basic example of what I provided, what it produced, and my thoughts on it.

Note this is but a single, isolated, and simple case so there’s only so much that it can recommend. The bottom line, for me, is not so much the code it recommended but how it offered to change that I’d originally written (and then researching the why it opted to use certain function calls over others).


The Intent of the Code

In the function I’m going to share, the purpose is to look at data in the REQUEST_URI of the $_SERVER array, separate the components of the URI, and then analyze them to determine what best course of action to take.

Namely, do we break it a part and redirect to another page or do we duck out of the functionality because it’s malformed or doesn’t fit with the scheme of the rewrite rule I have in place.

The Code

The Initial Function

I wrote the initial function like this:

add_action('template_redirect', function() {
  $requestUri = $_SERVER['REQUEST_URI'];
  $requestUriParts = array_filter(explode('/', $requestUri));

  if (
    count($requestUriParts) === 2 && 
    $requestUriParts[0] === 'report' && 
    is_numeric($requestUriParts[1])) {
        $userId = intval($requestUriParts[1]);
        include plugin_dir_path(__FILE__) . 'templates/report.php';
        exit;
    }
});

Granted, I made some concessions to keep it relatively simple. That is, there are no exception handling and there are no conditionals to handle the case when indexes of the array are invalid.

But part of this is to see just how much AI can improve the code above. They key things to take away, despite it not handling situations outside of the conditional, are:

  • I’m verifying the array has the proper number of parts,
  • The first index of the array is report,
  • The second index of the array is an integer,
  • And if all of the above is true, then we’ll redirect to a custom template.

Again, there are some immediate issues that you can spot with this (such as the case of strings, the range of integers for user IDs, and if the file even exists.

But I digress. This is what I fed into ChatGPT to help secure source code.

The Secured Code

Given the code above, this is what ChatGPT produced:

add_action('template_redirect', function () {
    $requestUri = filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL);
    $requestUriParts = array_values(array_filter(explode('/', $requestUri)));

    if (
        count($requestUriParts) === 2 &&
        strtolower($requestUriParts[0]) === 'report' &&
        ctype_digit($requestUriParts[1])
    ) {
        $userId = intval($requestUriParts[1]);

        // Validate $userId and ensure it's within an appropriate range
        if ($userId > 0 && $userId <= 1000000) { // Adjust the upper limit as needed
            $templatePath = plugin_dir_path(__FILE__) . 'templates/report.php';

            // Check if the template file exists before including it
            if (file_exists($templatePath)) {
                include $templatePath;
                exit;
            } else {
                // Handle the case where the template file is missing
                die('Report template not found.');
            }
        } else {
            // Handle invalid user IDs
            die('Invalid user ID.');
        }
    } else {
        // Handle invalid URLs
        die('Invalid URL.');
    }
});

Some of the things that immediately jump out at me in terms of improving the code are:

  • FILTER_SANITIZE_URL
  • strtolower on the report component of the URL
  • ctype_digit rather than is_numeric
  • Verifying a proper range of user IDs
  • Verifying the template file exists
  • Handling each case where it could fail

Now in terms of security, I don’t know where this would fall given that it’s not writing or reading data so much as sanitizing and validating it before redirecting a user to a page that should exist.

But I did like the steps that it took as they are things that we should be implementing naturally as engineers. Namely, sanitizing URL, verifying files exists, and making sure ID ranges are acceptable.

This is what struck me as the most interesting part though:

  • is_numeric. Determines if the given variable is a number or a numeric string. (A PHP string is considered numeric if it can be interpreted as an int or a float.).
  • ctype_digit. Checks if all of the characters in the provided string, text, are numerical.

Given the definitions above, we can verify that is_numeric(-5) would return true where ctype_digit(-5) would return false. Further, is_numeric(5.5) will be true and ctype_digit(5.5) will be false. This is important, especially when you’re working with non-negative whole numbers such as those that represent user IDs in a system such as WordPress.


I’m not recommending writing lazy code (like my example code above 🙃), feeding it into an AI system, and letting it do work for you. But if you’ve written something as strong and secure as you believe you can, then feeding that to an AI makes more sense as it can help take you a little further. And if you have a security analyst on your team, don’t hesitate to reach out to them for a code review.

For all the talk of AI replacing humans, we’re there yet – not in this field. But that’s not a discussion I care to have right now. If nothing else, using AI tools such as GitHub Copilot and ChatGPT to help secure source code isn’t a bad idea, but it’s not the best idea and it doesn’t replace someone who’s on your team. AI is going to be truly limited by its contextual knowledge of the environment and constraints of the system.

If anything, perhaps they are code assistants and nothing more.

WordPress Has No Templating Language (and That’s Okay!)

Most of us who have worked with WordPress for the last decade or so have lamented the lack of a templating language whenever it comes to front-end logic.

There are teams that have done this, such as Roots, and it’s admirable. It’s opinionated nature can make it incredibly easy to work with a project or more trouble than necessary. It depends on the project, the team, and so on. But I’m not here to talk about Roots. (Check it out if you have time.)

Anyway, as with most things in development, compromises can be made. Though there isn’t any native templating built into WordPress, this doesn’t mean we can’t introduce something approximating that when it comes to our frontend functionality.

First, remember that WordPress has a convention that if you prefix a function with ‘get’, then it means it’s going to retrieve the value for you, but it’s up to you to do something with the value. For example, get_the_content() will retrieve content for the post in question but it won’t actually render it. On the other hand, the_content() will retrieve the content and will render it.

I mention this because if you’re going to separate your logic in a similar way, it’s important to follow this convention (though I’m personally partial to the PSR12 way of naming functions with camelCase).

Secondly, remember that we’re now writing code in a time where it’s more than acceptable to mix multiple languages into a single file. This means that one file may include HTML, JavaScript, CSS, and maybe even some type of PHP. This isn’t how it’s always been and it isn’t how it has to be, but React ushered a lot of this into the mainstream.

All that to say, while it’s completely possible to set up your templates that look something like this:

<?php // Check $userId is set or can be retrieved in whatever way works for you. ?>
<li>Phone Number: <a href="tel:<?php echo get_user_meta($userId, 'phone_number', true) ?: 'Not Provided'; ?>"><?php get_user_meta($userId, 'phone_number', true) ?: 'Not Provided'; ?></a></li>
<li>Email Address: <a href="mailto:<?php echo strtolower(get_user_by('id', $userId)->user_email); ?>"><?php echo strtolower(get_user_by('id', $userId)->user_email); ?></a></li>

This is noisy and if you want to use those same function calls again, you’ll have to make sure they are used every single place throughout your codebase exactly as they are called here. Maybe that’s okay, maybe not.

If you’re building a large site or any application, it’s going to create more work than not, though.

You can separate the logic a bit so even though it’s not a full on templating language it still separates some of the rendering logic so the frontend isn’t so visually noisy. Plus, it allows you to update the code in one place rather than in every single template that uses it (yes, I know we could use partials or even some other smaller component of a partial that can be reused throughout the project but then you get into potential markup or design challenges with doing that).

This will make sure that the function is retrieve only the data that you need and leaves the rest of the markup and styling to be done elsewhere. Even better, it can be used in templates, in partials, or in fragments (or whatever they are called right now).

Anyway, this means that your template may look something like this:

<section id="contact-info">
    <ul>
        <li>Phone Number: <a href="tel:<?php echo getPhoneNumber($userId); ?>"><?php echo getPhoneNumber($userId); ?></a></li>
        <li>Email Address: <a href="mailto:<?php echo getEmailAddress($userId); ?>"><?php echo getEmailAddress($userId); ?></a></li>
    </ul>
</section>

And then the code behind the function call looks something like this:

/**
 * Retrieves the phone number associated with the specified user.
 *
 * @param int $user_id The ID of the user to retrieve the phone number for.
 *
 * @return string|false The phone number of the user, or false if not found.
 */
function getPhoneNumber(int $userId)
{
    return (
        get_user_meta($userId, 'phone_number', true) ?:
        'Not Provided'
    );
}

/**
 * Retrieves the email address associated with the specified user.
 *
 * @param int $user_id The ID of the user to retrieve the email address for.
 *
 * @return string|false The email address of the user, or false if not found.
 */
function getEmailAddress(int $userId)
{
    return strtolower(
        get_user_by('id', $userId)->user_email
    );
}

And though you’re not straight up calling a template in terms of using brackets and the properties of a model (which you technically could, but that’s another post), you’re at least still able to get the raw data with which you can work in your template.


Maybe the methodology discussed in this article strikes you as out of date, old, or even unfamiliar. And despite how I may have come off, I’m not necessarily against mixing languages in a single file, but I do think it’s important – in an application that has no templating language – to do what we can to incorporate a pattern of development that meets us half way.

It doesn’t require third-party dependencies and it doesn’t require a paradigm shift. Just keep the functionality required for retrieving and returning the data separate and have the presentation call said function. Then mark it up and style it however you want.

WordPress is a Foundation, Not a Framework

In 2016, I wrote a post about why WordPress is a foundation, not a framework. Though I don’t participate much on social media any more (I certainly lurk, but don’t converse), I’ll see things I’ve not thought about, things I’ve thought about, and things worth reconsidering.

And in an effort to continue writing more regularly and to revisit things I’ve previously written (because that’s healthy, right?), I thought I’d address something I recently read:

We need to stop thinking of WordPress as a CMS and start thinking of it as a framework.

There’s a litmus test as to what defines a framework and what defines a foundation

  • A framework doesn’t function until someone builds something using the tools it offers.
  • A foundation is an application that can run on its own without any additional functionality but it offers APIs that allows developers to build things on top of it.

To that end, WordPress is a foundation. Not a framework.


Note: It’s not that I don’t want to attribute the quote to the specific person out of disrespect; on the contrary, I’ve seen enough subtweeting and general unpleasant discourse online that I don’t want to spur that. It’s not about talking about the person; it’s about talking about the idea.

Yes, They’re Still Exciting: Headless WordPress Applications in 2023

In r/ProWordPress, OP asked a few questions around Headless WordPress in 2023. One question stood out the most:

Developers who develop headless WordPress sites, how are things going in 2023?

Given all that’s happened within the core WordPress application over the last few years – that is, with the Block Editor and Full Site Editing – it’s not only easy to lose sight this is functionality built into WordPress but there’s likely a portion of people onboarded into WordPress development who do almost nothing with this type of work.

Even with all of the excitement around the new editing tools and how much JavaScript has been introduced into WordPress core, building headless applications with WordPress is still something I find to be one of the most powerful aspects of working with the application.

Continue reading
« Older posts Newer posts »

© 2025 Tom McFarlin

Theme by Anders NorenUp ↑