Creating a plugin for WooCommerce

Dear Internet Traveler,

This guide is woefully out of date now. Though I appreciate the Google Link Juice this post has provided, I would highly suggest looking WooCommerce Docs Version, which has been updated since I thought it necessary to create this modified version.


I was looking around wanting to find info on creating a plugin that leverages WooCommerce, and found their guide on their docs site. However, it didn’t go according to plan because their docs are a bit outdated, probably due to the fact they aren’t accepting submission to their own repo anymore. So without further adieu, my edited version of that tutorial.

Want to create a plugin to extend WooCommerce? WooCommerce Plugins are essentially the same as regular WordPress plugins, just with a little more finesse. Sometimes you need a little finesse, sometimes you need a lot. If you don’t know how to write a plugin, you can get started here.

Check if WooCommerce is active

Most WooCommerce plugins do not need to run unless WooCommerce is already active. You can check within your plugin to see if WooCommerce is installed:

/**
 * Check if WooCommerce is active
 **/
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
if(is_plugin_active( 'woocommerce/woocommerce.php')) return;

// Put your plugin code here

If you are looking to create a WooCommerce plugin for a multisite, (which, why wouldn’t you?) it might look something like this:

/**
 * Check if WooCommerce is active
 **/
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
if( !is_plugin_active( 'woocommerce/woocommerce.php' ) || !is_plugin_active_for_network( 'woocommerce/woocommerce.php' ) ) return;

// Put your plugin code here

Main file naming

The main plugin file should adopt the name of the plugin, e.g., A plugin with the directory name of plugin-name would have its main file named plugin-name.php.

Text domains

Follow guidelines for Internationalization for WordPress Developers, the text domain should match your plugin directory name, e.g., A plugin with a directory name of plugin-name would have the text domain plugin-name. No underscores.

Localization

All text strings within the plugin code should be in English. This is the WordPress default locale, and English should always be the first language. If your plugin is intended for a specific market (e.g., Spain or Italy), appropriate translation files for those languages should be included within your plugin package. More at: Using Makepot to translate your plugin.

Follow WordPress PHP Guidelines

WordPress has a set of guidelines to keep all WordPress code consistent and easier to read. This includes quotes, indentation, brace style, shorthand php tags, yoda conditions, naming conventions, and more. Please review the guidelines.

Don’t get me wrong. Guidelines are great. I’m a fan of keeping things consistent. But if you use tabs and not spaces, snuggle your braces or not… it’s not going to break your code.

Code conventions help stop basic mistakes; but in my experience, don’t prevent them completely. Understand what you are doing before you do it, and don’t just cut n paste random code.

Custom Database Tables & Data Storage

Creating custom database tables should be avoided. Whenever possible, you should always use WordPress post types, taxonomies, and options.

Consider the permanence of your data. Here’s a quick primer:

  • If the data may not always be present (i.e., it expires), use a transient.
  • If the data is persistent but not always present, consider using the WP Cache.
  • If the data is persistent and always present, consider the wp_options table.
  • If the data type is an entity with n units, consider a post type.
  • If the data is a means or sorting/categorizing an entity, consider a taxonomy.
  • Logs should be written to a file using the WC_Logger class.

Prevent Data Leaks

Try to prevent direct access data leaks. Add this line of code after the opening PHP tag in each PHP file:

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

Readme

WooCommerce Plugins seem to all have a “standard” WordPress readme. I’ve only ever noticed this since diving into WooCommerce Plugins. You should add two extra headers to the top of the readme file:

  • WC requires at least
  • WC tested up to

Your readme might look something like this:

