Template files are very standard for themes and heavily used in child themes. For example, if a user wants to modify the layout of their site pages, they can simply copy page.php from their parent theme to a child theme, modify the HTML structure, and the changes will be reflected on the site. While it’s well known that template files like page.php can also be built for plugins, I feel most developers tend to avoid building them due to the complexity of building a template loader in a plugin. First I’d like to walk you through the logic of how a template file loader works; second I am going to show you a real example; and third I’m going to show you just how easy building a template loader into your plugin is now.



The concept of a template loader is pretty simple:

Determine the name of the file to be loaded Determine the locations to look for the file and the order in which each should be searched Search each location for the file Load the file and stop searching as soon as it is found Fall back to some default template if the requested file is never found

When building a template loader in a plugin, the developer will usually create one main templates folder (the name can vary) that exists inside the plugin and it will hold all of the available template files. These files can then be copied to a specifically-named folder in the currently active theme in order to be modified. If a file from the plugin’s templates folder exists inside of the theme, it gets loaded instead of the default version from the plugin.

Template file loaders like this are used in a lot of large-scale plugins in order to provide greater flexibility and better control for advanced users that want to tailor a plugin’s output more to their specific needs. A few examples of plugins that use template file loaders are:

Let’s look at a quick example of what a template file loader looks like in a plugin. We will use Restrict Content Pro (RCP) for this example.

RCP has several template files for the registration form, the login form, and the user’s profile editor form. The login form template, as an example, looks like this:

<?php global $rcp_login_form_args ; ?> <?php if ( ! is_user_logged_in ( ) ) : ?> <?php rcp_show_error_messages ( 'login' ) ; ?> <form id="rcp_login_form" class="rcp_form" method="POST" action=" <?php echo esc_url ( rcp_get_current_url ( ) ) ; ?> "> <fieldset class="rcp_login_data"> <p> <label for="rcp_user_Login"> <?php _e ( 'Username' , 'rcp' ) ; ?> </label> <input name="rcp_user_login" id="rcp_user_login" class="required" type="text"/> </p> <p> <label for="rcp_user_pass"> <?php _e ( 'Password' , 'rcp' ) ; ?> </label> <input name="rcp_user_pass" id="rcp_user_pass" class="required" type="password"/> </p> <p> <label for="rcp_user_remember"> <?php _e ( 'Remember' , 'rcp' ) ; ?> </label> <input type="checkbox" name="rcp_user_remember" id="rcp_user_remember" value="1"/> </p> <p class="rcp_lost_password"><a href=" <?php echo esc_url ( wp_lostpassword_url ( rcp_get_current_url ( ) ) ) ; ?> "> <?php _e ( 'Lost your password?' , 'rcp' ) ; ?> </a></p> <p> <input type="hidden" name="rcp_action" value="login"/> <input type="hidden" name="rcp_redirect" value=" <?php echo esc_url ( $rcp_login_form_args [ 'redirect' ] ) ; ?> "/> <input type="hidden" name="rcp_login_nonce" value=" <?php echo wp_create_nonce ( 'rcp-login-nonce' ) ; ?> "/> <input id="rcp_login_submit" type="submit" value="Login"/> </p> </fieldset> </form> <?php else : ?> <div class="rcp_logged_in"> <?php _e ( 'You are logged in.' , 'rcp' ) ; ?> <a href=" <?php echo wp_logout_url ( home_url ( ) ) ; ?> "> <?php _e ( 'Logout' , 'rcp' ) ; ?> </a></div> <?php endif ; ?> <?php global $rcp_login_form_args; ?> <?php if( ! is_user_logged_in() ) : ?> <?php rcp_show_error_messages( 'login' ); ?> <form id="rcp_login_form" class="rcp_form" method="POST" action="<?php echo esc_url( rcp_get_current_url() ); ?>"> <fieldset class="rcp_login_data"> <p> <label for="rcp_user_Login"><?php _e( 'Username', 'rcp' ); ?></label> <input name="rcp_user_login" id="rcp_user_login" class="required" type="text"/> </p> <p> <label for="rcp_user_pass"><?php _e( 'Password', 'rcp' ); ?></label> <input name="rcp_user_pass" id="rcp_user_pass" class="required" type="password"/> </p> <p> <label for="rcp_user_remember"><?php _e( 'Remember', 'rcp' ); ?></label> <input type="checkbox" name="rcp_user_remember" id="rcp_user_remember" value="1"/> </p> <p class="rcp_lost_password"><a href="<?php echo esc_url( wp_lostpassword_url( rcp_get_current_url() ) ); ?>"><?php _e( 'Lost your password?', 'rcp' ); ?></a></p> <p> <input type="hidden" name="rcp_action" value="login"/> <input type="hidden" name="rcp_redirect" value="<?php echo esc_url( $rcp_login_form_args['redirect'] ); ?>"/> <input type="hidden" name="rcp_login_nonce" value="<?php echo wp_create_nonce( 'rcp-login-nonce' ); ?>"/> <input id="rcp_login_submit" type="submit" value="Login"/> </p> </fieldset> </form> <?php else : ?> <div class="rcp_logged_in"><?php _e( 'You are logged in.', 'rcp' ); ?> <a href="<?php echo wp_logout_url( home_url() ); ?>"><?php _e( 'Logout', 'rcp' ); ?></a></div> <?php endif; ?>

