When developing a WordPress plugin, one crucial step is to pre-install essential data, ensuring the plugin operates smoothly from the get-go. Take, for example, an events manager plugin. Upon installation, it’s immensely beneficial if the plugin automatically generates a page titled Upcoming Events, displaying a list of future events.
This pre-configured page, embedded with a shortcode like [event_list number="10" scope="future" status="publish"]
, allows users to immediately leverage the plugin’s capabilities without reading through its documentation.
Installing data is helpful not only when first installing the plugin, but also when subsequently updating it. For instance, if an update introduces a calendar view feature, the plugin can automatically create a new page, Events Calendar, showcasing this addition through a shortcode such as [event_calendar status="publish"]
.
In general, the scope of data installation spans various needs:
- Generating new pages with specific titles and contents.
- Adding entries for custom post types (CPTs) created by the plugin.
- Inserting default settings into the
wp_options
table - Assigning new capabilities to user roles
- Assigning metadata to users, for new or updated features provided by the plugin (e.g. users could change the event date format, and a default value is first added for all users)
- Creating categories commonly used within the plugin’s context, such as “conferences” or “sports.”
Installing data must be an incremental process, otherwise we could create duplicate entries.
For example, if a plugin version 1.1 introduces an Upcoming Events page and a user updates from version 1.0, only the new data relevant to version 1.1 should be installed. This incremental update ensures that when version 1.2 rolls out with the calendar feature, only the new Events Calendar page is added, avoiding any duplication of the Upcoming Events page.
Hence, when updated, the plugin must retrieve what previous version was installed, and install the data that corresponds to the new version(s) only.
This article explains how to install initial data, and keep adding new data with further updates, in our WordPress plugins.
Providing the current version
To handle the incremental process, the plugin must track its current version, typically declared in the header of the plugin’s main file. But of course, we can’t reference it directly from there, as it is inside a PHP comment. So we also define this value in a variable and provide it to a Plugin
class responsible for initialization and configuration:
<?php
/*
Plugin Name: My plugin
Version: 1.6
*/
// Same version as in the header
$pluginVersion = '1.6';
new Plugin($pluginVersion)->setup();
The Plugin
class, leveraging PHP 8.0’s Constructor property promotion feature, stores this version, so we can reference it later on:
<?php
class Plugin {
public function __construct(
protected string $pluginVersion,
) {}
public function setup(): void
{
// Initialization logic here...
}
// ...
}
Notice how the logic to initialize and configure the plugin is added to the setup
method, not the constructor. That’s because the constructor must avoid producing side effects; otherwise, we could produce bugs when extending or composing the Plugin
class.
Let’s see how that could happen. Let’s say we eventually add a BetterPlugin
class that composes functionality from the Plugin
class:
class BetterPlugin {
public function printSomething(): string
{
$pluginVersion = '1.0';
$plugin = new Plugin($pluginVersion);
return '<div class="wrapper">' . $plugin->printSomething() . '</div>';
}
}
Whenever executing new Plugin()
inside printSomething
, a new instance of Plugin
is created. If the configuration logic were added to the constructor, it would be executed every time we create a new Plugin
object. In our case, we want to create the Upcoming Events page only once, not multiple times. By adding the logic to the setup
method, we can avoid this problem.
Tracking the previous version
WordPress does not provide a convenient way to retrieve the version of the plugin being replaced. Hence, we must store this value by ourselves in the wp_options
table of the database.
Store the version under entry "myplugin_version"
, where myplugin_
is the name of the plugin (e.g. eventsmanager_version
). It’s important to always prepend all our settings with myplugin_
, to avoid potential conflicts, as we can’t be sure that another plugin won’t add a version
option.
When loading the plugin on each request, Plugin
will already know what the current version (from property $pluginVersion
earlier on), and will retrieve the last stored version from the database. This comparison determines the plugin’s status:
- New installation: detects if the database lacks a version entry for the plugin, indicating first-time setup (i.e.
$storedPluginVersion
isnull
) - Update: Identified when the current version exceeds the database-stored version, signaling an upgrade requirement.
- Otherwise, there’s no change
Whenever there is a change, we call prepareAndInstallPluginSetupData
to install the appropriate data, whether for a new installation (in which case it must install all data for all versions) or an update (install data only for all the new versions). The nullable $previousVersion
variable indicates which situation it is ($previousVersion
is null
=> new install).
After calling this method, we must also store the current plugin version on the database, becoming the new “last stored” version. This must be done after calling prepareAndInstallPluginSetupData
so that if this method produces an error (e.g., throwing a RuntimeException
) and data is not installed, the previous version is still stored on the database, and a new round of installing data will be attempted on the next request.
<?php
class Plugin {
// ...
public function setup(): void
{
if (!is_admin()) {
return;
}
$this->managePluginDataVersioning();
}
/**
* If the plugin has just been newly-installed + activated
* or updated, install the appropriate data.
*/
protected function managePluginDataVersioning(): void
{
$myPluginVersionOptionName = 'myplugin_version';
$storedPluginVersion = get_option($myPluginVersionOptionName, null);
// Check if the main plugin has been activated or updated
$isPluginJustFirstTimeActivated = $storedPluginVersion === null;
$isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;
// If there were no changes, nothing to do
if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
return;
}
\add_action(
'init',
function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
$this->prepareAndInstallPluginSetupData($storedPluginVersion);
// Update on the DB
update_option($myPluginVersionOptionName, $this->pluginVersion);
}
);
}
protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
{
// Installation logic...
}
}
Notice that prepareAndInstallPluginSetupData
(and the subsequent DB update) is executed on the init
action hook. This is to make sure that all data from the CMS is ready for retrieval and manipulation.
In particular, taxonomies (tags and categories) cannot be accessed before the init
hook. If the plugin’s installation process needed to create a CPT entry and assign a custom category to it, this process could only run from the init
hook onwards.
Accessing the last stored version from the DB on every request is not ideal from a performance stance. To improve this, combine all the options needed by the plugin into an array, store them in a single entry, and then access them all with a single call to the DB.
For instance, if the plugin also needed to store a myplugin_date_format
option to display the event date, we can create a single entry myplugin_options
with properties version
and date_format
.
To access the last stored version, the PHP code must be then adapted like this:
<?php
class Plugin {
// ...
protected function managePluginDataVersioning(): void
{
$myPluginOptionsOptionName = 'myplugin_options';
$myPluginOptions = get_option($myPluginOptionsOptionName, []);
$storedPluginVersion = $myPluginOptions['version'] ?? null;
// ...
\add_action(
'init',
function () use ($myPluginOptionsOptionName, $myPluginOptions): void {
// ...
// Update on the DB
$myPluginOptions['version'] = $this->pluginVersion;
update_option($myPluginOptionsOptionName, $myPluginOptions);
}
);
}
}
Avoiding concurrent requests installing duplicate data
There is the possibility that the installation process may be triggered more than once if two or more users access the wp-admin at exactly the same time. To avoid the same data being installed twice or more, we use a transient as a flag to allow only the first request to install data:
- Check if transient
myplugin_installing_plugin_setup_data
exists (once again, this name must be prepended withmyplugin_
); if so, do nothing (as some other process is installing the data) - Otherwise, store the transient in the database for a reasonable maximum amount of time to install the data (e.g., 30 seconds)
- Install the data
- Delete the transient
Here is the code:
<?php
class Plugin {
// ...
/**
* Use a transient to make sure that only one instance
* will install the data. Otherwise, two requests
* happening simultaneously might execute the logic
*/
protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
{
$transientName = 'myplugin_installing_plugin_setup_data';
$transient = \get_transient($transientName);
if ($transient !== false) {
// Another instance is executing this code right now
return;
}
\set_transient($transientName, true, 30);
$this->installPluginSetupData($previousVersion);
\delete_transient($transientName);
}
protected function installPluginSetupData(?string $previousVersion): void
{
// Do something...
}
}
Installing data for all versions
As mentioned earlier, if updating the plugin, we must only install the data for the new versions, not all of them. That means that we need to manage what data to install version by version.
In the code below, array $versionCallbacks
indicates what function to execute for each version with the function executing the logic to install the data. We iterate the list of all of them, compare each against the previous version using version_compare
and, if it is greater, execute the corresponding function to install the corresponding data.
Notice that if $previousVersion
is null
(i.e., it’s a new install), then all functions are executed.
class Plugin {
/**
* Provide the installation in stages, version by version, to
* be able to execute it both when installing/activating the plugin,
* or updating it to a new version with setup data.
*
* The plugin's setup data will be installed if:
*
* - $previousVersion = null => Activating the plugin for first time
* - $previousVersion < someVersion => Updating to a new version that has data to install
*/
protected function installPluginSetupData(?string $previousVersion): void
{
$versionCallbacks = [
'1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
'1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
// ... Add more versions
];
foreach ($versionCallbacks as $version => $callback) {
/**
* If the previous version is provided, check if the corresponding update
* has already been performed, then skip
*/
if ($previousVersion !== null && version_compare($previousVersion, $version, '>=')) {
continue;
}
$callback();
}
}
protected function installPluginSetupDataForVersion1Dot1(): void
{
// Do something...
}
protected function installPluginSetupDataForVersion1Dot2(): void
{
// Do something...
}
}
Installing data for each specific version
Finally, we must install the actual data (create a page, a CPT entry, add an option, etc) for each version.
In this code, we add the Upcoming Events page for the events manager plugin, for v1.1
:
class Plugin {
// ...
protected function installPluginSetupDataForVersion1Dot1(): void
{
\wp_insert_post([
'post_status' => 'publish',
'post_type' => 'page',
'post_title' => \__('Upcoming Events', 'myplugin'),
'post_content' => '[event_list number="10" scope="future"]',
]);
}
// ...
}
Then, we create the Events Calendar page for v1.2
(in this case, using Gutenberg blocks on the page, adding a custom block called event-calendar
):
class Plugin {
// ...
protected function installPluginSetupDataForVersion1Dot2(): void
{
\wp_insert_post([
'post_status' => 'publish',
'post_type' => 'page',
'post_title' => \__('Events Calendar', 'myplugin'),
'post_content' => serialize_blocks([
[
'blockName' => 'myplugin/event-calendar',
'attrs' => [
'status' => 'publish',
],
'innerContent' => [],
],
]),
]);
}
}
All code together
We are done! The whole PHP code for the Plugin
class, containing the logic to track the plugin version and install the appropriate data, is the following:
<?php
class Plugin {
public function __construct(
protected string $pluginVersion,
) {
}
public function setup(): void
{
if (!is_admin()) {
return;
}
$this->managePluginDataVersioning();
}
/**
* If the plugin has just been newly-installed + activated
* or updated, install the appropriate data.
*/
protected function managePluginDataVersioning(): void
{
$myPluginVersionOptionName = 'myplugin_version';
$storedPluginVersion = get_option($myPluginVersionOptionName, null);
// Check if the main plugin has been activated or updated
$isPluginJustFirstTimeActivated = $storedPluginVersion === null;
$isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;
// If there were no changes, nothing to do
if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
return;
}
\add_action(
'init',
function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
$this->prepareAndInstallPluginSetupData($storedPluginVersion);
// Update on the DB
update_option($myPluginVersionOptionName, $this->pluginVersion);
}
);
}
/**
* Use a transient to make sure that only one instance
* will install the data. Otherwise, two requests
* happening simultaneously might both execute
* this logic
*/
protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
{
$transientName = 'myplugin_installing_plugin_setup_data';
$transient = \get_transient($transientName);
if ($transient !== false) {
// Another instance is executing this code right now
return;
}
\set_transient($transientName, true, 30);
$this->installPluginSetupData($previousVersion);
\delete_transient($transientName);
}
/**
* Provide the installation in stages, version by version, to
* be able to execute it both when installing/activating the plugin,
* or updating it to a new version with setup data.
*
* The plugin's setup data will be installed if:
*
* - $previousVersion = null => Activating the plugin for first time
* - $previousVersion < someVersion => Updating to a new version that has data to install
*/
protected function installPluginSetupData(?string $previousVersion): void
{
$versionCallbacks = [
'1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
'1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
// ... Add more versions
];
foreach ($versionCallbacks as $version => $callback) {
/**
* If the previous version is provided, check if the corresponding update
* has already been performed, then skip
*/
if ($previousVersion !== null && version_compare($previousVersion, $version, '>=')) {
continue;
}
$callback();
}
}
protected function installPluginSetupDataForVersion1Dot1(): void
{
\wp_insert_post([
'post_status' => 'publish',
'post_type' => 'page',
'post_title' => \__('Upcoming Events', 'myplugin'),
'post_content' => '[event_list number="10" scope="future" status="publish"]',
]);
}
protected function installPluginSetupDataForVersion1Dot2(): void
{
\wp_insert_post([
'post_status' => 'publish',
'post_type' => 'page',
'post_title' => \__('Events Calendar', 'myplugin'),
'post_content' => serialize_blocks([
[
'blockName' => 'myplugin/event-calendar',
'attrs' => [
'status' => 'publish',
],
'innerContent' => [],
],
]),
]);
}
}
Summary
WordPress plugins often need to install data upon installation. In addition, as newer versions of the plugin provide new features, the plugin may also need to install data when updated.
In this article, we learned how to track versions and install the appropriate data for our plugins.
Do you have a WordPress plugin that can benefit from installing data? Let us know in the comments.
Leave a Reply