WooCommerce extension development best practices
Following these best practices will help you create high-quality, secure, and maintainable WooCommerce extensions that work well with other plugins and provide a great experience for merchants.
Core principles
WooCommerce extensions should:
Plugin structure and naming
Main plugin file naming
The main plugin file should adopt the name of the plugin directory.
plugin-name/
└── plugin-name.php ✓ Correct
plugin-name/
└── main.php ✗ Incorrect
Text domain
The text domain should match your plugin directory name. Do not use underscores.
// Directory: woocommerce-my-extension
// Text domain: woocommerce-my-extension ✓ Correct
load_plugin_textdomain(
'woocommerce-my-extension',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
// Text domain: woocommerce_my_extension ✗ Incorrect
Namespacing and avoiding conflicts
Use unique prefixes
Prefix all functions, classes, constants, and global variables with a unique identifier to avoid naming conflicts.
// Good: Prefixed function names
function wcme_process_order( $order_id ) {
// Function code
}
function wcme_get_custom_field() {
// Function code
}
// Bad: Generic function names
function process_order( $order_id ) { // ✗ May conflict
// Function code
}
Use namespaces for classes
Use PHP namespaces to organize your code and prevent class name collisions.
namespace MyCompany\WooCommerce\MyExtension;
class Product_Handler {
// Class code
}
class Order_Manager {
// Class code
}
Prefix options and transients
// Good: Prefixed option names
add_option( 'wcme_settings', $settings );
set_transient( 'wcme_product_cache', $data, HOUR_IN_SECONDS );
// Bad: Generic option names
add_option( 'settings', $settings ); // ✗ May conflict
set_transient( 'cache', $data, HOUR_IN_SECONDS ); // ✗ May conflict
Avoid custom database tables
Whenever possible, use WordPress post types, taxonomies, and options instead of creating custom database tables.
// Good: Use WordPress post types
register_post_type( 'wcme_custom_order', array(
'labels' => array(
'name' => __( 'Custom Orders', 'woocommerce-my-extension' ),
),
'public' => false,
'show_ui' => true,
'supports' => array( 'title', 'custom-fields' ),
) );
// Use post meta for additional data
update_post_meta( $post_id, '_wcme_custom_field', $value );
If you must create custom tables, see the data storage primer for guidance.
Use transients for caching
Store expensive API calls or calculations in transients to improve performance.
function wcme_get_api_data() {
// Try to get cached data
$data = get_transient( 'wcme_api_data' );
if ( false === $data ) {
// Data not cached, fetch from API
$response = wp_remote_get( 'https://api.example.com/data' );
if ( ! is_wp_error( $response ) ) {
$data = json_decode( wp_remote_retrieve_body( $response ), true );
// Cache for 1 hour
set_transient( 'wcme_api_data', $data, HOUR_IN_SECONDS );
}
}
return $data;
}
Load scripts conditionally
Only enqueue scripts and styles where they’re needed.
function wcme_enqueue_admin_scripts( $hook ) {
// Only load on our settings page
if ( 'woocommerce_page_wcme-settings' !== $hook ) {
return;
}
wp_enqueue_script(
'wcme-admin',
plugins_url( 'assets/js/admin.js', __FILE__ ),
array( 'jquery' ),
'1.0.0',
true
);
}
add_action( 'admin_enqueue_scripts', 'wcme_enqueue_admin_scripts' );
Security best practices
Prevent direct file access
Add this line to the top of all PHP files to prevent direct access.
defined( 'ABSPATH' ) || exit;
Learn more about preventing data leaks.
Always validate and sanitize user input.
function wcme_save_settings() {
// Verify nonce
if ( ! isset( $_POST['wcme_nonce'] ) || ! wp_verify_nonce( $_POST['wcme_nonce'], 'wcme_save_settings' ) ) {
wp_die( __( 'Security check failed', 'woocommerce-my-extension' ) );
}
// Check user capabilities
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( __( 'Unauthorized', 'woocommerce-my-extension' ) );
}
// Sanitize input
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
$text = isset( $_POST['text'] ) ? sanitize_text_field( $_POST['text'] ) : '';
// Validate
if ( ! is_email( $email ) ) {
wp_die( __( 'Invalid email address', 'woocommerce-my-extension' ) );
}
// Save sanitized data
update_option( 'wcme_email', $email );
update_option( 'wcme_text', $text );
}
Escape output
Always escape data when outputting to prevent XSS attacks.
// Escape translated text
echo esc_html__( 'Hello World', 'woocommerce-my-extension' );
// Escape attributes
echo '<input type="text" value="' . esc_attr( $value ) . '" />';
// Escape URLs
echo '<a href="' . esc_url( $url ) . '">Link</a>';
// Escape HTML (when you need to allow some HTML)
echo wp_kses_post( $content );
Protect forms with nonces to prevent CSRF attacks.
// Output nonce field in form
<form method="post">
<?php wp_nonce_field( 'wcme_save_settings', 'wcme_nonce' ); ?>
<!-- Form fields -->
<input type="submit" value="Save" />
</form>
// Verify nonce when processing
if ( ! isset( $_POST['wcme_nonce'] ) || ! wp_verify_nonce( $_POST['wcme_nonce'], 'wcme_save_settings' ) ) {
wp_die( __( 'Security check failed', 'woocommerce-my-extension' ) );
}
Internationalization (i18n)
Follow i18n guidelines
Follow the Internationalization for WordPress Developers guidelines.
Use translation functions
Wrap all user-facing strings in translation functions.
// Simple translation
echo esc_html__( 'Hello World', 'woocommerce-my-extension' );
// Translation with echo
esc_html_e( 'Settings saved', 'woocommerce-my-extension' );
// Singular/plural
printf(
esc_html( _n( '%d item', '%d items', $count, 'woocommerce-my-extension' ) ),
$count
);
// Context-specific translation
echo esc_html_x( 'Read', 'past tense', 'woocommerce-my-extension' );
Localization files
All text strings should be in English (the WordPress default locale). If targeting a specific market, include appropriate translation files in your plugin package.
Learn more at Using Makepot to translate your plugin.
Code quality and maintainability
Follow WordPress coding standards
Adhere to WordPress PHP coding standards.
Key points:
- Use tabs for indentation
- Use Yoda conditions:
if ( true === $value )
- Use single quotes for strings unless interpolating variables
- Always use braces for control structures
- Follow WordPress naming conventions
Document functions and classes with PHPDoc blocks.
/**
* Get custom product data
*
* Retrieves additional product data stored by the extension.
*
* @since 1.0.0
*
* @param int $product_id Product ID.
* @param string $key Data key to retrieve.
* @return mixed Product data or false if not found.
*/
function wcme_get_product_data( $product_id, $key ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return false;
}
return $product->get_meta( '_wcme_' . $key );
}
Avoid God Objects
Break large classes and functions into smaller, focused pieces. See God Objects for more information.
// Good: Small, focused classes
class Product_Validator {
public function validate( $product_data ) {
// Validation logic only
}
}
class Product_Processor {
public function process( $product_data ) {
// Processing logic only
}
}
// Bad: One class doing too much
class Product_Handler { // ✗ Does everything
public function validate() { }
public function process() { }
public function save() { }
public function email() { }
public function render() { }
}
Separate business and presentation logic
Keep your business logic separate from presentation code.
// Good: Separate classes
class Order_Calculator {
// Business logic: Calculate totals
public function calculate_total( $order ) {
// Calculation logic
}
}
class Order_Display {
// Presentation logic: Display order
public function render_order( $order ) {
// Display logic
}
}
Remove unused code
With version control, there’s no reason to leave commented-out code. Remove it.
// Bad: Commented-out code
function wcme_process_order( $order_id ) {
$order = wc_get_order( $order_id );
// $old_status = $order->get_status();
// if ( 'pending' === $old_status ) {
// $order->update_status( 'processing' );
// }
$order->update_status( 'completed' );
}
// Good: Clean, current code only
function wcme_process_order( $order_id ) {
$order = wc_get_order( $order_id );
$order->update_status( 'completed' );
}
External libraries and dependencies
Minimize external dependencies
The use of entire external libraries is typically not recommended as this can open up the product to security vulnerabilities.
If an external library is absolutely necessary:
- Only include the strictly necessary parts
- Use a WordPress-friendly version when available
- Assume ownership and responsibility for the code
- Keep libraries updated
Executing outside code within a plugin is not allowed. Using third-party CDNs for non-service-related JavaScript and CSS is prohibited.
Example: Use WordPress versions
// Good: Use WordPress bundled library
wp_enqueue_script( 'jquery' );
wp_enqueue_script( 'jquery-ui-datepicker' );
// Bad: Load from external CDN
wp_enqueue_script(
'jquery',
'https://cdn.example.com/jquery.min.js' // ✗ Prohibited
);
Testing and quality assurance
Enable debugging during development
Test your code with WP_DEBUG mode enabled.
// In wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
This will flag issues like undefined variables and deprecated functions.
Integrate the Quality Insights Toolkit (QIT) into your development workflow.
The QIT allows you to test extensions against:
- New releases of PHP, WooCommerce, and WordPress
- Other active extensions
- Different configurations
Use the CLI tool to run tests against development builds.
Implement logging
Log data that can be useful for debugging, with two conditions:
- Make logging opt-in
- Use the WC_Logger class
function wcme_log( $message, $level = 'info' ) {
// Only log if debugging is enabled
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
$logger = wc_get_logger();
$context = array( 'source' => 'woocommerce-my-extension' );
$logger->log( $level, $message, $context );
}
// Usage
wcme_log( 'Order processed: ' . $order_id );
wcme_log( 'API error: ' . $error_message, 'error' );
Learn how to add a link to logged data in your extension.
WooCommerce-specific guidelines
Check if WooCommerce is active
Most WooCommerce plugins do not need to run unless WooCommerce is already active.
Learn how to check if WooCommerce is active.
Never use internal WooCommerce code
Classes in the Automattic\WooCommerce\Internal namespace and code marked with @internal annotations are for WooCommerce core use only. Backwards compatibility is not guaranteed.
Internal code may change, be renamed, or be removed in any future release. Using it in your extension may cause it to break when merchants update WooCommerce.
See the Internal namespace documentation for details.
Make extensions extensible
Use WordPress actions and filters to allow for modification and customization.
function wcme_calculate_custom_fee( $order ) {
$fee_amount = 5.00;
// Allow other plugins to modify the fee
$fee_amount = apply_filters( 'wcme_custom_fee_amount', $fee_amount, $order );
return $fee_amount;
}
// Allow other plugins to hook in before processing
do_action( 'wcme_before_process_order', $order );
// Process order
wcme_process_order( $order );
// Allow other plugins to hook in after processing
do_action( 'wcme_after_process_order', $order );
For more information, see Writing Extensible Plugins with Actions and Filters.
Use template files
If your plugin creates front-end output, use a templating engine so users can create custom template files in their theme’s WooCommerce folder to overwrite the plugin’s template files.
function wcme_get_template( $template_name, $args = array() ) {
// Look in theme first
$template = locate_template( array(
'woocommerce-my-extension/' . $template_name,
$template_name,
) );
// Fall back to plugin template
if ( ! $template ) {
$template = plugin_dir_path( __FILE__ ) . 'templates/' . $template_name;
}
// Extract args to variables
if ( $args && is_array( $args ) ) {
extract( $args );
}
include $template;
}
Documentation requirements
README file
All plugins need a standard WordPress README. See the WordPress plugin README file standard.
Changelog file
All plugins need a changelog file. See the changelog.txt documentation.
Follow the conventions in the example WordPress plugin header comment.
Summary checklist
Before releasing your extension:
Additional resources