How to write a Controller in WordPress?

Route in WordPress

What happens when we enter a URL to visit a WordPress website?

Let’s take a look at the .htaccess file of WordPress:

# BEGIN WordPress

RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

# END WordPress

As you can see, all URLs will be redirected to the file index.php:

<?php
/**
 * Front to the WordPress application. This file doesn't do anything, but loads
 * wp-blog-header.php which does and tells WordPress to load the theme.
 *
 * @package WordPress
 */

/**
 * Tells WordPress to load the WordPress theme and output it.
 *
 * @var bool
 */
define( 'WP_USE_THEMES', true );

/** Loads the WordPress Environment and Template */
require __DIR__ . '/wp-blog-header.php';

After that, it will load the file wp-blog-header.php:

<?php
/**
 * Loads the WordPress environment and template.
 *
 * @package WordPress
 */

if ( ! isset( $wp_did_header ) ) {

    $wp_did_header = true;

    // Load the WordPress library.
    require_once __DIR__ . '/wp-load.php';

    // Set up the WordPress query.
    wp();

    // Load the theme template.
    require_once ABSPATH . WPINC . '/template-loader.php';

}

We have 3 steps here:

  1. Load wp-load.php: this step also loads the file wp-config.php for all defined constants
  2. Call the function wp()
  3. Load wp-includes/template-loader.php

The function wp() will set up the WordPress query:

function wp( $query_vars = '' ) {
    global $wp, $wp_query, $wp_the_query;

    $wp->main( $query_vars );

    if ( ! isset( $wp_the_query ) ) {
        $wp_the_query = $wp_query;
    }
}

In the file template-loader.php, we need to pay attention to:

if ( wp_using_themes() ) {

    $tag_templates = array(
        'is_embed'             => 'get_embed_template',
        'is_404'               => 'get_404_template',
        'is_search'            => 'get_search_template',
        'is_front_page'        => 'get_front_page_template',
        'is_home'              => 'get_home_template',
        'is_privacy_policy'    => 'get_privacy_policy_template',
        'is_post_type_archive' => 'get_post_type_archive_template',
        'is_tax'               => 'get_taxonomy_template',
        'is_attachment'        => 'get_attachment_template',
        'is_single'            => 'get_single_template',
        'is_page'              => 'get_page_template',
        'is_singular'          => 'get_singular_template',
        'is_category'          => 'get_category_template',
        'is_tag'               => 'get_tag_template',
        'is_author'            => 'get_author_template',
        'is_date'              => 'get_date_template',
        'is_archive'           => 'get_archive_template',
    );
    $template      = false;

    // Loop through each of the template conditionals, and find the appropriate template file.
    foreach ( $tag_templates as $tag => $template_getter ) {
        if ( call_user_func( $tag ) ) {
            $template = call_user_func( $template_getter );
        }

        if ( $template ) {
            if ( 'is_attachment' === $tag ) {
                remove_filter( 'the_content', 'prepend_attachment' );
            }

            break;
        }
    }

    if ( ! $template ) {
        $template = get_index_template();
    }

    /**
     * Filters the path of the current template before including it.
     *
     * @since 3.0.0
     *
     * @param string $template The path of the template to include.
     */
    $template = apply_filters( 'template_include', $template );
    if ( $template ) {
        include $template;
    } elseif ( current_user_can( 'switch_themes' ) ) {
        $theme = wp_get_theme();
        if ( $theme->errors() ) {
            wp_die( $theme->errors() );
        }
    }
    return;
}

Important! The route in WordPress will be defined here:

foreach ( $tag_templates as $tag => $template_getter ) {
    if ( call_user_func( $tag ) ) {
        $template = call_user_func( $template_getter );
    }

    if ( $template ) {
        if ( 'is_attachment' === $tag ) {
            remove_filter( 'the_content', 'prepend_attachment' );
        }

        break;
    }
}

The condition function call_user_func( $tag ) will check what template is being called in the current request. The data will be handled by reading URL structure and converting to the template data in the global variable $wp_query. The value of $tag param can be “is_front_page”, “is_single”, “is_category”…

Next, we will take a look at the functions below:

function get_front_page_template() {
    $templates = array( 'front-page.php' );

    return get_query_template( 'frontpage', $templates );
}
function get_single_template() {
    $object = get_queried_object();

    $templates = array();

    if ( ! empty( $object->post_type ) ) {
        $template = get_page_template_slug( $object );
        if ( $template && 0 === validate_file( $template ) ) {
            $templates[] = $template;
        }

        $name_decoded = urldecode( $object->post_name );
        if ( $name_decoded !== $object->post_name ) {
            $templates[] = "single-{$object->post_type}-{$name_decoded}.php";
        }

        $templates[] = "single-{$object->post_type}-{$object->post_name}.php";
        $templates[] = "single-{$object->post_type}.php";
    }

    $templates[] = 'single.php';

    return get_query_template( 'single', $templates );
}

