WordPressプラグインを開発する際、プラグインが最初からスムーズに動作するよう、必要なデータを事前にインストールしておくことは重要なステップです。例えば、イベント管理プラグインであれば、インストールされると同時に「今後のイベント」というタイトルのページが自動生成され、今後のイベントの一覧が表示されると非常に便利です。

[event_list number="10" scope="future" status="publish"]のようなショートコードが埋め込まれたこの設定ページによって、ユーザーはドキュメントに目を通すことなく、すぐにプラグインの機能を利用することができます。

データのインストールは、プラグインを最初にインストールする際だけでなく、その後の更新にも役立ちます。例えば、カレンダービュー機能が追加された場合、プラグインが自動的に「イベントカレンダー」ページを作成し、[event_calendar status="publish"]のようなショートコードで機能を導入することができます。

他にも、データのインストールは以下のようなタスクに有用です。

  • 特定のタイトルとコンテンツを持つ新規ページの生成
  • プラグインによって作成されたカスタム投稿タイプ(CPT)のエントリを追加
  • wp_optionsテーブルへのデフォルト設定の挿入
  • ユーザー役割への機能の割り当て
  • プラグインの新しい(または改善された)機能に対して、ユーザーにメタデータを割り当てる(ユーザーがイベントの日付形式を変更できるようにし、最初にすべてのユーザーに対してデフォルト値が追加されるなど)
  • 「会議」や「スポーツ」など、プラグインのコンテキスト内で一般に使用されるカテゴリの生成

データのインストールは、重複エントリの生成を防ぐため、インクリメンタルに行われなければなりません。

「インクリメンタル」とは、段階的に行われる処理を意味します。例えば、プラグインバージョン1.1に「今後のイベント」ページが追加され、ユーザーがバージョン1.0から1.1に更新を行う際、バージョン1.1に関連するデータのみがインストールされます。また、その後バージョン1.2でカレンダー機能が追加されされても、「今後のイベント」を重複して追加することなく、「イベントカレンダー」ページのみが導入されます。

つまり、更新が行われる際には、プラグインはまず以前のバージョンを取得し、新しいバージョンに対応するデータのみをインストールすることになります。

少し前置きが長くなりましたが、今回はWordPressプラグインで初期データをインストールし、さらに更新に伴う新たなデータを継続的に追加していく方法をご紹介していきます。

現在のバージョンを提供する

インクリメンタルな処理を行うには、プラグインは現在のバージョンを追跡しなければなりません。これは通常、プラグインのメインファイルのヘッダで宣言されています。とはいえ、PHPのコメント内にあるため、そこから直接参照することはできません。この値を変数で定義し、初期化と設定を担当するPluginクラスに提供します。

<?php
/*
Plugin Name: My plugin
Version: 1.6
*/

// ヘッダと同じバージョン
$pluginVersion = '1.6';
new Plugin($pluginVersion)->setup();

Pluginクラスは、PHP 8.0のコンストラクタのプロパティ昇格機能でこのバージョンを保存し、後で参照できるようにします。

<?php

class Plugin {

  public function __construct(
    protected string $pluginVersion,
  ) {}

  public function setup(): void
  {
    // 初期化ロジック...
  }

  // ...
}

なお、プラグインを初期化して設定するロジックは、コンストラクタではなくsetupメソッドに追加されている点に注目します。これは、コンストラクタによる副作用の発生を回避するためです。Pluginクラスを拡張したり構成したりする際にバグが発生する可能性があります。

例として、最終的にPluginクラスの機能を合成するBetterPluginクラスを追加してみます。

class BetterPlugin {

  public function printSomething(): string
  {
    $pluginVersion = '1.0';
    $plugin = new Plugin($pluginVersion);
    return '<div class="wrapper">' . $plugin->printSomething() . '</div>';
  }
}

