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 is null)
  • 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 with myplugin_); 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.

Leonardo Losoviz

Leo writes about innovative web development trends, mostly concerning PHP, WordPress and GraphQL. You can find him at leoloso.com and twitter.com/losoviz.