The WP Plugin Base is a feature-rich composer project to be included in WordPress plugins. It provides a good base scaffolding, as well as many common base features to speed up development.
Note: We recommend scoping your end plugin using PHP Scoper to prevent version conflicts across plugins.
The plugin base is used by the Bernskiold Media WP Plugin Scaffold. We generally use this to start off new plugin development.
To use the Plugin Base, load it via composer in your plugin:
composer require bernskioldmedia/wp-plugin-base
In this plugin base you'll find classes to help you create:
- Base Plugin (Main Plugin Class)
- Installer
- Data Stores (Custom Post Types & Taxonomies)
- Data (Getters/Setters for a data store)
- FacetWP Facet
- ACF Field Group
- Custom REST Endpoint
- Customizer Settings
- Admin Columns Pro Sets
- Asset Loading
- Custom Blocks
- Jobs Class
Included in the plugin base is our forge
CLI that helps you scaffold new classes that extend the base classes provided by the plugin base. The CLI is automatically installed to
your vendor's bin folder for easy access.
For even simpler access, you may define a composer script in your project, such as our scaffold has:
"scripts": {
"forge": "./vendor/bin/forge",
}
You can then access the CLI by running: composer forge COMMAND
.
The following commands are available:
Generate a class that can be used to add a customizer section and settings.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.--prefix="my-plugin"
An optional prefix for your settings.
Generate a data class where getters and setters are used to interact with a data store.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.--type="taxonomy"
Create a taxonomy data class instead of the default custom post type.
forge make:cpt {name}
- Custom Post Type Data Store Class
Generate a custom post type data store.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.
--textdomain="my-plugin"
The plugin textdomain.
Generate a taxonomy data store.
Options:
-
--namespace="MyPlugin\Namespace"
The root plugin namespace. -
--textdomain="my-plugin"
The plugin textdomain.
Generate a class to register a FacetWP Facet based on a FacetWP facet export.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.
Generate a class to register an ACF field group based on the ACF field group export.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.
Generate a class to create a custom REST API endpoint.
Options:
--namespace="MyPlugin\Namespace"
The root plugin namespace.
Generate all the files needed for a custom block.
Options:
--dynamic
or-d
Creates a dynamic block which renders using PHP.--namespace="MyPlugin\Namespace"
The root plugin namespace. Only needed if dynamic is set.
Example:
./bin/forge make:block bm my-block
Generates the webpack config file as well as requires all the various NPM dependencies for building blocks.
We typically need to boot (run) a series of classes and hooks/init functions when the plugin loads. This to boot further features.
Normally we do this by running the function (usually containing action calls) within the init_hooks()
function.
To make this simpler, you can add your class to the $boot = []
array on the base plugin class. It will then be run on the init hooks function automatically. This way you don't
have to extend the init hooks method for simple bootable operations.
Note: A function that you load via the boot property must implement the Hookable
interface.
protected static $boot = [
Assets::class,
];
The abstract class AssetManager
can be used to load styles and script both public or admin intelligently.
It gives you two options. Either defining all values yourself, or if you are using @wordpress/scripts
(which we almost always are), rely on the name.asset.php
file that it
generates for dependencies and version.
The process is the same for public/admin scripts and styles.
When extending the class, you can define four arrays and methods. The array properties auto-registers the scripts, while the four methods are responsible for enqueuing.
Because you likely want to enqueue the scripts conditionally, the helper library doesn't do this automatically.
Registering scripts by using the asset meta file, requires just the simple config:
protected static array $public_scripts = [
'my-script' => 'assets/scripts/dist'
];
This configuration assumes the following structure:
- Script is called
my-script.js
. - Script is placed in the
assets/scripts/dist
folder. - There is a
my-script.asset.php
file next to the script file in the same folder.
Alternatively, you can define the full settings (with their defaults):
protected static array $public_scripts = [
'my-script' => [
'subfolder' => 'assets/scripts/dist', // Required.
'dependencies' => [], // Defaults to empty array if not set.
'version' => '1.0', // Defaults to plugin version if not set.
'in_footer' => true, // Defaults to true if not set.
],
];
Scripts are registered with the array key name as handle.
Registering styles by using the asset meta file, requires just the simple config:
protected static array $public_styles = [
'my-style' => 'assets/styles/dist'
];
This configuration assumes the following structure:
- Stylesheet is called
my-style.css
. - Stylesheet is placed in the
assets/styles/dist
folder. - There is a
my-style.asset.php
file next to the stylesheet file in the same folder.
Alternatively, you can define the full settings (with their defaults):
protected static array $public_styles = [
'my-style' => [
'subfolder' => 'assets/styles/dist', // Required.
'dependencies' => [], // Defaults to empty array if not set.
'version' => '1.0', // Defaults to plugin version if not set.
'media' => 'screen', // Defaults to 'all' if not set.
],
];
Styles are registered with the array key name as handle.
We don't automatically enqueue scripts or styles. Instead we have four magic methods that hook in correctly when defined. This allows you to conditionally enqueue scripts and styles easily as needed.
When they exist in the file, they are hooked appropriately:
public static function enqueue_public_scripts(): void
public static function enqueue_admin_scripts(): void
public static function enqueue_public_styles(): void
public static function enqueue_admin_styles(): void
They are run at priority 100 by default. To override, you can set the protected static int $enqueue_priority
.
By default we run the registration at priority 10. To customize, set the protected static int $register_priority
to a custom value.
To easily share Admin Columns Pro sets between environments, it's often a good idea to commit them. To export and save them easily, the plugin base hooks into Admin Columns for custom data stores.
First, include the HasAdminColumns
trait on the main plugin class.
import BernskioldMedia\WP\PluginBase\BasePlugin;
import BernskioldMedia\WP\PluginBase\Admin\HasAdminColumns;
class Plugin extends BasePlugin {
use HasAdminColumns;
// ...
}
Then, on the custom data stores that you want to store admin column sets for, set the $store_admin_columns
property to true:
/**
* When set to true the admin columns in Admin Columns Pro
* will be stored in a directory in this plugin as opposed to in the database.
*
* @var bool
*/
protected static $store_admin_columns = false;
Adding a new data store is best done via the Forge CLI forge make:cpt MyDataStore
.
To register the data store, add it to the $data_stores
property in the main plugin class:
/**
* The data stores (class names) that will be loaded
* alongside this plugin.
*
* @var string[]
*/
protected static $data_stores = [
MyDataStore::class,
];
Note: When creating a data store, you should also create a matching Data class with getters and setters.
Special data store features:
- Extend the
$metadata
array on the data store with names of the data class get/set method to automatically add the data to the REST API endpoint for the data store. - Automated permissions handling on the data store level. You may adjust default permissions by adjusting the permissions array.
- Simple hook for adjusting data store queries on
pre_get_posts
. Just add theprotected static query_modifications( \WP_Query $query ): \WP_Query;
method to the data store class. It will be automatically loaded. - Create Field Groups and add their class names to the
$field_groups
array to automatically load them. - Disable the block editor by setting the class property:
protected static $block_editor = false;
- CRUD methods for the data store.
By creating a data class, we have a simple method for interacting with data from the data store. It's best created with the Forge CLI forge make:data {name}
. It's name should
match the data store.
It's used as: $data = new DataClass( $id );
. Within the class $this->get_id()
will always get you the current object ID.
Note: When creating a data class for a taxonomy, the TaxonomyData
class will ensure ACF taxonomy compatibility automatically.
Add your getters and setters to the data class. It has built-in support for ACF fields especially through the following methods:
get_prop( $field_key )
set_prop( $field_key, $value )
get_date_prop( $field_key, $format )
get_bool_prop( $field_key ): bool
get_term_id_prop( $field_key ): ?int
get_taxonomy_prop( $field_key, bool $multiple )
get_taxonomy_string( $field_key, $separator, $key ): string
It also has some easy querying features that return the data class.
::find( $name )
looks up an object based on the name.::find_or_create( $name )
looks up an object based on the name, and creates it if it doesn't exist.
You may convert all the data for an object into an array with the to_array()
method.
We find ourselves increasingly creating a bunch of plugins that add one or more blocks to the block editor. This requires a few functions to load and register.
All you need to do is include the Has_Blocks
trait in the base plugin class.
Blocks are assumed to be placed in a blocks
subfolder, and they are being built to dist/blocks
.
use BernskioldMedia\WP\PluginBase\BasePlugin;
use BernskioldMedia\WP\PluginBase\Blocks\Has_Blocks;
class My_Plugin extends BasePlugin {
use Has_Blocks;
}
The trait assumes the following folder structure:
blocks/
is the location of all blocks with one folder per block. The index.js
file in each block folder is the entrypoint for the build script.
dist/blocks
is the location of built JavaScript blocks with the same filename as the main folder name.
languages/
is the location of the translation files. The handle and domain are both set to {$block_prefix}-{$block_name}
.
The trait will default to using the 'bm' prefix for blocks. Blocks are named with a prefix to scope them to the project they are a part of. This also influences how the script and style handles are registered ($prefix-block-$block_name).
Your project probably requires its custom prefix. You can set it on the main plugin class like so:
use BernskioldMedia\WP\PluginBase\BasePlugin;
use BernskioldMedia\WP\PluginBase\Blocks\Has_Blocks;
class My_Plugin extends BasePlugin {
use Has_Blocks;
// Set my custom block prefix.
protected static string $block_prefix = 'my-prefix';
}
When you have a long-running process that you need to run in the background, a job is what you want to run.
use BernskioldMedia\WP\PluginBase\Jobs\Job;
class My_Job extends Job {
protected $action = 'clone_media_job';
/**
* Process the Queue
*
* @param array $data
*
* @return bool
*/
protected function task( $data ) {
// Do things here...
return false;
}
protected function complete() {
parent::complete();
// This code will run once the entire job is complete.
}
}
You can then dispatch your job like this:
$job = new MyJob();
foreach( $items as $item ) {
$item_data = [];
$job->push_to_queue( $item_data );
}
$job->save()->dispatch();
The framework contains an abstract class that you can extend to add a bulk action to one of the post type table views.
use BernskioldMedia\WP\PluginBase\Admin\Bulk_Action;
class My_Bulk_Action extends Bulk_Action {
protected static $scope = 'edit-post';
protected static $slug = 'my_bulk_action';
public static function process( int $object_id ): void {
// Do something with each post here.
}
protected static function get_name(): string {
return __( 'My Bulk Action', 'TEXTDOMAIN' );
}
}
Don't forget to load the bulk action by adding it to the $boot
array in the main plugin file.
Extend this abstract class to add a new tab to the network admin website edit screen.
use BernskioldMedia\WP\PluginBase\Admin\Multisite_Tab;
class My_Tab extends Multisite_Tab {
protected static string $nonce = 'my-tab-nonce';
protected static string $slug = 'my-tab';
protected static string $capability = 'manage_sites';
protected static function get_title(): string {
return __( 'My Tab', 'TEXTDOMAIN' );
}
public static function notice(): void {
if ( ! isset( $_GET['updated'], $_GET['page'] ) || self::$slug !== $_GET['page'] ) {
return;
}
?>
<div class="notice is-dismissible updated">
<p><?php esc_html_e( 'Success message.', 'TEXTDOMAIN' ); ?></p>
</div>
<?php
}
public static function save( WP_Site $site, $request_data ): void {
// Handle saving.
}
public static function render(): void {
$site = self::get_site_from_request();
if ( ! $site ) {
return;
}
?>
<div class="wrap">
<p>Tab Content</p>
</div>
<?php
}
}
Don't forget to load the tab by adding it to the $boot
array in the main plugin file.