=== Plugin Name ===
Contributors: (this should be a list of wordpress.org userid's)
Tags: comments, spam
Requires at least: 4.0.1
Tested up to: 4.3
Stable tag: 4.3
License: GPLv3 or later License
URI: http://www.gnu.org/licenses/gpl-3.0.html
WC requires at least: 2.2
WC tested up to: 2.3

Plugin Author Name

Consistency is important to us and our customers. Products offered through WooCommerce.com should provide a consistent experience for all aspects of the product, including who the customer contacts if they have queries.

It should also be obvious for the customer to differentiate a product purchased at WooCommerce.com from a product purchased elsewhere, when looking through their plugin list in WordPress.

Thus, the following plugin headers should be in place:

The Plugin Author is WooCommerce
The Developer header is YourName/YourCompany, with the Developer URI field listed as http://yourdomain.com/
Copyright information is “WooCommerce”

Since you aren’t going to be selling on WooCommerce directly, the previous information is pointless. That being said, your plugin won’t work if you don’t have this at the beginning of your main plugin PHP file (because you read the primer on plugins, right?):

/**
 * Plugin Name: WooCommerce Extension
 * Plugin URI: http://yourepicdevsite.com/woocommerce-extension/
 * Description: Your extension's description text.
 * Version: 123.234.55656
 * Author: Sammy McSammerson
 * Author URI: http://yourepicdevsite.com/
 * Text Domain: woocommerce-extension
*/

In the WooCommerce Helper plugin — required for customers to receive product updates — developer information is included as part of the plugin info to ensure developers are given the credit they deserve. Nope.

Plugin URI

Ensure that the Plugin URI line of the above plugin header is provided. This line should contain the URL of the plugin’s product/sale page on WooCommerce.com (if sold by WooCommerce) or to a dedicated page for the plugin on your website. Again, not needed.

Use WordPress/WooCommerce UI

It’s important to keep a consistent UI across plugins. I can’t stress this enough. Having a custom designed interface, unless it’s super user friendly, can be jarring to the user. All plugins should be hooked into the WordPress/WooCommerce UI. Application data should be loaded via API instead of an iframe. Because, why would you use an iframe? Are you from 10 years ago?

Admin Menu Screens

Both WordPress and WooCommerce provide an extendable administration menu system, allowing for menu items to be added anywhere from the top level of the navigation to an integration sub-menu in the WooCommerce “Integrations” tab. It is important to understand the reasons for each of the various navigation systems, and in which circumstances we expect each to be employed.

tl;dr – which to use when, and why

  • If your plugin integrates with a third party service (e.g., to receive tax rates or connect to a Help Desk), you should use the integration class.
  • If your plugin adds a settings screen to set up the plugin, settings should be under an appropriate tab on the WooCommerce > Settings screen.
  • If your plugin has settings that don’t fit under existing tabs, and creating a sub-tab isn’t appropriate, create a top-level settings tab.
  • If your plugin adds administration screens that don’t involve settings (e.g., Checkout Add-Ons has a screen for managing checkout add-on fields), use a sub-menu under the WooCommerce admin menu item.

WooCommerce Integrations Sub-Menu

WooCommerce provides a facility for registering product integrations. If integrations are registered, an Integrations tab is made available under the WooCommerce > Settings screen.

If your plugin interfaces with a third-party service other than a payment gateway or shipping method that have their own structures, your settings screen should be placed in the Integrations tab.
If you’re integrating a plugin with WooCommerce, we strongly recommend that you use the WC_Integration class.

WordPress Settings Sub-Menu

The top-level Settings menu in WordPress is reserved for screens that are only accessible by full administrators. If your plugin has a single settings screen and is a standalone product, your settings screen should go here.

As WooCommerce extensions are not standalone products, your settings screen would look out of place under the main WordPress Settings menu.
Look how lost and out of place I look, being so far away from the other WooCommerce admin screens.
How a plugin would look if located under WordPress Settings, far away from the the WooCommerce admin.

WooCommerce Sub-Menu

If your plugin adds administration screens that don’t involve setting up your plugin, these screens can sit under the WooCommerce Sub-Menu. For example, Coupon Campaigns adds a WooCommerce sub-menu item to create, edit and delete coupon campaigns; Checkout Add-Ons adds a screen for managing checkout screen add-on fields.

Another reason is that not all admin screens belong in the WooCommerce sub-menu. If everyone placed their settings screen here, there would be a lengthy administration menu. This is inconvenient for the WooCommerce store owner or manager.

Settings used to set up your plugin do not belong in a WooCommerce sub-menu item.

WooCommerce Settings Tab

WooCommerce Settings tabs are intended for top-level, broad topics around setting up WooCommerce.

If your plugin requires settings that don’t fit any of the provided settings tabs, and does not interface with a third-party service, use a new WooCommerce Settings tab.

We make this distinction to avoid lengthy settings tab bars, which do not provide a pleasant UX for a store owner or manager.

If your settings require a page but fall under one of the top level tabs like Products, add them in a section to keep it organized and user friendly.

More info at Adding a Section to a Settings Tab — all it takes is a few lines of code.

Make it Extensible

Developers should use WordPress actions and filters to allow for modification/customization without users having to touch the plugin’s core code base. Don’t ever touch core. Unless you become a core contributor, but in that case, you’re probably way too advanced to have read this far.

And if your plugin creates a front-end output, it’s recommended to have a templating engine in place so users can create custom template files in their theme’s WooCommerce folder that overwrites the plugin’s template files. Now, we use the term templating engine loosely here, don’t go grabbing twig or smarty or anything. It doesn’t need to be that complicated.

For more information, check out Pippin’s post on Writing Extensible Plugins with Actions and Filters.

Remove Unused Code

With version control, there’s no reason to leave in commented out code that can be annoying to scroll through and read. Remove it and add it back later if needed. So do yourself a favour and use Git or Subversion.

Comment

If you have a function, what does the function do? There should be comments for most if not every function in your code. Someone/You may want to modify, and comments are helpful for that. I recommend using PHP Doc Blocks.

Avoid God Objects

God Objects are objects that know or do too much. I had a colleague of mine explain something called “Thor” to me. To this day, I still am not completely sure what it did, but it did way too much. The point of object-oriented programming is taking a large problem and breaking it into smaller parts; for example, a train would have both an Engine object and multiple Car objects; the Car object could contain multiple Person objects, etc.  By having functions do too much, it’s hard to follow that logic and a bug will be harder to fix. Instead of having massive functions, break them down into smaller pieces.

Test Your Code with WP_DEBUG

Always develop with WP_DEBUG mode on, so you can see all PHP warnings sent to the screen. It’s usually things like making sure a variable is set before checking the value.

Separate Business Logic & Presentation Logic

It’s a good practice to separate business logic (i.e., how the plugin works) from presentation logic (i.e., how it looks). Two separate pieces of logic are more easily maintained and swapped if necessary. An example is to have two different classes — one for displaying the end results, and one for the admin settings page. And there’s no reason you can’t keep your presentation logic in separate files.Putting presentation logic in functions/methods is just messy and lazy.

Use Transients to Store Off Site Information

If you provide a service via an API, it’s best to store that information so future queries can be done faster, and the load on your service is lessened. WordPress transients can be used to store data for a certain amount of time.

Logging Data

You may want to log data that can be useful for debugging purposes. This is great with two conditions:

  1. Allow any logging as an ‘opt in’.
  2. Use the WC_Logger class. A user can then view logs on their system status page.

If adding logging to your extension, here’s a snippet for presenting a link to the logs, in a way the extension user can easily make use of:

$label = __( 'Enable Logging', 'your-textdomain-here' );
$description = __( 'Enable the logging of errors.', 'your-textdomain-here' );

if ( defined( 'WC_LOG_DIR' ) ) {
  $log_url = add_query_arg( 'tab', 'logs', add_query_arg( 'page', 'wc-status', admin_url( 'admin.php' ) ) );
  $log_key = 'your-plugin-slug-here-' . sanitize_file_name( wp_hash( 'your-plugin-slug-here' ) ) . '-log';
  $log_url = add_query_arg( 'log_file', $log_key, $log_url );

  $label .= ' | ' . sprintf( __( '%1$sView Log%2$s', 'your-textdomain-here' ), '<a href="' . esc_url( $log_url ) . '">', '</a>' );
}

$form_fields['wc_yourpluginslug_debug'] = array(
  'title' => __( 'Debug Log', 'your-textdomain-here' ),
  'label' => $label,
  'description' => $description,
  'type' => 'checkbox',
  'default' => 'no'
);

UI for service integration plugins

If your plugin relies on connecting to an external service (for example, your service is a payment gateway, shipping method, or other service integration), it’s important to inform your users that a connection is required in order to use the plugin.

To do this, we recommend an admin notice, linking to the specific service integration screen within WooCommerce.

The following code was taken directly out of an integration we WooCommerce developed with Mailchimp, and forms part of a larger PHP class. The code has been preserved, rather than modified to be an example, for your reference in the context of a PHP class.

// Display an admin notice, if setup is required.
add_action( 'admin_notices', array( $this, 'maybe_display_admin_notices' ) );

/**
 * Display an admin notice, if not on the integration screen and if the account isn't yet connected.
 * @access public
 * @since 1.0.0
 * @return void
 */
public function maybe_display_admin_notices () {
  if ( isset( $_GET['page'] ) && 'wc-settings' == $_GET['page'] && isset( $_GET['section'] ) && 'mailchimp' == $_GET['section'] ) return; // Don't show these notices on our admin screen.

  // Find a different method of retrieving this value.
  $api_key = WC()->integrations->integrations['mailchimp']->get_option( 'wc_mailchimp_api_key' );

  if ( '' == $api_key ) {
    $url = $this->get_settings_url();
    echo '<div class="updated fade"><p>' . sprintf( __( '%sWooCommerce MailChimp is almost ready.%s To get started, %sconnect your MailChimp account%s.', 'woocommerce-mailchimp-integration' ), '<strong>', '</strong>', '<a href="' . esc_url( $url ) . '">', '</a>' ) . '</p></div>' . "\n";
  }
} // End maybe_display_admin_notices()

/**
 * Generate a URL to our specific settings screen.
 * @access public
 * @since 1.0.0
 * @return string Generated URL.
 */
public function get_settings_url () {
  $url = admin_url( 'admin.php' );
  $url = add_query_arg( 'page', 'wc-settings', $url );
  $url = add_query_arg( 'tab', 'integration', $url );
  $url = add_query_arg( 'section', 'mailchimp', $url );

  return $url;
}

Lovingly stolen & modified from WooCommerce Docs