The view files will be loaded by the function get_query_template() with 2 params: $type and $templates.

Next, in the function get_query_template(), we will see:

$template = locate_template( $templates );
$template = locate_block_template( $template, $type, $templates );

The function locate_template() will retrieve the name of the highest priority template file that exists. It searches in the STYLESHEETPATH before TEMPLATEPATH and wp-includes/theme-compat so that themes which inherit from a parent theme can just overload one file. This function will return the template filename (including its path) if one is located.

Finally, the template will be load or an error message will be displayed (at the end of file template-loader.php):

$template = apply_filters( 'template_include', $template );
if ( $template ) {
    include $template;
} elseif ( current_user_can( 'switch_themes' ) ) {
    $theme = wp_get_theme();
    if ( $theme->errors() ) {
        wp_die( $theme->errors() );
    }
}
return;

WordPress Template Hierarchy

The following diagram shows which template files are called to generate a WordPress page based on the WordPress template hierarchy.

WordPress uses the query string to decide which template or set of templates should be used to display the page. The query string is information that is contained in the link to each part of your website.

Put simply, WordPress searches down through the template hierarchy until it finds a matching template file. To determine which template file to use, WordPress:

  • Matches every query string to a query type to decide which page is being requested (for example, a search page, a category page, etc);
  • Selects the template in the order determined by the template hierarchy;
  • Looks for template files with specific names in the current theme’s directory and uses the first matching template file as specified by the hierarchy.

With the exception of the basic index.php template file, you can choose whether you want to implement a particular template file or not.

If WordPress cannot find a template file with a matching name, it will skip to the next file in the hierarchy. If WordPress cannot find any matching template file, the theme’s index.php file will be used.

Read more about WordPress template hierarchy:

Create Controller Class to handle data

My idea is that we will create a Controller PHP class to handle all data and save it to the global variable $wp_query to use in the template files. Centralizing data processing will help eliminate unnecessary processing functions or loops if we process data in each individual template file.

For example, I have a theme with the directory structure below:

my-theme/
    assets/
    inc/
        controllers/
        theme-options/
        vendors/
        theme-constants.php
        theme-helpers.php
        theme-setup.php
    category.php
    front-page.php
    functions.php
    home.php
    index.php
    single.php
    style.css

Next, we will create the file Theme_Controller.php in inc/controllers/ to initialize the view property in the global variable $wp_query:

<?php
namespace Theme\Controllers;

class Controller {
    public function __construct() {
        global $wp_query;
        $wp_query->view = new \stdClass();
    }
}

Next, we will create the file Theme_Front_Page.php in inc/controllers/ to handle data for the front page template:

<?php
namespace Theme\Controllers;

use Theme\Controllers\Controller;

class Theme_Front_Page extends Controller {

    public function __construct() {
        parent::__construct();
        add_action( 'template_redirect', [$this, 'get_data'], 99 );
    }

    public function get_data() {
        global $wp_query;
        if( is_front_page() ) {

            /** We will handle all data here */
            $data = new \stdClass();
            $data->title = 'Front Page';

            $wp_query->view->front_page = $data;
        }
    }
}

new Theme_Front_Page();

Similarly, we also create controllers for other templates:

my-theme/
    inc/
        controllers/
            Theme_Controller.php
            Theme_Front_Page.php
            Theme_Single.php
            Theme_Category.php
            Theme_Page.php

Remember to include all controllers in the file functions.php:

include __DIR__ . '/inc/controllers/Theme_Controller.php';
include __DIR__ . '/inc/controllers/Theme_Front_Page.php';
include __DIR__ . '/inc/controllers/Theme_Single.php';
include __DIR__ . '/inc/controllers/Theme_Category.php';
include __DIR__ . '/inc/controllers/Theme_Page.php';

Using Controller data in the front-end template

Now, we can access the front page data in the global variable $wp_query in the file front-page.php:

<?php
get_header();

global $wp_query;
$data = property_exists( $wp_query->view, 'front_page' ) ? $wp_query->view->front_page : null;

if( $data ) {
    // Handle the HTML template here
}

get_footer();

Or in the file single.php:

<?php
get_header();

global $wp_query;
$data = property_exists( $wp_query->view, 'single' ) ? $wp_query->view->single : null;

if( $data ) {
    // Handle the HTML template here
}

get_footer();