Every WordPress plugin I start begins with the same unglamorous hour: PSR-4
autoloading, a PHPCS ruleset, PHPUnit with mocked WordPress functions, a build
step for the admin JS, the GitHub Actions file. Only then do I get to the feature
I actually opened the editor for.
I packaged that hour into a generator. You answer a few prompts, tick the pieces
you need, and get a plugin that already lints, tests and ships.
One command
npx create-wp-plugin-cli
# or: npm create wp-plugin-cli
No install. Node 20+. Generated plugins target PHP 8.2 (fixed) — constructor
promotion, readonly, first-class callables throughout.
The interactive run
Twelve questions. Most have a pre-filled answer you accept with Enter; the two
that matter are the lint target and the modules multiselect.
$ npx create-wp-plugin-cli
🚀 Welcome to create-wp-plugin-cli scaffold generator!
√ 1. Plugin name: … Loyalty Points
√ 2. Plugin slug: … loyalty-points
√ 3. PHP namespace: … LoyaltyPoints
√ 4. Function/constant prefix (>= 4 chars, lowercase): … loyp
√ 5-7. Author name / email / URI …
√ 8. Description (one line): … Award and redeem loyalty points at checkout.
√ 9. Coding standard target for composer lint: › WordPress.org (standard hosting)
√ 10. Include React admin app build pipeline? … no
√ 11. Modules to include (space to toggle):
› REST API, cron, WP-CLI commands, WooCommerce integration
√ 11a. WooCommerce components: › Payment Gateway, Custom Order Status
√ 12. Output directory: … ./loyalty-points
√ Proceed with these values? … yes
[ok] Successfully scaffolded plugin "Loyalty Points" in ./loyalty-points!
--lint-target picks the PHPCS ruleset composer lint enforces: wp-org
(WordPress-Extra + Docs), vip (WordPress-VIP-Go), or both.
What you always get
Regardless of module choices:
| File | Role |
|---|---|
src/Plugin.php |
Bootloader with a re-entry guard; boot() is the composition root |
src/Core/Activator.php / Deactivator.php
|
Implement Contracts\Activatable / Deactivatable, run from the (de)activation hooks |
src/Services.php |
Static locator of memoised singletons — only emitted if a module needs a shared collaborator |
tests/Unit/Plugin_TestCase.php, tests/bootstrap.php
|
Brain Monkey base — unit tests run with no WordPress install |
composer.json, phpcs.xml, phpunit.xml.dist
|
PHPCS/WPCS(+VIP), PHPUnit, PHPCompatibility against 8.2-
|
.github/workflows/ci.yml |
PHPCS + a PHPUnit 8.2/8.3/8.4 matrix (+ more when there's JS) |
The modules
All opt-in, all freely combinable — nothing here depends on anything else.
Admin, content & data
| Module | What you get |
|---|---|
admin_settings |
A Settings API page split into Admin\Settings_Registrar / Settings_Repository / a view |
cpt_taxonomy |
PostTypes\Post_Types — a CPT + taxonomy, wired into activation |
custom_table |
dbDelta() schema + Database\Schema + an Item_Repository, with version-checked migrations |
caching |
Cache_Service — a persistent object cache or a transient fallback, chosen per call, never both |
Front-end
| Module | What you get |
|---|---|
shortcode |
A Frontend\Shortcode class registering one shortcode |
block |
Native Gutenberg block(s): block:dynamic (render.php) and/or block:static (save()). Blocks\Block_Registrar globs assets/build/blocks/*, so more blocks need no PHP |
interactivity |
A WordPress Interactivity API store — view.js + a Script Module (WP 6.5+) |
Integration points
| Module | What you get |
|---|---|
rest_api |
A WP_REST_Controller subclass with a real permission callback |
ajax_handler |
A nonce- and capability-guarded admin-ajax handler + the assets/js/main.js it enqueues |
cron |
Cron\Scheduler — a scheduled event with a worked example body |
cli |
wp <prefix> status and wp <prefix> cache clear, behind a defined( 'WP_CLI' ) guard |
elementor_widget |
Widget_Registrar auto-discovers src/Widgets/*; convention-based CSS/JS enqueue |
Tooling
| Module | What you get |
|---|---|
editor_config |
.vscode/ snippets, settings, recommended extensions |
integration_tests |
A real wp-phpunit suite (composer test:integration), phpunit-integration.xml.dist, a boot test, .wp-env.json, and a CI integration job |
WooCommerce
Pass woocommerce for all nine, or pick components. Each is a Woo\Providers\*
class wired inside a single class_exists( 'WooCommerce' ) guard, so the plugin
is inert on a site without WooCommerce.
| Component | What you get |
|---|---|
woo:gateway |
A WC_Payment_Gateway subclass + a Blocks (block checkout) payment method type |
woo:shipping |
A WC_Shipping_Method subclass |
woo:email |
A WC_Email subclass + HTML and plain-text templates |
woo:order-status |
A custom, HPOS-compliant order status |
woo:product-type |
A custom product type with data tabs/panels |
woo:blocks |
Cart & Checkout block extensions (front-end script + integration class) |
woo:action-scheduler |
An Action Scheduler-backed background task runner |
woo:store-api |
A Store API extension via ExtendSchema
|
woo:my-account |
A custom My Account endpoint (route + template) |
A JS pipeline (wp-scripts build + Playwright) is added automatically for
--react, interactivity, block, woo:gateway, or woo:blocks. Jest comes
in with --react. uninstall.php is derived — it ships only when a selected
module actually persists something (an option, a table, a scheduled event).
The architecture, in 60 seconds
Plugin is a bootloader with a re-entry guard. Its boot() is a flat list —
here's the real thing the run above produced:
public function boot(): void {
if ( $this->booted ) {
return;
}
$this->booted = true;
if ( defined( 'WP_CLI' ) && WP_CLI ) {
( new CLI\Commands() )->init_hooks();
}
( new Rest\Rest_Controller() )->init_hooks();
( new Cron\Scheduler() )->init_hooks();
if ( class_exists( 'WooCommerce' ) ) {
( new Woo\Providers\Gateway_Provider() )->init_hooks();
( new Woo\Providers\Order_Status_Provider( Services::order_status_service() ) )->init_hooks();
}
}
A "module" is a plain class with an init_hooks() method. No base class, no
interface, no auto-discovery — a class runs only because boot() names it. CLI
commands sit behind a WP_CLI check; WooCommerce providers behind the
class_exists guard.
Services is emitted only when something needs it — here, because
Order_Status_Provider takes a Services::order_status_service() injection.
Gateway_Provider needs nothing, so it's constructed bare. Services::set() /
reset() are the test seams.
Tests, per module
cd loyalty-points
composer install
composer test
Green on the first run. Every selected module gets its own unit test — the run
above generated Rest_Controller_Test, Commands_Test, Gateway_Test,
Order_Status_Service_Test, plus Services_Test and a Module_Hooks_Test that
asserts each module's init_hooks() registers exactly the hooks it claims. All
Brain Monkey, no WordPress install.
Gateway_Test, for instance, checks the gateway instantiates with id
loyp_gateway and that the unimplemented process_payment() stub fails
closed — returns ['result' => 'failure'], never marks an order paid.
For anything browser-facing:
npm install && npm run build
npm run test:e2e # Playwright — `npx @wordpress/env start` for a target site
The CI file it wrote
.github/workflows/ci.yml, unprompted:
-
phpcs —
composer validate --strict+composer lint -
phpunit — a PHP 8.2 / 8.3 / 8.4 matrix running
composer test -
node-build —
npm run build, JS/CSS lint, then Playwright E2E against awp-envsite (only when the selection has a JS side) -
integration — the
wp-phpunitsuite, when you pickedintegration_tests
Using it in scripts
Every answer is a flag:
npx create-wp-plugin-cli --yes \
--name "Loyalty Points" --prefix loyp --namespace LoyaltyPoints \
--modules "rest_api,cron,cli,woo:gateway,woo:order-status" \
--lint-target both \
--out ./loyalty-points
Piping in without --yes and no TTY exits with an error instead of hanging. A
failure partway through generation rolls back the directory it created.
When not to use it
- A one-file plugin. If it's 40 lines in a single file, this is overkill.
-
You want a DI container. It deliberately doesn't ship one — modules are
plain classes and
boot()is the composition root. -
Block-only projects are usually better served by
@wordpress/create-blockdirectly; this wraps it when you also need PHP, tests and CI around the block.
Links
- npm: https://www.npmjs.com/package/create-wp-plugin-cli
- Source (GPL-2.0-or-later): https://github.com/akshat009/create-wp-plugin-cli
Issues and PRs welcome.
Top comments (0)