printSomethingの内部でnew Plugin()を実行するたびに、Pluginのインスタンスが作成されます。この設定のロジックをコントラクタに追加すると、Pluginオブジェクトを作成するたびに実行されることになります。今回の場合は、「今後のイベント」ページを複数回作成するのではなく、一度だけ作成することを想定しています。setupメソッドにロジックを追加することで、この問題を回避することができます。

旧バージョンを追跡する

WordPressには、置き換えたいプラグインのバージョンを取得する方法がないため、この値をデータベースwp_optionsテーブルに保存しなければなりません。

"myplugin_version"というエントリの下にバージョンを保存します。myplugin_はプラグイン名です(例:eventsmanager_version)。他のプラグインがversionオプションを追加しないとは限らないため、すべての設定の前にmyplugin_をつけて競合の可能性を回避することが重要です。

各リクエストでプラグインを読み込む際、Pluginはすでに(先の$pluginVersionから)現在のバージョンを把握しており、データベースから最後に保存されたバージョンを取得。この2つのバージョンを比較することで、プラグインの状態が決定されます。

  • 新規インストール:データベースにプラグインのバージョンエントリがない場合に検出され、初めて設定されたことを示す($storedPluginVersionnull
  • 更新:現在のバージョンがデータベースに保存されているバージョンより新しい場合はアップグレードが必要であることを示す
  • いずれにも該当しない場合は変更なし

変更が行われる場合は、prepareAndInstallPluginSetupDataを呼び出し、新規インストール(すべてのバージョンのデータをインストール)または更新(新しいバージョンのデータのみをインストール)で、適切なデータをインストールします。null許容型の変数$previousVersionが、それがどちらの状態であるかを示します($previousVersionnullの場合は、新規インストール)。

このメソッドを呼び出した後、現在のプラグインバージョンをデータベースに保存し、「最後の保存したバージョン」にする必要があります。これは、prepareAndInstallPluginSetupDataを呼び出してから行い、このメソッドがエラーを生成し(RuntimeExceptionを投げるなど)、データがインストールされなかった場合、以前のバージョンはデータベースに保存されたままとなり、次のリクエストでデータのインストールが試行されます。

<?php

class Plugin {

  // ...

  public function setup(): void
  {
    if (!is_admin()) {
      return;
    }

    $this->managePluginDataVersioning();
  }

  /**
   * プラグインが新規にインストールされ
   * 有効化または更新されたばかりの場合は適切なデータをインストール
   */
  protected function managePluginDataVersioning(): void
  {
    $myPluginVersionOptionName = 'myplugin_version';
    $storedPluginVersion = get_option($myPluginVersionOptionName, null);

    // メインプラグインが有効化されているか、更新されているかをチェック
    $isPluginJustFirstTimeActivated = $storedPluginVersion === null;
    $isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;

    // 変更がなければ何もしない
    if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
      return;
    }

    \add_action(
      'init',
      function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
        $this->prepareAndInstallPluginSetupData($storedPluginVersion);

        // データベースの更新
        update_option($myPluginVersionOptionName, $this->pluginVersion);
      }
    );
  }

  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    // ロジックの記述...
  }
}

prepareAndInstallPluginSetupData(およびそれに続くデータベース更新)は、initアクションフックで実行されます。これは、CMSからのすべてのデータが検索と操作の準備ができているかどうかを確認するためです。

特にタクソノミ(タグとカテゴリー)は、initフックの前にはアクセスできません。プラグインのインストールプロセスでCPTエントリを作成し、カスタムカテゴリーを割り当てる必要がある場合、このプロセスはinitフック以降でしか実行できません。

リクエストのたびにデータベースから最後に保存されたバージョンにアクセスするのは、パフォーマンスの観点から理想的とは言えません。これを改善するには、プラグインが必要とするすべてのオプションを配列にまとめ、単一のエントリに保存し、データベースへの単一の呼び出しですべてにアクセスします。

