Loading templates in WordPress and WooCommerce
WooCommerce has a number of functions to load different templates. I describe what they do and how they're connected in this post.
I’ve been working on a few customer sites, and occasionally I need to write a plugin that loads in its own template file that overwrites a core WooCommerce template. However we would still want to make sure that if the same template is in the theme, that gets priority.
That said WooCommerce and WordPress has a ton of files to load templates, and uh... they can get confusing. Here are a few:
Template loading functions
wc_get_template
WooCommerce function for including an entire template file. Uses wc_locate_template
, and does include $path
.
Has wc_get_template
filter to modify the path to the template file it’s about to include. Has actions as well.
wc_get_template_part
WooCommerce function for loading template parts. Uses locate_template
to look in themes. Uses load_template
at the end of it. Mostly used in other WooCommerce templates in loops.
Has wc_get_template_part
filter to modify the path to the template file before including it.
wc_get_template_html
It literally is just this:
/**
* Like wc_get_template, but returns the HTML instead of outputting.
*
* @see wc_get_template
* @since 2.5.0
* @param string $template_name
* @param array $args
* @param string $template_path
* @param string $default_path
*
* @return string
*/
function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
ob_start();
wc_get_template( $template_name, $args, $template_path, $default_path );
return ob_get_clean();
}
wc_locate_template
WooCommerce function to figure out the path of a template file. Returns a string. Uses locate_template
.
Has filter woocommerce_locate_template
at the very end of it to change the path to the template file.
locate_template
WordPress function to figure out the path of the file that should be included. Looks in theme, in order:
- child-theme
- parent-theme
- wp-includes/theme-compat directory. Returns a string, OR loads the template
Does NOT have a filter.
load_template
WordPress function to include the template. Uses require
or require_once
.
No filters / actions.
template-loader.php
And of course this WordPress base file, and just include
s the first file to be shown as determined by the URL the visitor is visiting.
Has template_include
filter to modify the path to the template file to include.
Let’s use them!
Here’s the problem to solve: I need to redeclare the archive-product.php
, so I can statically cache the individual product templates so WordPress doesn’t go away grinding at the server all the time. That means that I needed to tell the site that whenever it would load the archive-product.php
, I want it to load my plugin’s copy of it, if the theme doesn’t have one. It’s pretty easy, right?
This is how WooCommerce loads the templates
/**
* Get other templates (e.g. product attributes) passing attributes and including the file.
*
* @access public
* @param string $template_name
* @param array $args (default: array())
* @param string $template_path (default: '')
* @param string $default_path (default: '')
*/
function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
if ( ! empty( $args ) && is_array( $args ) ) {
extract( $args );
}
$located = wc_locate_template( $template_name, $template_path, $default_path );
if ( ! file_exists( $located ) ) {
wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '<code>' . $located . '</code>' ), '2.1' );
return;
}
// Allow 3rd party plugin filter template file from their plugin.
$located = apply_filters( 'wc_get_template', $located, $template_name, $args, $template_path, $default_path );
do_action( 'woocommerce_before_template_part', $template_name, $template_path, $located, $args );
include( $located );
do_action( 'woocommerce_after_template_part', $template_name, $template_path, $located, $args );
}
Basically all I need to do is hook into the wc_get_template
filter, figure out whether there’s one in the theme, and if not, include my copy. Here’s my code, which is mostly just a copy of the above:
add_filter( 'woocommerce_locate_template', 'rs_locate_archive_product_template', 10, 3 );
/**
* Hooked into woocommerce_locate_template, this function will load in the archive-product.php template included
* with this plugin. If the theme has its own archive-product.php, that will be loaded.
*
* @param string $template
* @param string $template_name
* @param string $template_path
* @return void
*/
function rs_locate_archive_product_template( $template, $template_name, $template_path ) {
if ( 'archive-product.php' !== $template_name || WC_TEMPLATE_DEBUG_MODE ) {
return $template;
}
if ( ! $template_path ) {
$template_path = WC()->template_path();
}
// Look within passed path within the theme - this is priority.
$template = locate_template(
array(
trailingslashit( $template_path ) . $template_name,
$template_name,
)
);
// Get our template
if ( ! $template ) {
$template = plugin_dir_path( __FILE__ ) . 'templates/' . $template_name;
}
return $template;
}
And done. Yay!
NOT SO FAST, SON!
Client uses a plugin called WooCommerce On-Sale page, which collects all the products that are currently on sale, and then displays them using the archive-product.php
template. We’ve anticipated that, so yay! However it’s not using my special version that caches things.
Turns out the plugin does not go through the wc_get_template
functionality, so all my efforts right there go out the window.
In addition to all THAT, WooCommerce’s own Shop page is also just a product archive page, which, according to the template hierarchy of WordPress is actually just going to be the file called archive-product.php
. Which means that my function that’s hooked into woocommerce_locate_template
is now totally useless.
We need to go deeper!
So following the chain of filters, it seems I actually need to end up using the template_include
filter. On one hand the On-Sale plugin is using that one, and because the Shop page is a post type archive, and the theme may or may not have a file for that, and it’s a plugin, I went around to see whether WooCommerce has attached itself to that filter.
And sure enough it did. Basically in a file called class-wc-template-loader.php
it replicates sort of what WordPress is doing in its own template-loader.php
file.
Oh, and the On-Sale plugin is replicating some of the functionality WooCommerce is doing with its own template loaders. All fun and games.
Which also means, that in order to display a product category archive page, here’s what happens:
URL: https://example.local.dev/shop/product-category/awesome-products
template-loader.php
gives up because it didn’t find anything that would be WordPress-y, but has thetemplate_include
filter- to which WooCommerce’s
class-wc-template-loader.php
is hooked, which then takes over, and finds that it’s a category archive for the taxonomyproduct-category
, and the termawesome-products
WC_Template_Loader::template_loader
determines the default file to betaxonomy-product_cat.php
, and gets an array of possible other files too (taxonomy-product_cat-awesome-products.php
,<yourtheme>/woocommerce/taxonomy-product_cat-awesome-products.php
, etc...)- Uses
locate_template
with the possible other files to find a file, and includes that one, if found. If not found, includes the default file,taxonomy-product_cat.php
in the WooCommerce templates folder taxonomy-product_cat.php
callswc_get_template( 'archive-product.php' )
wc_get_template
useswc_locate_template
to see whether that file exists somewhere else, ie in the themewc_locate_template
fires thewoocommerce_locate_template
filter in case we want to hook in here to change what’s found- back to
wc_get_template
, it fires another filter,wc_get_template
in case you want to filter the path AGAIN (or here, instead ofwoocommerce_locate_template
) - and then includes whatever file it found
- assuming it’s the default one,
archive-product.php
will have a loop, and within that loop, it will callwc_get_template_part( 'content', 'product' )
wc_get_template_part
will uselocate_template
to check whether that file withslug-name.php
(in this casecontent-product.php
) is in the theme. If not,- it will check whether the file is in the WooCommerce plugin’s template folders
- if not, it will check whether the file
slug.php
is in the theme folders (in this casecontent.php
) - lastly it will run the path through the
wc_get_template_part
filter, in case we want to change what file is loaded - then calls
load_template
on the path - which will then
require
the file
In this 16 step process, we could change the outcomes in the following points:
-
-
template_include
filter, bypassing all of WooCommerce
-
-
-
woocommerce_locate_template
filter, changing the path
-
-
-
wc_get_template
filter, changing the path
-
-
-
wc_get_template_part
filter, changing the path of the parts
-
So which one to use?
The one that will work for your use case. Note that if you change the path in point 7, and something is hoooked into point 8, it can STILL overwrite your change.
Note also that sometimes changing something in point 7 or 8 will be too late. For example when we go to the Shop page, that’s determined in point 2, and it won’t go any further.
For this project, I need to hook into the template_include
to cover all possible bases, and then possibly later ones as well in case something overwrote them.
Hopefully this made things a bit clearer. If you have any questions reach out to me on the twitters at @javorszky.
Cover photo by Peter Hershey. Found on Unsplash.