/ wordpress

Loading templates in WordPress and WooCommerce

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:

  1. child-theme
  2. parent-theme
  3. 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 includes 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

  1. template-loader.php gives up because it didn’t find anything that would be WordPress-y, but has the template_include filter
  2. 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 taxonomy product-category, and the term awesome-products
  3. WC_Template_Loader::template_loader determines the default file to be taxonomy-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...)
  4. 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
  5. taxonomy-product_cat.php calls wc_get_template( 'archive-product.php' )
  6. wc_get_template uses wc_locate_template to see whether that file exists somewhere else, ie in the theme
  7. wc_locate_template fires the woocommerce_locate_template filter in case we want to hook in here to change what’s found
  8. back to wc_get_template, it fires another filter, wc_get_template in case you want to filter the path AGAIN (or here, instead of woocommerce_locate_template)
  9. and then includes whatever file it found
  10. assuming it’s the default one, archive-product.php will have a loop, and within that loop, it will call wc_get_template_part( 'content', 'product' )
  11. wc_get_template_part will use locate_template to check whether that file with slug-name.php (in this case content-product.php) is in the theme. If not,
  12. it will check whether the file is in the WooCommerce plugin’s template folders
  13. if not, it will check whether the file slug.php is in the theme folders (in this case content.php)
  14. lastly it will run the path through the wc_get_template_part filter, in case we want to change what file is loaded
  15. then calls load_template on the path
  16. 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.

Gabor Javorszky

I'm a freelance WordPress / WooCommerce developer focusing on Subscriptions and advanced functionality. Get in touch: @javorszky / gabor (at) javorszky (dot) co (dot) uk. Read more on the About page.

Read More