WordPress│REST APIで自前のプラグイン更新サーバーを作る仕組み

WordPress.org に公開していない自作プラグインも、専用の 更新サーバー を立てれば「ダッシュボードからワンクリック更新」が可能になります。

ここでは、ライセンス認証なしの最小構成 で、自分の WordPress サイトを「自作プラグインの更新配信サーバー」にする仕組みを、ソースコードと一緒にまるごと公開します。

完成版のZIPファイルもあるので、コピペで動かしてみたい方はそのまま使えます。

目次

著者

WEB制作をしているデジタルノマド
WordPressのカスタマイズが好きで、色々と自作しています。

WordPressのカスタマイズに困ったらご相談ください!

なぜ自前の更新サーバーが必要なのか

自作プラグインを WordPress.org に公開すれば、更新は WordPress 側が面倒を見てくれます。ですが、

  • WordPress.org には公開したくないけど、自分のサイト群には配布したい
  • クライアント案件で、複数のサイトに同じプラグインを入れて一括更新したい
  • 内部ツール的なプラグインを、社内の複数サイトで使いたい

といったケースでは、自分のサーバーから ZIP を配信する仕組み が必要になります。

WordPress には pre_set_site_transient_update_plugins などのフィルタが用意されていて、ここに「最新バージョン情報」と「ZIP のダウンロード URL」を差し込むだけで、ダッシュボードに更新通知を出せます。

つまり配信側は、

  1. 「最新バージョンはいくつ?」に答える API
  2. 「ZIP をダウンロードさせる API」

の2つを REST API で提供すればいい、というのが基本設計です。

システム全体の流れ

ざっくりと処理の流れを書くと、こんな構成になります。

[クライアントサイト(プラグイン利用者)]
        │
        │ ① 更新チェック(slug + 現在バージョン)
        ▼
[更新サーバー(本プラグイン)]
        │
        │ ② 最新リリース情報 + ZIPダウンロードURL を返す
        ▼
[クライアントサイト]
        │
        │ ③ ダウンロードURLにアクセスしてZIP取得
        ▼
[更新サーバー]
        │
        │ ④ 登録されたZIPをそのまま配信
        ▼
[クライアントサイトが ZIP を解凍してプラグイン更新]

ポイントは 「バージョン情報の取得」と「ZIP のダウンロード」を分離 していることです。

WordPress 側の更新フローは、まず「新しいバージョンあるよ」という情報を取得してから、ユーザーが「更新」ボタンを押した時に改めて ZIP をダウンロードする、という2段階になっています。

サーバー側もこの構造に合わせて2つのエンドポイントを用意します。

プラグインのファイル構成

実際のファイル構成はこうなっています。

your-update-server/
├── your-update-server.php    # メインファイル(定数・require・有効化フック)
└── inc/
    ├── db.php                # テーブル定義 + ヘルパー関数
    ├── rest.php              # REST APIルート(update / download)
    └── admin.php             # 管理画面(リリース登録・一覧)

合計で約400行。ライセンス管理機能を入れると倍くらいになりますが、今回は 「とりあえず動く最小構成」 に振り切ってあります。

データベース設計

テーブルは1つだけ。wp_yus_releases というシンプルな構成です。