例えば、プラグインがイベントの日付を表示するmyplugin_date_formatオプションも保存する必要がある場合、versiondate_formatのプロパティを持つmyplugin_optionsというエントリを作成することができます。

最後に保存されたバージョンにアクセスするには、PHPコードを以下のように変更します。

<?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 {
        // ...

        // データベースの更新
        $myPluginOptions['version'] = $this->pluginVersion;
        update_option($myPluginOptionsOptionName, $myPluginOptions);
      }
    );
  }
}

重複データをインストールする同時リクエストを回避する

2名以上のユーザーが同時にWordPress管理画面にアクセスすると、インストールプロセスが2回以上発生する可能性があります。同じデータが重複してインストールされるのを避けるためには、データをインストールする最初のリクエストだけを許可するフラグとしてtransientを使用します。

  • myplugin_installing_plugin_setup_dataが存在するかどうかをtransientが確認し(myplugin_をつけること)、存在する場合は他のプロセスがデータをインストールしているため何もしない
  • そうでなければ、データをインストールするのに必要な最大時間(例えば30秒)の間、transientをデータベースに保存
  • データをインストールする
  • transientを削除する

コードは以下のようになります。

<?php

class Plugin {

  // ...

  /**
   * 2つのリクエストが同時に発生した場合
   * 両方がこのロジックを実行する可能性があるため
   * transientを使って、1つのインスタンスだけが
   * データをインストールするようにする
   */
  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    $transientName = 'myplugin_installing_plugin_setup_data';
    $transient = \get_transient($transientName);
    if ($transient !== false) {
      // 現在別のインスタンスがこのコードを実行中
      return;
    }

    \set_transient($transientName, true, 30);
    $this->installPluginSetupData($previousVersion);
    \delete_transient($transientName);
  }

  protected function installPluginSetupData(?string $previousVersion): void
  {
    // 何らかの操作を実行...
  }
}

すべてのバージョンのデータをインストールする

前述したとおり、プラグインを更新する際、すべてのバージョンではなく、新しいバージョンのデータのみをインストールしなければなりません。つまり、バージョンごとにインストールするデータを管理する必要があります。

以下のコードでは、$versionCallbacks配列が、バージョンごとに実行する関数を示し、その関数がデータをインストールするロジックを実行します。すべてのリストを繰り返し、version_compareを使用してそれぞれを前のバージョンと比較し、新しい場合は対応する関数を実行してデータをインストールします。

なお、$previousVersionnull(新規インストール)であれば、すべての関数が実行されます。

class Plugin {
  /**
   * プラグインをインストールおよび有効化する際
   * または設定データを使用して新しいバージョンに更新する際の両方で実行できるよう
   * バージョンごとに段階的にインストールを提供
   *
   * プラグインの設定データは以下の場合にインストール
   *
   * - $previousVersion = null => 初めてプラグインを有効化
   * - $previousVersion < someVersion => インストールが必要なデータを持つ新しいバージョンに更新
   */
  protected function installPluginSetupData(?string $previousVersion): void
  {
    $versionCallbacks = [
      '1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
      '1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
      // ... Add more versions
    ];
    foreach ($versionCallbacks as $version => $callback) {
      /**
       * 以前のバージョンが提供された場合は
       * 対応する更新がすでに実行されているかどうかを確認してスキップ
       */
      if ($previousVersion !== null && version_compare($previousVersion, $version, '>=')) {
        continue;
      }
      $callback();
    }
  }

  protected function installPluginSetupDataForVersion1Dot1(): void
  {
    // 何かしらの操作を実行...
  }

  protected function installPluginSetupDataForVersion1Dot2(): void
  {
    // 何かしらの操作を実行...
  }
}

各バージョンのデータをインストールする

最後に、各バージョンの実際のデータ(ページの作成、CPTエントリ、オプションの追加など)をインストールします。

以下のコードは、イベント管理プラグイン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"]',
    ]);
  }

  // ...
}

