A modern, pure-PHP WordPress plugin boilerplate with built-in licensing, SaaS support, WooCommerce integration, and self-hosted updates. Zero Node.js. Zero npm.
- PHP 8.1+
- WordPress 6.0+
- Composer 2.x
# 1. Clone/fork the template
git clone https://github.com/your-org/wordpress-plugin-template.git my-plugin
cd my-plugin
# 2. Edit src/Config.php — set your plugin name, slug, namespace, author, URLs, and mode
# 3. Run the setup script (renames files, namespaces, headers — everything)
php setup.php
# 4. Install dependencies and rebuild the autoloader
composer install
composer dump-autoload
# 5. Delete setup.php — you won't need it again
rm setup.phpAfter setup, src/Config.php is the only file you edit for configuration. All other files reference Config:: constants at runtime.
your-plugin.php Main plugin entry point + escape hatches
setup.php One-time setup script (delete after use)
emergency-uninstall.php Standalone emergency cleanup (CLI)
uninstall.php WordPress uninstall hook
composer.json Dependencies + PSR-4 autoloading
phpunit.xml PHPUnit 10 configuration
phpstan.neon PHPStan level 6
phpcs.xml WordPress coding standards
.editorconfig Editor-agnostic formatting
.github/workflows/ci.yml GitHub Actions CI (PHP 8.1–8.4)
src/
Config.php Single source of truth for all identifiers
Plugin.php Singleton bootstrap, mode-aware loading
Admin/
AdminAPI.php Form field rendering, validation, metaboxes
Settings.php Tabbed settings page (WordPress Settings API)
License/
LicenseClient.php Activate/deactivate/validate against any API
FeatureGate.php Tier checks, feature flags, usage metering
LicenseAdmin.php License key admin UI with status display
PostType/
PostType.php Custom post type registration helper
Taxonomy/
Taxonomy.php Custom taxonomy registration helper
Update/
UpdateChecker.php Self-hosted plugin updates via WP's update system
WooCommerce/
WooCommerceBootstrap.php WC detection, HPOS compat, license-gated loading
WooCommerceSettings.php Custom settings tab under WooCommerce > Settings
WooCommerceRestAPI.php REST API endpoints + webhook handler (HMAC)
assets/
css/admin.css Admin stylesheet
css/frontend.css Frontend stylesheet
js/ Drop your own JS library here
tests/
bootstrap.php PHPUnit bootstrap with WP function stubs
Unit/FeatureGateTest.php Example unit tests
lang/ Translation files (.pot/.po/.mo)
Everything is configured in src/Config.php. The setup script handles the one-time structural changes (namespace, filenames, headers). After that, Config drives all runtime behaviour.
Set Config::MODE to control what loads and what requires a license:
| Mode | WP Features | WooCommerce | License gates... |
|---|---|---|---|
wp_only |
Free | Not loaded | Nothing |
wc_only |
Not loaded | Free | Nothing |
wp_licensed_wc |
Free | Gated | WooCommerce features |
wc_licensed_wp |
Gated | Free | WP features |
| Constant | Purpose |
|---|---|
NAME |
Display name shown to users |
SLUG |
URL slug, filenames, CSS handles, text domain |
PREFIX |
wp_options prefix, transient prefix, cron hook prefix |
TEXT_DOMAIN |
i18n text domain (usually same as SLUG) |
PHP_NAMESPACE |
PSR-4 namespace (set by setup.php) |
FUNCTION_NAME |
Global accessor function name (set by setup.php) |
VERSION |
Plugin version |
MODE |
Deployment mode (see table above) |
LICENSE_API_URL |
Your license server API base URL |
UPDATE_URL |
Your self-hosted update server endpoint |
REQUIRES_WP |
Minimum WordPress version |
REQUIRES_PHP |
Minimum PHP version |
The licensing module is backend-agnostic. It talks to any REST API that implements the expected contract. Compatible with:
- cubiclesoft/php-license-server (pure PHP, self-hosted)
- UpdatePulse Server (WordPress-based)
- Any custom REST API
POST /activate { license_key, site_url, plugin_version } -> { success, message, data }
POST /deactivate { license_key, site_url } -> { success, message }
POST /validate { license_key, site_url } -> { success, message, data }
GET /check { license_key } -> { success, data }
The data object returned by your API should include:
{
"tier": "pro",
"license_type": "subscription",
"features": ["woocommerce", "export_csv", "api_access"],
"expires_at": "2026-12-31",
"activations": { "used": 2, "limit": 5 },
"usage": {
"api_calls": { "used": 450, "limit": 1000 }
}
}$gate = your_plugin()->features();
// Check specific features
if ( $gate->can( 'export_csv' ) ) { /* ... */ }
// Check tier
if ( $gate->tier_at_least( 'pro' ) ) { /* ... */ }
if ( $gate->tier_is( 'enterprise' ) ) { /* ... */ }
// Check license type
if ( $gate->is_subscription() ) { /* ... */ }
if ( $gate->is_standard() ) { /* ... */ } // perpetual / one-time
// Check validity
if ( $gate->is_valid() ) { /* active + not expired */ }
if ( $gate->is_expired() ) { /* ... */ }
// Usage metering
$remaining = $gate->usage_remaining( 'api_calls' ); // int or null
if ( $gate->has_capacity( 'api_calls' ) ) { /* ... */ }Default: free < starter < pro < business < enterprise
Customise via filter:
add_filter( 'your_plugin_tier_hierarchy', function( $tiers ) {
return [ 'free', 'basic', 'premium', 'ultimate' ];
} );The UpdateChecker hooks into WordPress's native update system (pre_set_site_transient_update_plugins) to check your own server for new versions. No wordpress.org required.
Your update server should return:
{
"version": "1.2.0",
"download_url": "https://your-server.com/downloads/your-plugin-1.2.0.zip",
"requires": "6.0",
"requires_php": "8.1",
"tested": "6.7",
"changelog": "<h4>1.2.0</h4><ul><li>New feature</li></ul>"
}License key and site URL are sent as query parameters for authenticated downloads.
WooCommerce support auto-detects WC and only loads when it's active. Includes:
- HPOS compatibility (Custom Order Tables) declared automatically
- Settings tab under WooCommerce > Settings with General, API, and Display sections
- REST API at
/wp-json/{slug}/v1/with status, license, and webhook endpoints - Webhook handler with HMAC-SHA256 signature verification for receiving events from your backend
- Order completion hook with license provisioning via
{prefix}provision_licenseaction - License gating based on
Config::MODE
The webhook endpoint (POST /wp-json/{slug}/v1/webhook) handles:
license.activated/license.deactivated/license.expiredsubscription.renewed/subscription.cancelled
Verify webhooks by setting an API key in WooCommerce > Settings > {Plugin} > API and sending it as an X-Webhook-Signature header (HMAC-SHA256 of the request body).
use YourPlugin\PostType\PostType;
use YourPlugin\Taxonomy\Taxonomy;
// Register a custom post type
new PostType( 'book', 'Books', 'Book', 'A library of books.', [
'menu_icon' => 'dashicons-book',
'supports' => [ 'title', 'editor', 'thumbnail' ],
] );
// Register a taxonomy for it
new Taxonomy( 'genre', 'Genres', 'Genre', [ 'book' ] );Registration args are filterable via {prefix}{post_type}_register_args and {prefix}{taxonomy}_register_args.
Tabbed settings page under Settings > {Plugin Name} with support for:
text, email, url, number, password, textarea, checkbox, checkbox_multi, radio, select, select_multi, hidden, color, editor (wp_editor)
Add or modify tabs via the {prefix}settings_tabs filter.
Drop your .js file in assets/js/ and uncomment the enqueue lines in src/Plugin.php:
wp_enqueue_script(
Config::SLUG . '-frontend',
YOUR_PLUGIN_URL . 'assets/js/your-library.js',
[],
Config::VERSION,
true
);No build step. No npm. Just enqueue and go.
Four ways to disable the plugin, from gentle to nuclear:
| Level | Method | When to use |
|---|---|---|
| 1 | define('YOUR_PLUGIN_DISABLE', true); in wp-config.php |
Clean disable, zero code runs |
| 2 | Create .disable file in the plugin directory |
FTP/SSH access but can't edit wp-config |
| 3 | ?{prefix}safe_mode=1 on any admin URL |
wp-admin works but plugin is causing chaos |
| 4 | php emergency-uninstall.php from CLI |
WordPress is completely bricked |
Safe mode (level 3) loads nothing except an admin notice with a one-click deactivate button.
Emergency uninstall (level 4) bypasses WordPress entirely, connects directly to the database, removes all plugin data, and deactivates the plugin. Also works via wp eval-file.
All pure PHP. Zero Node.js dependencies.
# Run tests
composer test
# Static analysis (PHPStan level 6)
composer phpstan
# Coding standards (WordPress + PHP 8.1 compat)
composer phpcs
# Auto-fix coding standard violations
composer phpcbfGitHub Actions CI runs all three across PHP 8.1, 8.2, 8.3, and 8.4.
| Filter | Description |
|---|---|
{prefix}license_api_url |
Override the license server URL |
{prefix}update_url |
Override the update server URL |
{prefix}settings_tabs |
Add/modify settings tabs |
{prefix}tier_hierarchy |
Customise tier ordering |
{prefix}unlicensed_feature |
Allow features without a license |
{prefix}wp_features_allowed |
Override WP feature license gate |
{prefix}wc_features_allowed |
Override WC feature license gate |
{prefix}license_request_params |
Modify license API request params |
{prefix}meta_box_fields |
Register meta box fields for saving |
{prefix}wc_settings_sections |
Add WC settings sections |
{prefix}wc_settings |
Modify WC settings fields |
{prefix}{post_type}_register_args |
Modify CPT registration args |
{prefix}{taxonomy}_register_args |
Modify taxonomy registration args |
| Action | Description |
|---|---|
{prefix}woocommerce_loaded |
Fires after WC integrations are loaded |
{prefix}provision_license |
Fires on order completion for license provisioning |
{prefix}webhook_received |
Fires when a webhook event is received |
GPL-2.0-or-later. See LICENSE for details.