This file is mostly straight HTML with a little bit of PHP. A pretty standard template file.

Restrict Content Pro loads this file through a short code, and when that happens, it looks like this:

rcp_get_template_part ( 'login' ) ; rcp_get_template_part( 'login' );

The rcp_get_template_part() takes care of searching each of the possible locations for a file called login.php and then loading the file if it exists.

The rcp_get_template_part() function looks like this:

/** * Retrieves a template part * * @since v1.5 * * Taken from bbPress * * @param string $slug * @param string $name Optional. Default null * * @uses rcp_locate_template() * @uses load_template() * @uses get_template_part() */ function rcp_get_template_part ( $slug , $name = null , $load = true ) { // Execute code for this part do_action ( 'get_template_part_' . $slug , $slug , $name ) ; // Setup possible parts $templates = array ( ) ; if ( isset ( $name ) ) $templates [ ] = $slug . '-' . $name . '.php' ; $templates [ ] = $slug . '.php' ; // Allow template parts to be filtered $templates = apply_filters ( 'rcp_get_template_part' , $templates , $slug , $name ) ; // Return the part that is found return rcp_locate_template ( $templates , $load , false ) ; } /** * Retrieves a template part * * @since v1.5 * * Taken from bbPress * * @param string $slug * @param string $name Optional. Default null * * @uses rcp_locate_template() * @uses load_template() * @uses get_template_part() */ function rcp_get_template_part( $slug, $name = null, $load = true ) { // Execute code for this part do_action( 'get_template_part_' . $slug, $slug, $name ); // Setup possible parts $templates = array(); if ( isset( $name ) ) $templates[] = $slug . '-' . $name . '.php'; $templates[] = $slug . '.php'; // Allow template parts to be filtered $templates = apply_filters( 'rcp_get_template_part', $templates, $slug, $name ); // Return the part that is found return rcp_locate_template( $templates, $load, false ); }

This function does four things:

Fires a do_action() call so that other plugins can hook into the load process of specific template files. Determines the names of the template files to look for. Passes the template file names through a filter so other plugins can force it to look for additional or different template files. Fires rcp_locate_template(), which performs the actual file lookups.

Before we have a final picture of how this works, we need to look at rcp_locate_template():

/** * Retrieve the name of the highest priority template file that exists. * * Searches in the STYLESHEETPATH before TEMPLATEPATH so that themes which * inherit from a parent theme can just overload one file. If the template is * not found in either of those, it looks in the theme-compat folder last. * * Taken from bbPress * * @since v1.5 * * @param string|array $template_names Template file(s) to search for, in order. * @param bool $load If true the template file will be loaded if it is found. * @param bool $require_once Whether to require_once or require. Default true. * Has no effect if $load is false. * @return string The template filename if one is located. */ function rcp_locate_template ( $template_names , $load = false , $require_once = true ) { // No file found yet $located = false ; // Try to find a template file foreach ( ( array ) $template_names as $template_name ) { // Continue if template is empty if ( empty ( $template_name ) ) continue ; // Trim off any slashes from the template name $template_name = ltrim ( $template_name , '/' ) ; // Check child theme first if ( file_exists ( trailingslashit ( get_stylesheet_directory ( ) ) . 'rcp/' . $template_name ) ) { $located = trailingslashit ( get_stylesheet_directory ( ) ) . 'rcp/' . $template_name ; break ; // Check parent theme next } elseif ( file_exists ( trailingslashit ( get_template_directory ( ) ) . 'rcp/' . $template_name ) ) { $located = trailingslashit ( get_template_directory ( ) ) . 'rcp/' . $template_name ; break ; // Check theme compatibility last } elseif ( file_exists ( trailingslashit ( rcp_get_templates_dir ( ) ) . $template_name ) ) { $located = trailingslashit ( rcp_get_templates_dir ( ) ) . $template_name ; break ; } } if ( ( true == $load ) && ! empty ( $located ) ) load_template ( $located , $require_once ) ; return $located ; } /** * Retrieve the name of the highest priority template file that exists. * * Searches in the STYLESHEETPATH before TEMPLATEPATH so that themes which * inherit from a parent theme can just overload one file. If the template is * not found in either of those, it looks in the theme-compat folder last. * * Taken from bbPress * * @since v1.5 * * @param string|array $template_names Template file(s) to search for, in order. * @param bool $load If true the template file will be loaded if it is found. * @param bool $require_once Whether to require_once or require. Default true. * Has no effect if $load is false. * @return string The template filename if one is located. */ function rcp_locate_template( $template_names, $load = false, $require_once = true ) { // No file found yet $located = false; // Try to find a template file foreach ( (array) $template_names as $template_name ) { // Continue if template is empty if ( empty( $template_name ) ) continue; // Trim off any slashes from the template name $template_name = ltrim( $template_name, '/' ); // Check child theme first if ( file_exists( trailingslashit( get_stylesheet_directory() ) . 'rcp/' . $template_name ) ) { $located = trailingslashit( get_stylesheet_directory() ) . 'rcp/' . $template_name; break; // Check parent theme next } elseif ( file_exists( trailingslashit( get_template_directory() ) . 'rcp/' . $template_name ) ) { $located = trailingslashit( get_template_directory() ) . 'rcp/' . $template_name; break; // Check theme compatibility last } elseif ( file_exists( trailingslashit( rcp_get_templates_dir() ) . $template_name ) ) { $located = trailingslashit( rcp_get_templates_dir() ) . $template_name; break; } } if ( ( true == $load ) && ! empty( $located ) ) load_template( $located, $require_once ); return $located; }

This function takes an array of template file names, such as array( ‘login.php’, ‘login-form.php’ ), loops through the list and searches in each of the possible locations for the files. As soon as it finds a matching file, it does one of two things:

If the third parameter, $load, is true, the file is loaded with the core WordPress function load_template() If $load is false, it returns the absolute path to the file

For Restrict Content Pro, the locations rcp_locate_template() looks are (in this order):

wp-content/themes/CHILD_THEME/rcp/{filename}

wp-content/themes/PARENT_THEME/rcp/{filename}

wp-content/plugins/restrict-content-pro/templates/{filename}

That’s the entire process. While it is not a ton of code, it is something that can be intimidating to write from scratch (note, I took most of my code straight from bbPress, thanks JJJ!). If you are not writing a large plugin, it can be difficult to justify spending a lot of time on a system like this, which leads me into the next part of this tutorial.

Gary Jones recently released a Template Loader class that does most of the heavy lifting for you. The class is based on the template loader built into Easy Digital Downloads (which was based off of the one in bbPress), so it works nearly identically to the methods described above for Restrict Content Pro, except it takes an OOP approach.

I’m not going to walk you through the class, but I am going to show you a quick example of how to use it so that you can easily build template file loaders into your own plugins.

First, you will copy the main Gamajo_Template_Loader class into a new file in your plugin.

Second, you will write a new class that extends Gamajo_Template_Loader, like this:

<?php /** * Template loader for PW Sample Plugin. * * Only need to specify class properties here. * */ class PW_Template_Loader extends Gamajo_Template_Loader { /** * Prefix for filter names. * * @since 1.0.0 * @type string */ protected $filter_prefix = 'pw' ; /** * Directory name where custom templates for this plugin should be found in the theme. * * @since 1.0.0 * @type string */ protected $theme_template_directory = 'pw-templates' ; /** * Reference to the root directory path of this plugin. * * @since 1.0.0 * @type string */ protected $plugin_directory = PW_SAMPLE_PLUGIN_DIR ; } <?php /** * Template loader for PW Sample Plugin. * * Only need to specify class properties here. * */ class PW_Template_Loader extends Gamajo_Template_Loader { /** * Prefix for filter names. * * @since 1.0.0 * @type string */ protected $filter_prefix = 'pw'; /** * Directory name where custom templates for this plugin should be found in the theme. * * @since 1.0.0 * @type string */ protected $theme_template_directory = 'pw-templates'; /** * Reference to the root directory path of this plugin. * * @since 1.0.0 * @type string */ protected $plugin_directory = PW_SAMPLE_PLUGIN_DIR; }

This class simply defines the prefix for filters, the name of the directory that files will be searched for in from the current theme, and also the plugin’s own directory.

Third, you include both of these class files into your plugin and instantiate the sub class:

<?php define ( 'PW_SAMPLE_PLUGIN_DIR' , plugin_dir_path ( __FILE__ ) ) ; require PW_SAMPLE_PLUGIN_DIR . 'class-gamajo-template-loader.php' ; require PW_SAMPLE_PLUGIN_DIR . 'class-pw-template-loader.php' ; function pw_sample_shortcode ( ) { $templates = new PW_Template_Loader ; // Templates will be loaded here } add_shortcode ( 'pw_sample' , 'pw_sample_shortcode' ) ; <?php define( 'PW_SAMPLE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); require PW_SAMPLE_PLUGIN_DIR . 'class-gamajo-template-loader.php'; require PW_SAMPLE_PLUGIN_DIR . 'class-pw-template-loader.php'; function pw_sample_shortcode() { $templates = new PW_Template_Loader; // Templates will be loaded here } add_shortcode( 'pw_sample', 'pw_sample_shortcode' );

Note, I’m using a simple short code here just to help illustrate how this works. It does not have to be in a short code.

Fourth, create a folder called templates in your plugin’s directory and add your template files to that directory.

Now you can load template files like this:

$templates -> get_template_part ( $slug , $name ) ; $templates->get_template_part( $slug, $name );

When put in our short code, it looks like this:

function pw_sample_shortcode ( ) { $templates = new PW_Template_Loader ; ob_start ( ) ; $templates -> get_template_part ( 'content' , 'header' ) ; $templates -> get_template_part ( 'content' , 'middle' ) ; $templates -> get_template_part ( 'content' , 'footer' ) ; return ob_get_clean ( ) ; } add_shortcode ( 'pw_sample' , 'pw_sample_shortcode' ) ; function pw_sample_shortcode() { $templates = new PW_Template_Loader; ob_start(); $templates->get_template_part( 'content', 'header' ); $templates->get_template_part( 'content', 'middle' ); $templates->get_template_part( 'content', 'footer' ); return ob_get_clean(); } add_shortcode( 'pw_sample', 'pw_sample_shortcode' );

Note that an output buffer is used because short codes, in order to work correctly, must always return their content, and the template loader includes the files directly.

To help demonstrate exactly how to use this class, I’ve written a sample plugin that can be viewed and downloaded from Github.

Having template files available for advanced users of your plugin can dramatically improve the flexibility of your plugin. I would absolutely recommend implementing them in any and all decently large plugins that include output on the front end of the site.