次に、v1.2の「イベントカレンダー」ページを作成しますページ上でGutenbergブロックを使用して、カスタムブロック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' => [],
        ],
      ]),
    ]);
  }
}

コードをまとめる

以上で完了です。プラグインのバージョンを追跡し、適切なデータをインストールするロジックを含む、 PluginクラスのPHPコードの全貌は以下のようになります。

<?php

class Plugin {

  public function __construct(
    protected string $pluginVersion,
  ) {
  }

  public function setup(): void
  {
    if (!is_admin()) {
      return;
    }

    $this->managePluginDataVersioning();
  }

  /**
   * プラグインが新規にインストールされ
   * 有効化または更新されたばかりの場合は適切なデータをインストール
   */
  protected function managePluginDataVersioning(): void
  {
    $myPluginVersionOptionName = 'myplugin_version';
    $storedPluginVersion = get_option($myPluginVersionOptionName, null);

    // メインプラグインが有効化されているか、更新されているかをチェック
    $isPluginJustFirstTimeActivated = $storedPluginVersion === null;
    $isPluginJustUpdated = !$isPluginJustFirstTimeActivated && $storedPluginVersion !== $this->pluginVersion;

    // 変更がなければ何もしない
    if (!$isPluginJustFirstTimeActivated && !$isPluginJustUpdated) {
      return;
    }

    \add_action(
      'init',
      function () use ($myPluginVersionOptionName, $storedPluginVersion): void {
        $this->prepareAndInstallPluginSetupData($storedPluginVersion);

        // データベースの更新
        update_option($myPluginVersionOptionName, $this->pluginVersion);
      }
    );
  }

  /**
   * 2つのリクエストが同時に発生した場合
   * 両方がこのロジックを実行する可能性があるため
   * transientを使って、1つのインスタンスだけが
   * データをインストールするようにする
   */
  protected function prepareAndInstallPluginSetupData(?string $previousVersion): void
  {
    $transientName = 'myplugin_installing_plugin_setup_data';
    $transient = \get_transient($transientName);
    if ($transient !== false) {
      // 現在別のインスタンスがこのコードを実行中
      return;
    }

    \set_transient($transientName, true, 30);
    $this->installPluginSetupData($previousVersion);
    \delete_transient($transientName);
  }

  /**
   * プラグインをインストールおよび有効化する際
   * または設定データを使用して新しいバージョンに更新する際の両方で実行できるよう
   * バージョンごとに段階的にインストールを提供
   *
   * プラグインの設定データは以下の場合にインストール
   *
   * - $previousVersion = null => 初めてプラグインを有効化
   * - $previousVersion < someVersion => インストールが必要なデータを持つ新しいバージョンに更新
   */
  protected function installPluginSetupData(?string $previousVersion): void
  {
    $versionCallbacks = [
      '1.1' => $this->installPluginSetupDataForVersion1Dot1(...),
      '1.2' => $this->installPluginSetupDataForVersion1Dot2(...),
      // ...さらにバージョンを追加
    ];
    foreach ($versionCallbacks as $version => $callback) {
      /**
       * 以前のバージョンが提供された場合は
       * 対応する更新がすでに実行されているかどうかを確認してスキップ
       */
      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' => [],
        ],
      ]),
    ]);
  }
}

まとめ

WordPressプラグインは、インストール時にデータのインストールが必要になります。また、新たなバージョンに新機能が導入されている場合も、更新時にもデータをインストールしなければなりません。

今回はプラグインのバージョンを追跡し、適切なデータをインストールする方法を例を用いてご紹介しました。

データをインストールするのが有益なWordPressプラグインをご存知ですか?以下のコメント欄でお聞かせください。

Leonardo Losoviz

PHP、WordPress、GraphQLを中心に革新的なウェブ開発のトレンドについて執筆している。仕事情報は、leoloso.comとXアカウントで公開中。