Plugin Boilerplate Generator
A plugin that runs the moment you paste it in: header, activation hooks, loader, and the settings page, assets, REST route and uninstall you tick.
Enable JavaScript to change the fields; the files below are for the defaults.
What the Plugins screen shows.
Left empty it is the plugin name, slugged. The folder has to match it.
Shown on the Plugins screen. Over 150 characters it is cut for the readme's short description.
One file for something small. Classes for the usual WordPress layout. Namespaced classes for an autoloader and Composer.
Each one is a class the loader already constructs, so nothing needs wiring up by hand.
Prefixes every function, constant and option. Left empty it is the folder name with underscores.
Only used by the namespaced structure. Left empty it is the prefix in StudlyCaps.
Also becomes the readme contributor and the Composer vendor, slugged.
WordPress.org uses the first five.
Create this folder: wp-content/plugins/contact-bucket
The loader builds the plugin class on plugins_loaded, and its hooks() constructs Contact_Bucket_Settings. Nothing here needs wiring up by hand.
Most plugin boilerplates give you a folder of classes that do nothing, and a
TODO where the wiring should be. This one runs. Paste the files in, activate
it, and the settings page is under Settings, the REST route answers, and the
assets enqueue: the loader already constructs every class it wrote.
How to use
- Name the plugin. The folder and text domain default to the name, slugged, and the folder has to match.
- Choose the structure. single file for something small enough to read in one
sitting. classes for the usual WordPress layout,
class-my-plugin.phpand friends. namespaced classes for an autoloader, PSR-4 and Composer. - Tick what you want. Each one is a class, and each one is constructed by the
plugin class’s
hooks(). - Create
wp-content/plugins/<folder>/and write each file from the tabs. - Activate it.
Example
With classes and all six options, the main file requires what it wrote:
require_once CONTACT_BUCKET_PATH . 'includes/class-contact-bucket.php';
require_once CONTACT_BUCKET_PATH . 'includes/class-contact-bucket-settings.php';
require_once CONTACT_BUCKET_PATH . 'includes/class-contact-bucket-assets.php';
require_once CONTACT_BUCKET_PATH . 'includes/class-contact-bucket-rest.php';
function contact_bucket() {
static $instance = null;
if ( null === $instance ) {
$instance = new Contact_Bucket();
$instance->hooks();
}
return $instance;
}
add_action( 'plugins_loaded', 'contact_bucket' );
and the plugin class constructs the rest:
public function hooks(): void {
add_action( 'init', array( $this, 'init' ) );
( new Contact_Bucket_Settings() )->hooks();
( new Contact_Bucket_Assets() )->hooks();
( new Contact_Bucket_Rest() )->hooks();
}
Two details in there are the point of the whole thing. The instance is built on
plugins_loaded, not at the bottom of the file, so every other plugin has had
its chance to add the filters yours reads. And hooks are registered in hooks()
rather than in a constructor, so a class can be built in a test without
attaching itself to WordPress.
The REST route’s permission callback is a real capability check:
'permission_callback' => array( $this, 'can_read' ),
Not __return_true, which is how plugins end up with public endpoints they did
not mean to publish.
Pitfalls
The folder name has to match the text domain. WordPress.org builds translation files from the slug, and a mismatch means they are never loaded.
Deactivation is not uninstall. The generated deactivation hook flushes
rewrites and deletes nothing, because a deactivated plugin is usually a plugin
about to be reactivated. Deleting data belongs in uninstall.php, which only
runs when the plugin is deleted from the Plugins screen.
flush_rewrite_rules() on activation only. Never on init: it rewrites an
option on every request and is one of the classic causes of a slow site.
There is no load_plugin_textdomain(). For a plugin hosted on
WordPress.org, WordPress has loaded translations by itself since 4.6, and calling
it too early is what produces the “translation loading was triggered too early”
notice in 6.7 and later. If you ship translations in the plugin instead, add the
call on init.
The single file structure has no autoloader. That is the point of it. If it grows past a few hundred lines, regenerate with classes and move the work across; the loader is the only thing that changes.
Compatibility
Every file is also downloadable as a single .zip, built in your browser. The archive stores the files rather than deflating them, which is what WordPress’s own installer reads, and the whole set goes in a folder named after the plugin, so Plugins → Add New → Upload installs it directly.
The generated code targets WordPress 6.5 and PHP 7.4, which is what the defaults
say. Two pieces need a recent WordPress: the array( 'strategy' => 'defer' )
argument to wp_enqueue_script() is 6.3 and later, and typed return values on
the class methods need PHP 7.4.
Every combination of structure and options was run through php -l, and the
full namespaced variant was activated in a real WordPress: the autoloader
resolves, register_setting accepts the option and its sanitiser, the REST route
answers 200 for an administrator and 401 for anyone else, and both assets
enqueue.
The file names follow the WordPress coding standards, so phpcs --standard=WordPress
finds the classes where it expects them. The Composer option adds phpcs and WPCS
as dev dependencies with lint and fix scripts.
Frequently asked questions
Which structure should I pick?
Does the settings page store anything sensitive?
<prefix>_settings, with a sanitiser that keeps only the
keys it knows. Everything that reaches the option goes through that function,
which is the property worth keeping as you add fields.Can I remove a class later?
require_once, and delete the one line in
hooks() that constructs it. That is the whole coupling.Why is hooks() separate from the constructor?
add_action cannot be instantiated in a test, and cannot be
instantiated twice without registering its hooks twice.What is missing from this that a real plugin needs?
languages/ folder with a POT file. The
first two depend on your toolchain; the third is generated by wp i18n make-pot
in one command, which is why it is not duplicated here.