カラム用途
product_slugプラグインのスラッグ(例: my-plugin
versionバージョン番号(例: 0.1.3
zip_pathサーバー上のZIPファイルパス
changelog更新履歴
requires_wp / tested_wp / requires_php動作要件
homepage_urlホームページURL
is_published公開フラグ

product_slugversion の組み合わせに UNIQUE 制約を付けてあるので、同じバージョンを誤って重複登録しないようになっています。

複数のプラグインを1つのサーバーから配信したい場合も、product_slug で区別できるので問題ありません。

REST APIの設計

inc/rest.php で2つのエンドポイントを登録しています。

1. 更新チェック API: GET /wp-json/your-update/v1/update

クライアントサイトから定期的に呼ばれるエンドポイントです。

受け取るパラメータ

パラメータ内容
slugプラグインスラッグ(必須)
version現在のバージョン
actionplugin_information の場合は詳細情報を返す

処理の流れ

function yus_rest_update(\WP_REST_Request $request): \WP_REST_Response
{
    $action = (string) $request->get_param('action');
    $product_slug = sanitize_key((string) $request->get_param('slug'));
    $current_version = (string) $request->get_param('version');

    // 1. パラメータチェック
    if ($product_slug === '') {
        return new \WP_REST_Response(['ok' => false, 'message' => 'Missing slug parameter.'], 400);
    }

    // 2. 最新リリース取得
    $release = yus_get_latest_release($product_slug);
    if (! $release) {
        return new \WP_REST_Response(['ok' => false, 'message' => 'No published release found.'], 404);
    }

    // 3. ダウンロードURLを組み立て
    $package = add_query_arg([
        'rest_route' => '/your-update/v1/download',
        'slug'       => $product_slug,
    ], untrailingslashit(home_url('/')) . '/index.php');

    // 4. plugin_information アクションの場合は詳細情報を返す
    if ($action === 'plugin_information') {
        // ...詳細情報のレスポンス
    }

    // 5. 既に最新バージョン以上なら空オブジェクトを返す
    if ($current_version !== '' && version_compare($release['version'], $current_version, '<=')) {
        return new \WP_REST_Response([], 200);
    }

    // 6. 更新情報を返す
    return new \WP_REST_Response([
        'new_version' => $release['version'],
        'package'     => $package,
        'tested'      => $release['tested_wp'],
        'requires'    => $release['requires_wp'],
        'homepage'    => $release['homepage_url'] ?: home_url('/'),
    ], 200);
}

plugin_information アクション

プラグイン詳細画面(「詳細を表示」リンク)で呼ばれるアクションです。

if ($action === 'plugin_information') {
    return new \WP_REST_Response([
        'name'           => $product_slug,
        'slug'           => $product_slug,
        'version'        => $release['version'],
        'requires'       => $release['requires_wp'],
        'tested'         => $release['tested_wp'],
        'requires_php'   => $release['requires_php'],
        'last_updated'   => $release['updated_at'],
        'download_link'  => $package,
        'package'        => $package,
        'sections'       => [
            'description' => '<p>' . esc_html($product_slug) . '</p>',
            'changelog'   => '<pre>' . esc_html($release['changelog']) . '</pre>',
        ],
    ], 200);
}

ここで返した内容が WordPress のモーダルウィンドウに表示されます。

2. ダウンロード API: GET /wp-json/your-update/v1/download

function yus_rest_download(\WP_REST_Request $request)
{
    $product_slug = sanitize_key((string) $request->get_param('slug'));
    if ($product_slug === '') {
        return new \WP_REST_Response('Missing slug.', 400);
    }

    $release = yus_get_latest_release($product_slug);
    if (! $release) {
        return new \WP_REST_Response('No published release found.', 404);
    }

    $zip_path = $release['zip_path'];
    if ($zip_path === '' || ! file_exists($zip_path)) {
        return new \WP_REST_Response('Package file not found.', 404);
    }

    // ZIPをそのまま配信
    nocache_headers();
    header('Content-Type: application/zip');
    header('Content-Length: ' . filesize($zip_path));
    header('Content-Disposition: attachment; filename="' . basename($zip_path) . '"');
    readfile($zip_path);
    exit;
}

readfile() でストリーム配信して exit で即終了。WordPressの後続処理を走らせないことで、ZIPに余計な出力が混ざらないようにしています。


管理画面の作り方

inc/admin.php で「Update Server」というメニューを追加して、ZIPアップロードフォームとリリース一覧を表示しています。

リリース登録フォームでは以下を入力します。

  • Product Slug(プラグインのフォルダ名と一致させる)
  • Version(0.1.3 のような数字+ドット形式)
  • ZIPファイル
  • Requires WP / Tested WP / Requires PHP(動作要件)
  • Changelog(更新履歴)
  • 公開フラグ

バージョン文字列のバリデーション

ここはハマりやすいので強調しておきます。

if (! preg_match('/^\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+)?$/', $version)) {
    yus_set_notice('error', 'Version must look like 0.1.3 or 1.0.0-beta.');
    ...
}

0.1.31.0.0-beta は通すけど、v0.1.30.1.3 (release) は弾く、という正規表現です。

WordPress の version_compare() は SemVer っぽい文字列なら動きますが、頭に v があるだけで挙動が変わったりするので、入り口で形を揃える のが安全です。

ZIPファイルの保存先

function yus_upload_dir(): array
{
    $uploads = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']) . 'yus-releases';
    wp_mkdir_p($base_dir);
    ...
}

wp-content/uploads/yus-releases/{slug}-{version}.zip というファイル名で保存します。

ここはセキュリティ上要注意で、本来なら uploads ディレクトリ外 に置いて直接URLアクセスできなくするのが理想です。

今のままでも zip_path を知らなければ直接ダウンロードはできませんが、より厳密にやるなら .htaccessyus-releases/ 以下を deny にすると安全です(Apache前提)。

クライアントプラグイン側の実装

ここまでがサーバー側。ここからは、実際の利用者にインストールされるほうのプラグイン で何をやるかです。

クライアント側では、WordPress の更新トランジェントにフックして、サーバーへ問い合わせた結果を埋め込みます。最小限のコードはこんな感じです。

<?php
/**
 * Plugin Name: My Plugin
 * Version: 0.1.0
 */

define('MY_PLUGIN_VERSION', '0.1.0');
define('MY_PLUGIN_SLUG', 'my-plugin');
define('MY_PLUGIN_FILE', plugin_basename(__FILE__));
define('MY_PLUGIN_UPDATE_API', 'https://example.com/wp-json/your-update/v1/update');

// 更新チェック
add_filter('pre_set_site_transient_update_plugins', function ($transient) {
    if (! is_object($transient)) {
        return $transient;
    }

    $response = wp_remote_get(add_query_arg([
        'slug'    => MY_PLUGIN_SLUG,
        'version' => MY_PLUGIN_VERSION,
    ], MY_PLUGIN_UPDATE_API));

    if (is_wp_error($response)) {
        return $transient;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);

    if (! empty($body['new_version']) && version_compare($body['new_version'], MY_PLUGIN_VERSION, '>')) {
        $transient->response[MY_PLUGIN_FILE] = (object) [
            'slug'        => MY_PLUGIN_SLUG,
            'plugin'      => MY_PLUGIN_FILE,
            'new_version' => $body['new_version'],
            'package'     => $body['package'],
            'tested'      => $body['tested'] ?? '',
            'requires'    => $body['requires'] ?? '',
        ];
    }

    return $transient;
});

// プラグイン詳細画面用
add_filter('plugins_api', function ($result, $action, $args) {
    if ($action !== 'plugin_information' || ($args->slug ?? '') !== MY_PLUGIN_SLUG) {
        return $result;
    }

    $response = wp_remote_get(add_query_arg([
        'slug'    => MY_PLUGIN_SLUG,
        'action'  => 'plugin_information',
    ], MY_PLUGIN_UPDATE_API));

    if (is_wp_error($response)) {
        return $result;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    return (object) $body;
}, 10, 3);

このコードを自作プラグインのメインファイルに入れておけば、WordPress 標準の更新フローに乗ります。「プラグイン > 更新」から普通にワンクリックで更新できるようになります。

MY_PLUGIN_UPDATE_API のURLは、Update Serverプラグインを入れたサイトのREST APIエンドポイント に書き換えてください。


使い方の流れ

実際に運用する手順をまとめます。

サーバー側(更新を配信するサイト)

  1. 後述のZIPをダウンロードしてWordPressにインストール・有効化
  2. ダッシュボード左メニュー「Update Server」を開く
  3. 配信したいプラグインのZIPファイルをアップロード
    • Product Slug: プラグインのフォルダ名(例: my-plugin
    • Version: 0.1.3 のような形式
    • 動作要件・Changelog などを入力
  4. 「Save Release」で登録完了

クライアント側(プラグインを使うサイト)

  1. 自作プラグインのメインファイルに、上記のクライアント側コードを追加
  2. MY_PLUGIN_UPDATE_API をサーバー側のURLに書き換える
  3. プラグインをインストール

これだけで、サーバー側で新しいバージョンを登録すると、クライアント側の「プラグイン > 更新」に自動で出てくるようになります。

動作確認

WordPressは更新情報を12時間キャッシュするので、すぐに反映されない場合があります。手動で更新チェックを走らせるには、WP-CLIで以下を実行します。

wp transient delete update_plugins --network
wp plugin update-check

または、ダッシュボードの「ダッシュボード > 更新」ページを開けば再チェックされます。

まとめ

ここまでの内容をまとめると、自前の更新サーバーは以下の要素で成り立っています。

  • 2つのREST API:バージョン情報を返すAPIと、ZIPを配信するAPI
  • 1つのテーブル:リリース管理(wp_yus_releases
  • 管理画面:ZIPアップロードでリリースを登録、一覧から削除も可能
  • クライアント側のフィルタ2つpre_set_site_transient_update_pluginsplugins_api

「自分のサイト群で同じプラグインを使い回したい」「クライアント案件で複数サイトに同じプラグインを配って一括更新したい」みたいなケースでは、こういう仕組みが一つあると非常に強いです。

WordPress の更新フローは標準でよく出来ているので、「乗っかれるところは全部標準に乗っかる」 という設計にしておくと、利用者側の体験も自然になります。

【ソースコード全公開】Your Update Server

ここまで解説してきたプラグインのソースコードを全文公開します。

  • URLをコピーしました!

WAZAの有料記事のサブスクリプションも開始しました。

サービス

Service

WordPressサイトのカスタマイズのサービスに関心がありましたら、ぜひ詳細をご覧ください。

目次