It is becoming more and more common for plugins to provide template files that can be copied to a theme’s folder and then safely modified. This means that the site owner that is using the plugin can add, remove, and modify the markup of the plugin without hampering the upgrade process when new versions are released. This practice is great and something that many, many plugins should start adopting, but there is more to it than just including the template files. In order for the template files to really be useful, they need to be built well. What exactly does this mean? Let’s find out.
Some plugins that include a templating engine:
- bbPress
- Easy Digital Downloads
- Restrict Content Pro
- WP e-Commerce
- WooCommerce
- many more that I haven’t listed
The primary purpose of providing template files is to allow the user to modify the way the plugin displays its data. For example, if you are using an e-commerce plugin, then you might modify the template files to adjust the HTML markup of the product or checkout pages.
The process of modifying a template file usually looks about like this:
- Create a folder in the currently active theme with a specific name, such as edd_templates, bbpress, or store. The exact name is dependent on the plugin.
- Copy files from within wp-content/plugins/{plugin name}/templates/ to wp-content/themes/{active theme}/{plugin’s template folder}
- Modify the newly placed template files
In order to understand how to build a great template file, we need to think about what users want to modify within the template file. Do they want to add HTML above or below the default output? Do they want to change some HTML tags of the default output to use different tags, such as changing SPAN tags to DIV tags? Do they want to add text inline? Do they want to remove some of the default HTML?
Determining some of the common ways that users will wish to modify the template files is really important when deciding exactly what kind of info to include.
In order to accommodate the less technical users, you do not necessarily want to include every single bit of HTML possible (theoretically giving the most flexibility), since that might be overly confusing for some.
Also to help accommodate the less technical users, you will want to avoid large amounts of raw PHP in the template files.
In general, you want to make the files as simple as possible, while also not overly simplifying things so as to remove all of the control.
Over the last few years I have seem some really great template files, and also some really poor template files, but what really makes a file good or bad?
Here’s a bad one:
1 2 3 4 5 6 7 | <div id="my_plugin_form"> <?php do_action( 'my_plugin_form_top' ); ?> <div id="my_plugin_form_mid"> <?php do_action( 'my_plugin_form_mid' ); ?> </div> <?php do_action( 'my_plugin_form_bottom' ); ?> </div> |
This is bad because the main bulk of the template file (let’s assume it outputs a submission form of some kind) is actually rendered via action hooks, meaning the functions that spit out the HTML reside somewhere else within the plugin. We want template files to provide flexibility, buy by having the main HTML locked away inside of the functions users are severely limited by what they can modify with this template file.
Here’s another example of a bad template file:
1 | <?php echo do_shortcode( '[my_plugin_form]' ); ?> |
This is a bad template because it does nothing more than spit out the contents of a short code the plugin registers. The only flexibility this template file provides is the ability to wrap the short code with additional HTML, rather than providing the ability to modify the existing markup, which is what we really want a template file to provide.
Now let’s look at one that is okay (note this is a real template file from one of my own plugins). Not horrible but not great either.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | <?php // Retrieve all purchases for the current user $purchases = edd_get_users_purchases( get_current_user_id(), 20, true, array( 'pending', 'revoked', 'publish', 'failed', 'preapproval', 'refunded' ) ); if ( $purchases ) : do_action( 'edd_before_download_history' ); ?> <table id="edd_user_history"> <thead> <tr class="edd_download_history_row"> <?php do_action( 'edd_download_history_header_start' ); ?> <th class="edd_download_download_name"><?php _e( 'Download Name', 'edd' ); ?></th> <?php if ( ! edd_no_redownload() ) : ?> <th class="edd_download_download_files"><?php _e( 'Files', 'edd' ); ?></th> <?php endif; //End if no redownload?> <?php do_action( 'edd_download_history_header_end' ); ?> </tr> </thead> <?php foreach ( $purchases as $payment ) : $downloads = edd_get_payment_meta_cart_details( $payment->ID, true ); $purchase_data = edd_get_payment_meta( $payment->ID ); if ( $downloads ) : foreach ( $downloads as $download ) : // Skip over Bundles. Products included with a bundle will be displayed individually if ( edd_is_bundled_product( $download['id'] ) ) continue; ?> <tr class="edd_download_history_row"> <?php $price_id = edd_get_cart_item_price_id( $download ); $download_files = edd_get_download_files( $download['id'], $price_id ); $name = get_the_title( $download['id'] ); // Retrieve and append the price option name if ( ! empty( $price_id ) ) { $name .= ' - ' . edd_get_price_option_name( $download['id'], $price_id, $payment->ID ); } do_action( 'edd_download_history_row_start', $payment->ID, $download['id'] ); ?> <td class="edd_download_download_name"><?php echo esc_html( $name ); ?></td> <?php if ( ! edd_no_redownload() ) : ?> <td class="edd_download_download_files"> <?php if ( edd_is_payment_complete( $payment->ID ) ) : if ( $download_files ) : foreach ( $download_files as $filekey => $file ) : $download_url = edd_get_download_file_url( $purchase_data['key'], $purchase_data['email'], $filekey, $download['id'], $price_id ); ?> <div class="edd_download_file"> <a href="<?php echo esc_url( $download_url ); ?>" class="edd_download_file_link"> <?php echo esc_html( $file['name'] ); ?> </a> </div> <?php do_action( 'edd_download_history_files', $filekey, $file, $id, $payment->ID, $purchase_data ); endforeach; else : _e( 'No downloadable files found.', 'edd' ); endif; // End if payment complete else : ?> <span class="edd_download_payment_status"> <?php printf( __( 'Payment status is %s', 'edd' ), edd_get_payment_status( $payment, true) ); ?> </span> <?php endif; // End if $download_files ?> </td> <?php endif; // End if ! edd_no_redownload() do_action( 'edd_download_history_row_end', $payment->ID, $download['id'] ); ?> </tr> <?php endforeach; // End foreach $downloads endif; // End if $downloads endforeach; ?> </table> <div id="edd_download_history_pagination" class="edd_pagination navigation"> <?php $big = 999999; echo paginate_links( array( 'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ), 'format' => '?paged=%#%', 'current' => max( 1, get_query_var( 'paged' ) ), 'total' => ceil( edd_count_purchases_of_customer() / 20 ) // 20 items per page ) ); ?> </div> <?php do_action( 'edd_after_download_history' ); else : ?> <p class="edd-no-downloads"><?php _e( 'You have not purchased any downloads', 'edd' ); ?></p> <?php endif; |
The good things about this template file:
- All of the HTML markup is available so it can be freely modified
- It is reasonably clear what everything is for
The bad things about this template file:
- There is way too much raw PHP, making it much more difficult for less technical users to modify it
- There is HTML echoed out via PHP instead of being placed directly in the file
In order to write a really good template file, there are several key things to keep in mind:
- Avoid raw PHP as much as possible
- Write clear, semantic HTML
- Expose as much HTML as possible
- Avoid using PHP variables as much as possible
Let’s now look at an excellent template file. This one comes from bbPress:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <?php /** * Search Loop * * @package bbPress * @subpackage Theme */ ?> <?php do_action( 'bbp_template_before_search_results_loop' ); ?> <ul id="bbp-search-results" class="forums bbp-search-results"> <li class="bbp-header"> <div class="bbp-search-author"><?php _e( 'Author', 'bbpress' ); ?></div><!-- .bbp-reply-author --> <div class="bbp-search-content"> <?php _e( 'Search Results', 'bbpress' ); ?> </div><!-- .bbp-search-content --> </li><!-- .bbp-header --> <li class="bbp-body"> <?php while ( bbp_search_results() ) : bbp_the_search_result(); ?> <?php bbp_get_template_part( 'loop', 'search-' . get_post_type() ); ?> <?php endwhile; ?> </li><!-- .bbp-body --> <li class="bbp-footer"> <div class="bbp-search-author"><?php _e( 'Author', 'bbpress' ); ?></div> <div class="bbp-search-content"> <?php _e( 'Search Results', 'bbpress' ); ?> </div><!-- .bbp-search-content --> </li><!-- .bbp-footer --> </ul><!-- #bbp-search-results --> <?php do_action( 'bbp_template_after_search_results_loop' ); ?> |
Writing excellent template files is an art that not many developers have truly mastered, but by keeping the suggestions above in mind, you will be able to write template files that are much, much more useful to your users.
If you haven’t yep adopted the practice of including templates in your plugins, and you have a plugin that could use them, please consider adding a template engine. If any one has any questions on how to build a template engine that allows flexible template files, I will be more than happy to assist.
Another reason the second example is bad is that they could have just called the callback function of the registered shortcode. No need to have WordPress perform a regular expression when all we want to do is to call the shortcode callback function, right? 🙂
Excellent point.
I recently found out you also have to anticipate where users are going to use your plugin templates. One of my plugins has a shortcode and one of the templates is using the_content() function. If used inside the filter hook “the_content” with do_shortcode() will produce an infinite loop. Things to think about 🙂
Pippin,
I’ve got one plugin that has a template file (which is ugly!) and another two I plan on adding some kind of template file.
I would like to understand more about the template engine that you mentioned above.
Perhaps more importantly, the reason that the bbPress template system is great is because it makes great use of template-part files. Breaking up the HTML into logical chunks makes it far easier for people downstream (end users, Theme developers, etc.) to modify the template, and to maintain those modifications – whereas in your previous example, all of the HTML is in one single template file. Even a minor change requires overriding the entire template.
Excellent point Chip!
I know there isn’t currently a standard way to do this but considering plugin modifications get overwritten during an update, it would be great for authors to store the template changes in ‘child plugins’ instead of the original plugins.
The main point of these template files is to allow them to be copied over to the currently active theme so that they don’t get overwritten on updates.
Sorry Pippin, I didn’t explain myself properly. I appreciate your example was for plugins that already check the theme path for template files but plenty of plugins don’t. In a case like that, updates to the plugin will remove the customizations.
It would be nice if plugins worked on a method similar to the parent/child relationship of themes where a plugin could specify a theme file and WordPress would know to scan for that theme specific file in the theme hierarchy, or a child plugin directory to avoid overwrites.
Oh yes, absolutely. Thanks for the clarification 🙂
Good write up and definitely something I’ve been looking more into lately with client projects requiring a couple of the plugins you mentioned (woocommerce, bbpress).
I’m not 100% sure this is possible, so correct me if I’m wrong, but one thing that could help expedite the copy/paste of files from plugin folder to theme folder would be to have the plugin automatically dump a folder (ie: /bbpress) into the current theme’s folder.
Also, good point from Chip about the template-part files. I’ve used them recently on a theme I developed and it made making adjustments so much smoother of a process.
That’s definitely possible. WP e-Commerce is one plugin that I’m aware of that offers a button in the plugin settings to automatically copy the template files over.
I believe woocommerce allows for the push-button moving of files as well. I’d love to see them automatically added as the plugin is activated, bypassing the need for the user to have to push anything in order to have the files to edit.
Pushing them automatically is actually not a very good idea, simply because the moment a user has copied the template files to their theme, the plugin can no longer update them when modifications to the files are made with plugin updates.
It’s best to only copy template files that are needed so you can ensure user’s have the latest version of any un-changed template file.
Ah yeah, you’re right. I guess that would be a bit of a barrier and would make the push button access to those files the best solution.
If you build amazing template files that never, ever need updated again because they are perfect, that kind of option is fantastic, though we all know that is an extreme rarity 😀
Thanks for sharing this with us, very useful post. I also would like to get more info on template engines. Cheers!!