タグアーカイブ PHP

WordPressからS3アップロードが失敗する署名エラーの直し方

WordPressからS3アップロードが失敗する署名エラーの直し方

WordPressのプラグインや自作コードからAWS S3へファイルをアップロードすると「SignatureDoesNotMatch」というエラーで失敗する場合、シグネチャ生成後にヘッダー名の大文字小文字が入れ替わっていることが代表的な原因だ。署名の計算時とリクエスト送信時でヘッダー名を完全に一致させると、この問題は解決する。

S3アップロードの署名エラーは何が起きているのか

S3アップロードの署名エラーは何が起きているのか

AWS S3に対してファイルを直接アップロードする場合、多くのWordPressプラグインは「Presigned URL」や「署名付きリクエスト」を使う。これは、アクセスキーを直接ブラウザに渡さず、署名入りのURLやHTTPヘッダーを生成してアップロードを許可する仕組みだ。静的にサイトを書き出すプラグイン「Static Porter」もこの方式を採用している。

署名付きリクエストでは、リクエストの一部をSHA256でハッシュ化し、そのハッシュ値をシークレットキーで暗号化して「シグネチャ」を作る。S3側は同じ計算を再現し、シグネチャが一致するかどうかで正当性を検証する。ここで気をつけたいのは、署名の計算対象にHTTPヘッダー名も含まれる点だ。

つまり、署名を計算した時点で「content-type」という小文字のヘッダー名を使っていたのに、送信する直前に「Content-Type」と大文字に変更してしまうと、S3側の署名検証が失敗する。これが今回のエラーの正体だ。

Content-Typeの大文字小文字が署名を無効にする仕組み

Content-Typeの大文字小文字が署名を無効にする仕組み

AWSのSignature V4(署名バージョン4)では、署名の計算対象となる正規リクエストを作る際、ヘッダー名はそのままの文字列として扱われる。HTTPヘッダー名は本来大文字小文字を区別しないとされているが、署名計算の世界では「文字列として完全に一致していること」が要求される。

ここで問題になるのが、PHPの配列キーの扱いだ。PHPの連想配列はキーの大文字小文字を厳密に区別するため、「content-type」と「Content-Type」は別物として扱われる。署名を計算する関数に小文字の「content-type」を渡した後、別の場所で大文字の「Content-Type」を代入すると、配列には両方のキーが存在する状態になる。

wp_remote_requestがヘッダーを送信する際、どちらのキーが優先されるかは実装次第だ。しかし重要なのは、シグネチャの計算対象になったヘッダー名と、実際に送信されるヘッダー名が一致しなくなる点にある。S3はこの不一致を「署名が無効」と判定する。

Before(エラー)
シグネチャ生成時 → content-type を署名
送信直前 → Content-Type に上書き
S3検証 → 署名不一致で403エラー
↓
After(修正後)
シグネチャ生成時 → content-type を署名
送信直前 → content-type のまま変更なし
S3検証 → 署名一致でアップロード成功
■ エラー状態 ■ 修正後

上図の通り、シグネチャ生成時と送信時でヘッダー名の表記を揃えるだけで、署名検証のエラーは解消する。

署名エラーを直す具体的な修正手順

署名エラーを直す具体的な修正手順

Static Porterのようにプラグイン内部のコードに起因する場合は、プラグインの更新で修正されるのを待つ以外にも、一時的なパッチを当てる方法がある。次の手順で問題箇所を特定し、修正する。

STEP 1 シグネチャ生成関数への引数を確認する
↓
STEP 2 送信前のヘッダー上書き処理を探す
↓
STEP 3 ヘッダー名の大文字小文字を署名側に合わせる
↓
STEP 4 キャッシュをクリアして再テストする

Static Porterの場合、問題が起きる箇所はプラグインのメインPHPファイル内で、MIMEタイプを推測した後にヘッダーを上書きしている行だ。

シグネチャ生成に渡したヘッダー配列を特定する

まず、プラグインのソースコードを開き、AWSの署名を計算している関数呼び出しを探す。Static Porterでは「static_porter_aws_sigv4」という関数だ。この関数に渡しているヘッダー配列には、小文字の「content-type」がキーとして含まれている。

署名の計算はこの配列を受け取った時点の内容で行われる。ここでのキー名を記憶しておくことが修正の基準になる。

送信前のヘッダー上書き処理を探す

署名を計算した後、wp_remote_requestに渡す直前に、ヘッダー配列へ新しい値を代入している行がないか確認する。今回のケースでは、次のようなコードが問題になる。

// 修正前のコード
$content_type = static_porter_guess_mime($file_path);
$headers = static_porter_aws_sigv4('PUT', $host, $uri, $region, 's3', $body, [
    'content-type' => $content_type,
], $access_key, $secret_key);
$headers['Content-Type'] = $content_type; // ← ここで大文字キーが追加され署名と食い違う
$response = wp_remote_request('https://' . $host . $uri, [
    'method'  => 'PUT',
    'headers' => $headers,
    'body'    => $body,
    'timeout' => 60,
]);

シグネチャ生成時には小文字の「content-type」を渡しているのに、その直後に大文字の「Content-Type」という別キーを代入している。これが署名不一致の直接の原因だ。

ヘッダー名のケースを署名側に合わせる

最も確実な修正は、シグネチャ生成時のヘッダー名と送信時のヘッダー名を完全に一致させることだ。今回のケースでは、シグネチャ生成時に小文字を使っているため、後から大文字キーを代入している行をコメントアウトするだけで解決する。

// 修正後のコード
$content_type = static_porter_guess_mime($file_path);
$headers = static_porter_aws_sigv4('PUT', $host, $uri, $region, 's3', $body, [
    'content-type' => $content_type,
], $access_key, $secret_key);
// $headers['Content-Type'] = $content_type; // コメントアウトして小文字のまま送信する
$response = wp_remote_request('https://' . $host . $uri, [
    'method'  => 'PUT',
    'headers' => $headers,
    'body'    => $body,
    'timeout' => 60,
]);

あるいは逆に、シグネチャ生成時のキーを大文字の「Content-Type」に統一し、後からの代入を削除する方法もある。どちらにせよ、署名を計算した時点と送信時点で同じヘッダー名を使うことが本質だ。

キャッシュをクリアして再テストする

コードを修正したら、WordPressのキャッシュプラグインやオブジェクトキャッシュをクリアし、実際にS3へのアップロードを実行して確認する。アップロード処理に署名済みURLがキャッシュされる仕組みがある場合は、古い署名が残っていないことも確認しておく。

プラグイン本体の編集は子テーマや別プラグインで安全に行う

プラグイン本体のPHPファイルを直接編集すると、プラグインのアップデート時に修正が上書きされて消える。長期的には、プラグイン開発者へバグ報告を出し、公式のアップデートで修正されるのを待つのが妥当だ。

どうしても動かす必要がある場合は、プラグインのコードを直接修正した上で、変更内容を記録しておくか、mu-plugin(Must Use Plugin)で代替処理を差し込む方法を検討する。

自作コードでS3署名を使う場合の落とし穴

自作コードでS3署名を使う場合の落とし穴

プラグイン任せにせず、自前のPHPコードでS3への署名付きアップロードを実装している場合、同じような問題がいくつか潜んでいる。ここでは署名検証が失敗する主なパターンを整理する。

ホスト名とURIの不一致

シグネチャの計算対象には、エンドポイントのホスト名とパス(URI)が含まれる。バケット名のリージョン表記や、URLエンコードの有無、末尾のスラッシュの扱いが違うだけで署名は別物になる。署名を作る関数と実際にリクエストを送るURLが完全に一致しているか確認する。

ボディの変更によるシグネチャ崩れ

S3のPUTリクエストでは、アップロードするファイルのバイナリデータをそのままボディとして署名に含める。署名を計算した後に、余計な空白や改行、文字コード変換が入るとボディが変化し、署名検証に失敗する。ファイルはバイナリのまま扱い、加工しないことが基本だ。

タイムスタンプのスキュー

Signature V4は、リクエスト日時を署名に含める。サーバーのシステムクロックが大幅にずれていると、S3側が「リクエストが古すぎる」と判断して拒否する。エラーメッセージに時刻に関する記述がある場合は、サーバーの時刻同期(NTP)を確認する。

よくある質問

S3の署名エラーとアクセス権限エラーの違いは何か

署名エラーは「SignatureDoesNotMatch」や「The request signature we calculated does not match」と表示され、リクエスト内容が署名と食い違っていることを示す。一方、権限エラーは「AccessDenied」や「403 Forbidden」の形で、署名自体は正しいがIAMポリシーで操作が許可されていないことを示す。エラーコードを見分けることが切り分けの第一歩だ。

プラグインをアップデートしたらS3アップロードが壊れた場合はどうするか

プラグインのアップデートで内部実装が変わった可能性が高い。まずはプラグインの変更履歴(チャンジログ)を確認し、S3関連の修正が入っているか調べる。問題が再現するなら、1つ前のバージョンへ戻して切り分けるのが確実だ。プラグインのフォーラムやGitHubリポジトリで同様の報告が上がっていないかも確認する。

PHPのHTTPヘッダー名は大文字小文字を区別するのか

HTTPの仕様上、ヘッダー名は本来大文字小文字を区別しない。だが、AWSの署名計算は「文字列としての完全一致」を要求するため、区別される。PHPの配列キーも「content-type」と「Content-Type」は別物として扱われる。この2つの事情が重なって問題が起きる。

wp_remote_requestのheaders配列で小文字を使っても動作するのか

WordPressのHTTP APIはHTTPヘッダー名の大文字小文字をある程度正規化する。しかし、S3への署名付きリクエストでは、署名計算時のヘッダー名と食い違うと検証に失敗する。小文字で署名したなら小文字のまま渡すのが安全だ。

AWS SDKを直接使うほうが安全なのか

AWS SDK for PHPを使えば、シグネチャ生成と送信の整合性をライブラリが保証するため、今回のような手動ミスは起きにくい。ただし、SDKを導入すると依存関係が増え、WordPressとの相性も考える必要がある。小規模なアップロード機能なら、シグネチャ生成を手動で実装しつつ、ヘッダー名の一致を徹底する方がシンプルな場合もある。

この記事のポイント

  • S3の署名エラーはヘッダー名の大文字小文字不一致が原因になりやすい
  • シグネチャ生成時と送信時でヘッダー名を完全に一致させる
  • 問題のある代入行をコメントアウトするだけで多くの場合解決する
  • プラグイン本体の直接編集はアップデートで消えるため要注意
  • 自作コードではホスト名やボディ、時刻ずれにも気をつける
WooCommerce 11.6でPHP 8.1必須化。11.3から警告表示、11.5まで利用可能

WooCommerce 11.6でPHP 8.1必須化。11.3から警告表示、11.5まで利用可能

WooCommerce 11.6が2027年2月にリリース予定で、このバージョンからPHP 8.1以上が必須要件になる。PHP 7.4または8.0で動かしているストアは、それまでに移行準備を進める必要がある。

移行期間を確保するため、WooCommerce 11.3では管理画面に警告通知が追加される。通知は現在のPHPバージョンとホスティング事業者への相談を促す内容で、閉じることもできる。11.5までは従来のPHP環境でも更新を継続できる仕組みだ。

WooCommerceが公開した利用統計によれば、追跡対象ストアの約7%がPHP 7.4、約2%がPHP 8.0で稼働している。この記事では移行の背景、通知の内容、具体的な手順、影響範囲を整理する。

PHP 8.1必須化の背景にある3つの理由

PHP 8.1必須化の背景にある3つの理由

セキュリティサポートの終了

PHP 7.4と8.0は、PHP公式のセキュリティサポートがすでに終了している。サポートが切れたバージョンでサイトを運営し続けると、新しい脆弱性が発見されても修正パッチが提供されない。WooCommerce側でパッチを出しても、PHPランタイム自体のリスクは残る。

依存関係の更新とコードパスの削減

PHP 7.4と8.0に対応し続けるには、古い依存パッケージを固定し、バージョンごとの分岐コードを維持する必要がある。PHP 8.1を最低ラインにすることで、開発者とエクステンション製作者は新しいPHP機能を使え、テスト対象の環境も減らせる。WooCommerceはすでにPHP 8以上での動作を長年サポートしてきた実績がある。

今回の変更は、最低ラインを引き上げて将来のリリースの土台を作るものだ。コミュニティからのフィードバックを受けて、対象リリースをWooCommerce 11.6に設定した。繁忙期の後に十分なテストとアップグレードの時間を確保できるよう配慮されている。

従来の最低要件(Before)
PHP 7.4 PHP 8.0
セキュリティサポートが終了したバージョンでも動作。ただし新たな脆弱性が発見されてもパッチが提供されない。
↓
WooCommerce 11.6の最低要件(After)
PHP 8.1以上 推奨はPHP 8.3以上
新しいPHP機能を利用でき、依存パッケージの更新も進めやすくなる。テスト対象の環境も削減できる。
※ PHP 8.1で最低要件を満たすが、WooCommerceの現在の推奨はPHP 8.3以上

デモで示したように、最低要件の引き上げは単なる制限強化ではなく、開発基盤を健全化するための措置だ。PHP 8.1以降には型システムの改善やJITコンパイラの成熟など、パフォーマンスと保守性の面で有利な機能が揃っている。

WooCommerce 11.3で表示される警告通知

WooCommerce 11.3で表示される警告通知

WooCommerce 11.3から追加される通知は、PHP 7.4または8.0で稼働するストアの管理画面にのみ表示される。現在のPHPバージョンを表示し、ホスティング事業者にアップグレードを相談するよう案内する。通知は閉じられる仕組みで、11.3の利用自体は従来のPHPでも継続できる。

通知の役割はあくまで予告だ。実際に最低要件が変わるのはWooCommerce 11.6がリリースされたタイミングになる。それまでは通常どおり11.3、11.4、11.5へと更新を続けられる。

WooCommerce 11.3 管理画面の警告通知イメージ
⚠️ PHPバージョンの更新が必要です
このストアはPHP 7.4で動作しています。WooCommerce 11.6からPHP 8.1以上が必須になります。ホスティング事業者にPHP 8.3以上へのアップグレードを相談してください。
閉じる ※ 閉じてもストアの動作には影響しない
■ 通知はPHP 7.4または8.0の環境のみに表示 ■ 11.5までは従来のPHPでも更新可能

通知を閉じた後でPHPバージョンを確認したい場合は、管理画面の「WooCommerce」→「ステータス」を開き、「サーバー環境」にある「PHPバージョン」を確認すればよい。ホスティング環境が更新済みなら、WooCommerceの推奨はPHP 8.3以上だ。

通知を確認した後の移行手順

通知を確認した後の移行手順

通知が表示されたストアは、以下の手順でPHPアップグレードを進める。焦らず、ステージング検証を挟むのが安全だ。

PHPバージョンの確認

管理画面の「WooCommerce」→「ステータス」を開き、「サーバー環境」にある「PHPバージョン」を確認する。PHP 7.4または8.0が表示された場合は、移行の対象になる。WooCommerceのPHPアップデートガイドも参考になる。

ステージング環境での検証

バックアップを取得し、ステージング環境でPHP 8.3以上に切り替える。決済と支払い方法、配送計算、税計算、注文管理、スケジュールジョブ、テーマ、拡張機能、カスタムコードを一通り検証する。互換性の問題があれば、本番を変更する前に解決する。

本番環境の切り替えと確認

ステージングで問題がなければ本番を切り替え、再度「WooCommerce」→「ステータス」と重要フローを確認する。その後、WooCommerce 11.6への更新が可能になる。

STEP 1 WooCommerceのステータス画面でPHPバージョンを確認
↓
STEP 2 バックアップを取得し、ステージング環境でPHP 8.3以上に切り替え
↓
STEP 3 決済・配送・税計算・注文管理・拡張機能を一通り検証
↓
STEP 4 本番環境を切り替え、重要フローを再確認してからWooCommerce 11.6へ更新
■ 青=確認 ■ 緑=準備 ■ 橙=検証 ■ 紫=切り替え

ステージング検証は省略しがちだが、決済や配送計算の不具合は売上に直結する。特に拡張機能やカスタムコードを多用するストアでは、本番前に必ず検証したい。

アップグレードが間に合わない場合の選択肢

アップグレードが間に合わない場合の選択肢

WooCommerce 11.5はPHP 7.4と8.0に対応する最後のリリースになる予定だ。ホスティングや拡張機能の制約で移行が遅れる場合は、11.5に留まって作業を進められる。WordPressはPHP要件を満たさないプラグインの更新を通常ブロックするため、11.6への更新はサーバー側のPHPバージョンが8.1以上になるまで制限される。

11.5に留まるのは一時的な選択肢だ。WooCommerceのセキュリティパッチサポートポリシーでは、CVSS 9以上の重大な脆弱性の修正は、直近21メジャーリリースシリーズにバックポートされる。しかしそれ以外の修正は最新版にしか提供されない。加えてPHP 7.4と8.0自体のセキュリティサポートも終了しているため、WooCommerceのパッチだけではランタイムのリスクを解消できない。

WooCommerceバージョンとPHP要件の対応
11.3〜11.5 PHP 7.4 / 8.0 でも利用可能(通知は表示される)
11.5まで PHP 7.4 / 8.0 で更新できる最後のリリース
11.6以降 PHP 8.1以上が必須。PHP 7.4 / 8.0では更新できない
⚠️ 11.5に留まる場合の注意点
CVSS 9以上の重大な脆弱性のみ直近21メジャーリリースにバックポートされる。それ以外の修正は最新版のみ。PHP 7.4 / 8.0自体のセキュリティサポートも終了しているため、リスクは残る。
■ 青=移行期間 ■ 橙=最終ライン ■ 赤=必須要件

移行が遅れる理由として、特定の拡張機能がPHP 8.1以上に対応していないケースがある。その場合は拡張機能開発者に互換性の状況を確認し、代替手段を検討するのが現実的な対応だ。

ストアと拡張機能への影響範囲

ストアと拡張機能への影響範囲

利用統計から見る影響

WooCommerceが公開した統計によると、追跡対象ストアの約7%がPHP 7.4、約2%がPHP 8.0で稼働している。直近1年間にWooCommerceを更新したストアに限ると合計約6%で、この数値は継続的に減少している。実際に要件が変わる2027年2月にはさらに低くなると見られる。使用状況の追跡はオプトイン方式のため、この数値はサンプルであり全ストアの正確なカウントではない。

拡張機能マーケットの静的解析

WooCommerceは全拡張機能マーケットプレイスを対象にPHP 8.1互換性の静的解析を実施した。購入可能な1,357個の拡張機能のうち、0.22%に潜在的な問題が見つかり、開発者と手動で確認中だ。ただしこのスクリーニングはすべての拡張機能、テーマ、カスタム統合の動作を保証するものではない。各ストアは自分の環境でテストする必要がある。

拡張機能開発者と制作会社向けには、WooCommerce 11.6が使うPHP 8.1と推奨PHPバージョンの両方でテストするよう案内がある。互換性の問題を見つけた場合は、PHPとWooCommerceのバージョン、影響する拡張機能、再現手順をGitHubのイシューかコメントで共有することが推奨されている。

拡張機能マーケットのPHP 8.1互換性スキャン結果
99.78% 問題なし 0.22% 要確認
1,357個の拡張機能を静的解析。問題が見つかったものは開発者と手動で確認中。ただしスクリーニングは動作保証ではない。
ストアのPHPバージョン分布(追跡対象サンプル)
PHP 8以上 約91% PHP 7.4 約7% PHP 8.0 約2%
直近1年間に更新したストアでは合計約6%で継続的に減少。実際の要件変更時にはさらに低くなる見込み。
■ 緑=安全 ■ 青=PHP 8以上 ■ 橙=要移行 ■ 赤=要確認・要移行

影響範囲は限定的だが、該当するストアにとっては計画的な移行が欠かせない。PHPアップグレードは早めに着手し、繁忙期に入る前に完了しておくのが安全だ。

この記事のポイント

  • WooCommerce 11.6からPHP 8.1以上が必須になり、11.3で警告通知が追加される
  • PHP 7.4と8.0は公式セキュリティサポートが終了しており、移行は不可避だ
  • 11.5までは従来のPHP環境で更新可能だが、あくまで一時的な選択肢だ
  • 移行前にはバックアップとステージング検証を必ず実施する
  • PHP 8.3以上への更新がWooCommerceの現在の推奨だ
WooCommerce 11.5でPHP 8.1以上が必須に。移行の影響と準備を解説

WooCommerce 11.5でPHP 8.1以上が必須に。移行の影響と準備を解説

WooCommerce 11.5(2027年1月目標)から、最低動作環境がPHP 8.1以上になる提案が発表された。この変更が採用されると、PHP 7.4とPHP 8.0のサポートは将来のバージョンで終了する。

WooCommerceの利用データでは、PHP 7.4が追跡対象ストアの約7%、PHP 8.0が約2%を占めている。比較的新しいWooCommerceバージョンに限ると合計約6%まで下がり、減少傾向が続いている。

ストア運営者や拡張機能開発者は、移行の影響を早めに把握して準備を進める必要がある。本記事では提案の内容、影響範囲、推奨される対応をまとめた。

WooCommerce 11.5からPHP 8.1以上が最低要件に

WooCommerce 11.5からPHP 8.1以上が最低要件に
従来のWooCommerce(Before)
PHP 7.4 または PHP 8.0 で動作
WooCommerce 11.4 以前の最低要件
↓
WooCommerce 11.5以降(After)
PHP 8.1 以上が最低要件
PHP 7.4 と 8.0 のサポート終了

上の図のように、WooCommerce 11.5を境に最低PHPバージョンが引き上げられる。PHP 7.4と8.0を使うストアは、設定変更なしでは新しいバージョンへ更新できなくなる。

PHPサポート終了の現状

PHP 7.4は2022年11月に、PHP 8.0は2023年11月に公式サポートが終了している。以降はセキュリティ修正が提供されていないため、脆弱性が見つかっても修正されない状態が続く。

WooCommerceの内部データによると、追跡対象ストア全体ではPHP 7.4が約7%、PHP 8.0が約2%を占める。一方、直近1年以内にリリースされた比較的新しいWooCommerceバージョンでは合計約6%まで縮小している。

提案の具体的内容

WooCommerceエンジニアリングチームは、WooCommerce 11.5のリリースを目標に最低要件をPHP 8.1以上へ引き上げることを提案している。採用された場合、WooCommerce 11.5はPHP 8.1以上を必須とする。

この変更により、WooCommerce 11.5以降はPHP 7.4と8.0向けの互換コードを削除できる。コードベースの保守性が高まり、開発スピードの向上につながると見込まれている。

PHPバージョン引き上げのメリット

PHPバージョン引き上げのメリット
最新構文 union types
match式
名前付き引数
セキュリティ EOL回避
安全な依存パッケージ
開発効率 CIテスト高速化
互換性コード削減
性能向上 PHP 8系の
ランタイム改善
■ 最新構文 ■ セキュリティ ■ 開発効率 ■ 性能向上

PHP 8.1以上への引き上げで得られる主な利点は4つ。最新構文の活用、セキュリティ向上、開発効率の改善、そしてランタイム性能の向上だ。

最新PHP機能の活用

PHP 8.1以上では、union types(複数の型を許容する宣言)、attributes(メタデータを付与する仕組み)、null-safe operator(null判定を簡潔に書く演算子)、match式、名前付き引数などが使えるようになる。WooCommerce本体だけでなくサードパーティ製の拡張機能も、これらの機能を前提に開発できる。

union typesとは、関数の引数や戻り値に「intまたはstring」のように複数の型を指定できる記法だ。これがあると、無理な型変換や冗長な分岐を減らせる。コードが読みやすくなり、バグの発生も抑えられる。

セキュリティとパフォーマンスの向上

サポート終了済みのPHPを使い続けると、既知の脆弱性が修正されないまま残る。PHP 8.1以上へ移行することで、セキュリティ修正が提供される安心な環境に切り替えられる。

さらに、WooCommerceと拡張機能が新しいPHP専用の安全な依存パッケージを採用できるようになる。旧バージョン向けの互換レイヤーを削除でき、継続的インテグレーション(CI)でテストするPHPバージョンも減るため、テストやリリースの速度も上がる。

PHP 8系へのアップグレード自体にも性能向上の効果がある。PHPランタイムの改善により、WooCommerceストアの表示速度や処理能力が底上げされる。これはコード変更なしで得られる利点だ。

PHP 7.4・8.0を使うストアへの影響

PHP 7.4・8.0を使うストアへの影響
PHP 7.4・8.0 のストア(Before)
WooCommerce 11.5 の更新が提供されない
既存バージョンのドットリリースのみ継続
↓
PHP 8.1 以上へ移行後(After)
WooCommerce 11.5 以降を更新可能
PHPアップグレード後に更新

PHP 7.4や8.0を使っていても、WooCommerceが自動でPHPを更新したり、ストアを停止させたりすることはない。ただし、WooCommerce 11.5以降への更新だけはブロックされる。

自動更新の停止と移行手順

提案が採用された場合、WooCommerce 11.5はPHP 8.1以上を必須とする。WordPressの標準機能により、要件を満たさないストアにはWooCommerce 11.5の更新が表示されなくなる。

PHP 7.4または8.0を使うストアは、現在のWooCommerceバージョンをそのまま使い続けることになる。同じブランチのドットリリース(例 11.4.x)は引き続き提供される。新しいメジャーバージョンへ移るには、先にPHPを8.1以上へアップグレードする必要がある。

前例となったWooCommerce 8.2

今回の進め方は過去のWooCommerceのPHP要件変更と同様のアプローチを取っている。WooCommerce 8.2では最低要件をPHP 7.4以上へ引き上げ、事前通知と移行ガイダンスが提供された。

当時も、要件を満たさないストアは自動更新されず、既存バージョンの更新のみ継続する方式だった。今回のPHP 8.1引き上げでも同じ流れが想定されている。

WordPressと拡張機能開発者への影響

WordPressと拡張機能開発者への影響

WordPressのPHP要件との違い

WordPress本体は現在、PHP 7.4以上で動作し、PHP 8.3以上を推奨している。しかしWooCommerceは、WordPressより高いPHP要件を維持してきた経緯がある。

WooCommerceは決済処理や在庫管理、注文ワークフローなどを持つ複雑なECアプリケーションだ。独自の依存関係や性能要件があるため、最低PHPバージョンをWordPressと完全に一致させる必要はないとされている。

拡張機能開発者への推奨事項

WooCommerce向けの拡張機能を開発している場合、すでに新しいPHPバージョンでテストしていることが理想だ。今回の変更により、WooCommerce 11.5以降だけを対象にする拡張機能はPHP 8.1以上の機能を使えるようになる。

一方、古いWooCommerceバージョンを引き続きサポートする拡張機能は、互換レイヤーや別コードパスを維持する必要が出てくる。PHPバージョンが上がっても、すべての拡張機能やテーマ、カスタムスニペットがそのまま動く保証はない。

本番環境へ適用する前に、ステージングサイトでPHP 8.1以上への移行テストを行うことが重要だ。特に独自カスタマイズや古い拡張機能を使っている場合、互換性の問題が顕在化しやすい。

意見募集と今後のスケジュール

意見募集と今後のスケジュール

フィードバックの募集ポイント

WooCommerceエンジニアリングチームは、ストア運営者、代理店、ホスティングプロバイダー、拡張機能開発者などから幅広く意見を求めている。主な質問は以下の通りだ。

  • PHP 7.4や8.0から移行できない理由はあるか
  • 該当PHPバージョンを必要とする拡張機能やテーマは存在するか
  • WooCommerce 11.5(2027年1月目標)で準備期間は十分か
  • ホスティングプロバイダーはWooCommerceストアのPHP利用状況について追加データを持っているか
  • 対応が望まれる移行ガイダンスは何か

互換性の問題を報告する場合は、PHPバージョン、WooCommerceバージョン、影響を受ける拡張機能、エラーメッセージを添えることが推奨されている。

決定までのタイムライン

WooCommerceチームはフィードバックと利用データを確認した上で、対象リリースと最低PHPバージョンを最終決定する。決定後は事前告知を行い、開発者が新しい要件でテストできるよう準備期間を設ける方針だ。

現時点ではまだ提案段階であり、正式な決定ではない。ただし利用データの減少傾向やEOLの状況を踏まえると、PHP 8.1以上への引き上げは現実的な選択肢として有力だ。

この記事のポイント

  • WooCommerce 11.5(2027年1月目標)からPHP 8.1以上が最低要件になる提案が発表された
  • PHP 7.4と8.0はサポート終了済みで、利用は全体の約6〜9%まで縮小している
  • PHP 7.4・8.0のストアは自動更新されず、移行後にWooCommerce 11.5へ更新できる
  • 最新PHP機能の活用、セキュリティ向上、開発効率の改善が主な目的だ
  • 拡張機能開発者はステージング環境でPHP 8.1以上へのテストを推奨
WooCommerceのマジックストリング廃止へ、エナムクラス導入で拡張開発が安全に

WooCommerceのマジックストリング廃止へ、エナムクラス導入で拡張開発が安全に

WooCommerceのコードベースで長年使われてきた「マジックストリング」が、人知れず問題を引き起こしていた。注文ステータス、商品タイプ、在庫状態、税金モード。これらの重要な値が、コード中で生の文字列として比較されていたのだ。タイプミス一つでバグになり、プレフィックスの有無で混乱し、コード検索でノイズが大量に混ざる。開発者にとって頭の痛い状況だった。

WooCommerceはこの問題に対処するため、Automattic\WooCommerce\Enumsという名前空間にエナムクラス群を導入した。注文ステータスや商品タイプを表す名前付き定数をパブリックAPIとして公開し、拡張機能開発者に採用を促している。本記事では、この変更の背景、利用可能なクラス、実際の導入方法を整理する。

マジックストリングがもたらす4つの問題

マジックストリングがもたらす4つの問題

WooCommerceは歴史が長い。そのため、注文ステータスや商品タイプといった重要な値が、PHPの近代的な慣習が生まれる前から存在している。コードベースの各所で、次のような生の文字列比較が行われてきた。

if ( 'completed' === $order->get_status() ) {
    // タイプミスがないことを祈るしかない
}

この書き方は一見シンプルに見えるが、WooCommerceのような大規模なコードベースでは深刻な代償を伴う。Developer WooCommerce Blogの記事では、その問題を4つの観点から説明している。

沈黙のエラー

文字列リテラルの最大のリスクは、タイプミスが静かに潜むことだ。'complete'と書くべきところを'completed'と書いても、リンターもオートローダーもテストもエラーを検出しない場合がある。コードは実行時に黙って失敗し、デバッグに時間を奪われる。

プレフィックスの有無による混乱

WordPressのデータベースでは、注文ステータスにwc-というプレフィックスが付く。データベースにはwc-completedとして保存されるが、WooCommerceの多くのAPIはプレフィックスなしのcompletedを期待する。この違いは、開発者が実際にコードを動かして初めて気づく類の落とし穴だ。

検索の非効率さとドキュメントの分散

エージェントやコード検索で'simple'という文字列を探すと、商品タイプのロジックを探しているのに無関係な箇所が大量にヒットする。一方、ProductType::SIMPLEのように名前付き定数で検索すれば、該当箇所を正確に絞り込める。さらに、on-holdのようなステータスの定義は、宣言箇所のdocblockにドキュメントとして残せるようになる。

マジックストリングの問題点(Before)
タイプミス検出困難 → 実行時まで気づけない
プレフィックス混乱 → wc-completed と completed の区別が曖昧
検索ノイズ → simple で検索すると無関係な箇所が大量ヒット
ドキュメント分散 → 値の意味がコード中に散在
↓
エナムクラス導入後(After)
名前付き定数 → タイプミスをコンパイル時に検出
意図の明確化 → OrderStatus::COMPLETED と OrderInternalStatus::COMPLETED を区別
検索精度向上 → ProductType::SIMPLE で該当箇所を正確に絞り込み
ドキュメント統合 → docblockに意味を記載

この比較で示したように、エナムクラスは文字列リテラルが抱える問題を構造的に解決する。特に、プレフィックス有無の違いを別々のクラスに分けることで、意図を明確に伝えられる点が重要だ。

ネイティブPHPエナムを採用しなかった理由

ネイティブPHPエナムを採用しなかった理由

PHPにはバージョン8.1からネイティブのエナム型が導入されている。しかしWooCommerceはこの選択肢を取らなかった。主に2つの理由がある。

PHP 7.4のサポート

WooCommerceの最低サポートPHPバージョンは7.4だ。このバージョンにはネイティブエナムが存在しない。PHP 8.1を前提にすると、多くのユーザーを切り捨てることになる。クラス定数として文字列を定義する方式なら、PHP 7.4でも問題なく動作する。

文字列互換性という設計判断

もう一つの理由はアーキテクチャ上の判断だ。注文ステータスや商品タイプの値は、すでに数百万のデータベースにプレーンな文字列として保存されている。数千の拡張機能もその形式を前提にしている。ネイティブPHPエナムを使うと値がオブジェクトに変換され、既存のコードが壊れる可能性がある。

文字列定数を使う方式なら、OrderStatus::COMPLETEDは従来の'completed'と同じ文字列を生成する。開発者はより明確な名前を使いつつ、WooCommerceの動作を変えない。既存コードが動き続け、拡張機能は準備ができた時点で新しい定数に移行できる。

final class OrderStatus {
    /**
     * 注文が完了した状態
     */
    public const COMPLETED = 'completed';
    // ...
}

このコードが示すように、エナムクラスはfinalで宣言され、public constとして定数を公開する。docblockで各定数の意味をドキュメント化できるのが利点だ。クラスを継承させないことで、APIの一貫性を保つ。

利用可能なエナムクラス

利用可能なエナムクラス

現在WooCommerceのsrc/Enumsディレクトリには、4つのカテゴリにまたがるエナムクラスが用意されている。各クラスの一覧を確認しよう。

注文と商品のエナム

  • OrderStatus(プレフィックスなしの値。例としてcompleted)
  • OrderInternalStatus(データベースに保存されるwc-プレフィックス付きの値)
  • OrderItemType(注文アイテムのタイプ)
  • ProductType(商品タイプ。例としてsimple)
  • ProductStatus(商品ステータス)
  • ProductStockStatus(在庫状態)
  • ProductTaxStatus(税金状態)
  • CatalogVisibility(カタログの表示設定)

支払いと設定値のエナム

  • PaymentGatewayFeature(ゲートウェイがsupports配列で宣言する文字列)
  • WeightUnit(重量単位)
  • DimensionUnit(寸法単位)
  • CurrencyPosition(通貨位置)
  • TaxBasedOn(課税基準)
  • TaxDisplayMode(税表示モード)
  • DefaultCustomerAddress(デフォルト顧客住所)
  • StockDisplayFormat(在庫表示形式)
  • CatalogSortOrder(カタログ並び順)

各クラスの完全な一覧は、WooCommerceのGitHubリポジトリにあるsrc/EnumsディレクトリとREADMEで確認できる。このディレクトリが権威ある情報源として公開されている。

注文関連 OrderStatus / OrderInternalStatus / OrderItemType
商品関連 ProductType / ProductStatus / ProductStockStatus 他
支払い関連 PaymentGatewayFeature
設定値関連 WeightUnit / DimensionUnit / CurrencyPosition 他

以上の4カテゴリが、現在WooCommerceで利用可能なエナムクラスの全体像だ。注文関連と商品関連が特に充実している。

拡張機能での採用方法

拡張機能での採用方法

エナムクラスは公的に発見可能なAPIとして設計されており、publicの可視性、docblock、開発者向けドキュメントを備えている。拡張機能開発者は安心して利用できる。

基本的な使い方

使い方はシンプルだ。use文で必要なクラスをインポートし、定数を参照する。例えば注文ステータスをチェックする場合、次のように書ける。

use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductType;

if ( OrderStatus::COMPLETED === $order->get_status() ) {
    // 注文が完了した場合の処理
}

$products = wc_get_products( array( 'type' => ProductType::SIMPLE ) );

採用前に確認すべき2つのポイント

エナムクラスを拡張機能に導入する前に、確認しておくべきことが2つある。

1. 最小サポートWooCommerceバージョン。エナムクラスは段階的に追加されてきた。OrderStatusはWooCommerce 9.5で導入され、商品関連クラスは9.7〜9.8頃、設定値クラスは10.xシリーズで追加された。古いバージョンもサポートする場合は、文字列リテラルを使い続けるか、class_exists()でガードする必要がある。

2. WooCommerceが期待する文字列。OrderStatus::COMPLETEDはcompletedを返すが、OrderInternalStatus::COMPLETEDはwc-completedを返す。ほとんどのWooCommerce関数はプレフィックスなしの形式を取るが、データベースレベルやpost_statusコンテキストではプレフィックス付きを使う必要がある。どちらの形式を使うべきかを意識するのが重要だ。

商品検索と注文検索の公式ドキュメントには、文字列リテラルとエナムクラスの両方の形式が併記されている。移行時の参考になる。

プレフィックスなし(API用)
OrderStatus::COMPLETED → completed を返す
使用場面 → wc_get_orders() などの API
↓
プレフィックスあり(DB用)
OrderInternalStatus::COMPLETED → wc-completed を返す
使用場面 → post_status やデータベース操作

このデモで示したように、2つの定数は同じ注文完了状態を表すが、使用するコンテキストが異なる。混同すると期待通りに動作しない。

今後の展開

今後の展開

Developer WooCommerce Blogの記事によれば、コアに新しい語彙(文字列値の集合)を追加する場合、エナムクラスをデフォルトで付ける方針が示されている。WooCommerceコアにはまだ名前の付いていない文字列値が多数残っており、コミュニティからの貢献も歓迎している。

この変更は拡張機能開発者にとって歓迎すべき動きだ。タイプミスのリスクが減り、コードの意図が読みやすくなり、IDEの補完機能も効きやすくなる。長期的にはWooCommerceエコシステム全体のコード品質向上につながる。

一方で、移行には段階的な対応が必要だ。既存の拡張機能は古いWooCommerceバージョンとの互換性を維持しながら、徐々にエナムクラスへ移行していくことになる。一括で全置換するのではなく、新規コードから採用していくのが現実的なアプローチだろう。

この記事のポイント

  • WooCommerceがマジックストリング(生の文字列リテラル)をエナムクラスに置き換える動きを進めている
  • エナムクラスはタイプミス防止、プレフィックス区別、検索精度向上、ドキュメント統合の4つの利点を持つ
  • ネイティブPHPエナムではなく文字列定数クラスを採用した理由は、PHP 7.4互換性と既存データベースの文字列互換性
  • 注文・商品・支払い・設定値の4カテゴリにエナムクラスが用意されている
  • 採用時は最小サポートWooCommerceバージョンと、プレフィックス有無のどちらの定数を使うかを確認する
WordPressのdo_action()を見直す イベントをオブジェクト化し、フックをクラス名にする最新の開発パターン

WordPressのdo_action()を見直す イベントをオブジェクト化し、フックをクラス名にする最新の開発パターン

WordPressの開発でdo_action()を一度も使ったことがない人はまずいないだろう。カスタムフックを定義して他のプラグインやテーマから振る舞いを拡張する手法は、バージョン1.2で導入されて以来変わらず愛用されている。

しかし長年の使い込みの中で、いくつかの「小さな摩擦」が無視されてきたのも事実だ。引数の順序を覚えきれない、フック名が大域空間で衝突する、戻り値の返し忘れでフィルターが壊れる、といった問題は大規模なコードベースになるほど開発者の時間を食う。

この記事では、do_action()に1つのオブジェクトを渡し、そのクラス名をフック名として使うことで、これらの問題を一掃するパターンを解説する。独自のライブラリもフレームワークも不要で、PHPの標準機能だけで実装できる。2026年8月にDeveloper WordPress Newsで公開された洞察をもとに、実装例と仕組みを詳しく見ていこう。

do_action()で起きていた4つの摩擦

do_action()で起きていた4つの摩擦

従来のdo_action()は「フック名」と「任意の数の引数」を受け取るシンプルなAPIだ。会員登録をトリガーとするプラグインであれば、次のようにフックを発火させるのが一般的な書き方になる。

do_action( 'myplugin_member_registered', $userId, $plan );

これを受け取る側のコードは以下のようになる。

add_action( 'myplugin_member_registered', static function ( $userId, $plan ) {
    // ... 何らかの処理
}, 10, 2 );

動作自体に問題はない。だが、よく見ると運用上のストレスがいくつも潜んでいる。Developer WordPress Newsの記事が指摘する摩擦点は大きく次の4つだ。

  • 引数が位置に依存する $userIdと$planの順序を覚えておかなければならず、間違えた場合のエラーは追跡しにくい。
  • フック名がグローバルな名前空間 'myplugin_member_registered'という文字列が他のプラグインと衝突しないよう、prefixをつけて管理する必要がある。
  • 型情報が一切ない $planは文字列かもしれないしIDかもしれない。コードエディタの補完も静的解析も効かず、ドキュメントを読まなければ正体がわからない。
  • フィルターの戻り値忘れ apply_filters()を使うと、どのリスナーも必ず値を返さなければならない。うっかりreturnを書き忘れると、後続のフィルターがすべて破綻する。

これらはフックの仕組みそのものの問題ではなく、ペイロード(運ぶデータ)の渡し方に起因する。そして、このペイロード設計を見直すだけで、すべてがきれいに解決する。

従来のフック呼び出し(Before)
do_action( ‘myplugin_member_registered’, $userId, $plan );
※フック名は文字列でグローバルスコープ、引数は位置で決まるため順序を覚えておかなければならない
↓
オブジェクトを使った新しいパターン(After)
do_action( MemberRegistered::class, $event );
※完全修飾クラス名がフック名になり、オブジェクト1つですべての情報を運ぶ。型が自動的に決まる

上の比較でわかるとおり、do_action()に渡す情報をオブジェクトにまとめるだけで多くの摩擦が消える。フック名の衝突リスクがなくなり、コードエディタの補完も効く。

イベントオブジェクトとクラス名フック 1行で変わる設計

イベントオブジェクトとクラス名フック 1行で変わる設計

解決策の核心は驚くほど単純だ。Developer WordPress Newsの記事の著者が試みたのは、「引数をバラバラに渡すのではなく、起こった出来事を表すオブジェクトを1つだけ渡す」というやり方である。

具体的には、次のように書く。

do_action( $event::class, $event );

$event::classが何が起きたかを示すフック名になり、$eventがその詳細を運ぶ。フック名にはそのクラスの完全修飾名が使われるため、名前空間が自動的に適用され、他のプラグインと衝突することはまずない。

フック名を::classで決めるか、あるいは固定の文字列にするかは選択できる。クラス名を使えば、IDEでクラスをリネームするとフックも追従する。文字列(例:'myplugin/member-registered')を指定すれば、後からクラスを移動させてもフック名は変わらない。移行の危険を減らしたいなら固定文字列のほうが安全だが、どちらにせよペイロードは整ったオブジェクトになる。

また、このテクニックはapply_filters()を使わなくてもフィルター的な振る舞いを実現できる。オブジェクトのプロパティを書き換え可能にしておけば、リスナーが自由に値を変更し、ディスパッチャ側が後から読み取れるからだ。戻り値の管理に頭を悩ませる必要がなくなる。

具体例 会員登録のDispatcherとListener

具体例 会員登録のDispatcherとListener

ここでは会員登録を題材に、イベントオブジェクトを使ったフックの流れを具体的に見ていこう。名前空間MyPlugin\Membersの下に、登録イベントを表すクラスと、それを発行するクラス、そして受け取る側のコードを用意する。

イベントクラスの定義

namespace MyPlugin\Members;

final class MemberRegistered
{
    public function __construct(
        public readonly int    $userId,
        public readonly string $plan,
        public          bool   $sendWelcomeEmail = true,
    ) {}
}

$userIdと$planはreadonlyとして宣言され、リスナーは読み取り専用で使用する。一方、$sendWelcomeEmailは書き換え可能にしておき、ウェルカムメールを送るかどうかの判断をリスナーに委ねる。

Dispatcher(発行側)

namespace MyPlugin\Members;

final class MemberRegistrar
{
    public function register( int $userId, string $plan ): void
    {
        // アカウント作成処理...

        $event = new MemberRegistered( userId: $userId, plan: $plan );

        do_action( $event::class, $event );

        if ( $event->sendWelcomeEmail ) {
            // ウェルカムメールをキューに追加
        }
    }
}

ポイントは、do_action()の後で$event->sendWelcomeEmailを評価しているところだ。この値は後続のリスナーが変更したかもしれない。オブジェクトは参照で渡されるため、ディスパッチャはリスナーによる変更をそのまま拾える。ここにapply_filters()は必要ない。

ObserverとMutatorの2パターン

受け側はおおまかに2種類に分かれる。1つはイベントを観測するだけのObserverで、値を読み取るが変更はしない。もう1つはプロパティを書き換えるMutatorだ。

// Observer ログ出力のみ
add_action( MemberRegistered::class, static function ( MemberRegistered $event ): void {
    error_log( sprintf(
        'Member #%d registered on the %s plan.',
        $event->userId,
        $event->plan
    ) );
} );

// Mutator 無料プランの場合はウェルカムメールを無効化
add_action( MemberRegistered::class, function ( MemberRegistered $event ): void {
    if ( 'free' === $event->plan ) {
        $event->sendWelcomeEmail = false;
    }
} );

Mutatorのコールバックを見ると、型ヒントによって$eventのプロパティが自動補完されることがわかる。$userIdや$planの順序を気にする必要はなく、add_action()の第4引数で引数の数を指定する手間もない。これがオブジェクト化の地味ながら大きな恩恵だ。

Observerの動作
イベント → ログ記録
イベントオブジェクトを読むだけで、値を変更しない。
↓
Mutatorの動作
イベント → プロパティ変更 → Dispatcherが後で読み取り
$sendWelcomeEmailをfalseにすることで、メール送信の挙動を変更する。
■ イベントオブジェクト  ■ Observer  ■ Mutator  ■ Dispatcherの後工程

このように、1つのオブジェクトを受け渡すだけで、従来のアクションとフィルターの両方の役割を統一的に扱える。コードの見通しは飛躍的に良くなるはずだ。

オブジェクト化がもたらすメリットのまとめ

オブジェクト化がもたらすメリットのまとめ

これまでの例から、イベントオブジェクトパターンを採用することで得られる具体的な利点を整理する。

  • 型安全なリスナー すべてのコールバックがイベントクラスで型を宣言するため、エディタの補完と静的解析がフルに働く。
  • 名前衝突からの解放 フック名は完全修飾クラス名(または明示的な文字列)で管理されるため、prefixを手動で付ける必要がなくなる。
  • フィルターのような書き換えをアクションで実現 オブジェクトのプロパティをミュータブルにしておけば、apply_filters()を使わずとも値の変更が可能。戻り値の返し忘れによるバグも消える。
  • 自己文書化 各イベントクラスが独立したファイルになるため、どの拡張ポイントが存在するかがディレクトリを見るだけで把握できる。

これらの改善は、特別なライブラリを導入しなくても、今日から自社のプラグインに適用できる。既存のdo_action()やadd_action()をそのまま使いつつ、ペイロードをオブジェクトに切り替えるだけだ。

WordPressのルーツとPSR-14の関係

WordPressのルーツとPSR-14の関係

このパターンを「新しいアイデア」と感じるかもしれないが、実はWordPressが2004年のバージョン1.2で搭載したプラグインAPIの時点で、根幹の設計は既に存在していた。

PHPの世界でPSR-14(Event Dispatcher)として標準化された「イベント、リスナー、ディスパッチャ」の概念は、do_action()とadd_action()の組み合わせでほぼ表現できる。つまり、WordPressはとっくにイベント駆動の基盤を持っていたことになる。

PSR-14が定める厳密な契約(伝播制御、リスナープロバイダー、ディスパッチャの交換など)はWordPressには組み込まれていない。だが、「イベントをオブジェクトで表現し、そのクラス名でフックする」という発想は、PSR-14のイベントモデルと驚くほど調和する。結果として、WordPressのネイティブな関数の上に、よりモダンで安全なイベント設計を載せられるわけだ。

Developer WordPress Newsの記事の著者も、PSR-14を完全実装する試みの中で、大半の価値がこの小さな習慣に詰まっていることに気づいたと述べている。段階的な移行が可能で、いきなり大掛かりなフレームワークに乗り換える必要はない。

このパターンの限界と発展の方向性

このパターンの限界と発展の方向性

ここまでのテクニックで、自作プラグインのカスタムフックの多くは十分に改善できる。ただし、次のような高度な要件に直面したときは、より本格的なイベントシステムの導入を検討してもいい。

  • 伝播の停止 あるリスナーが「以降のリスナーは実行するな」と指示する仕組み。これはremove_action()と優先度では再現しきれない場面がある。
  • 複雑なリスナー管理 数十個のリスナーを手作業で登録するのではなく、1つのサブスクライバークラスにまとめて登録したいケース。
  • テストのための差し替え ディスパッチャ自体を丸ごと入れ替えて、イベントの発生をテストダブルで差し替えたい状況。

これらの必要性を感じ始めた時が、PSR-14準拠のディスパッチャを導入する潮時といえる。しかし、そこに至るまでは、「do_action()にオブジェクトを渡す」習慣だけで、保守性も開発体験も大きく前進させられる。

今日からすぐに実践できる小さな設計変更が、コードベースの品質を長期にわたって支える土台になる。次のカスタムフックを書くときに、このパターンをぜひ試してみてほしい。

この記事のポイント

  • do_action()にバラバラの引数ではなく1つのイベントオブジェクトを渡すと、引数の順序問題や型の不明瞭さが解消される。
  • フック名にクラスの完全修飾名を使うことで、名前空間が自然に確保され、衝突リスクが劇的に下がる。
  • オブジェクトの書き換え可能プロパティを利用すれば、apply_filters()を使わずにフィルター的な挙動を安全に実装できる。
  • オブザーバーとミューテーターの2種類のリスナーを型安全に記述でき、コードエディタの補完がフルに働く。
  • PSR-14のような本格的なイベントシステムを導入しなくても、小さな習慣を変えるだけで開発体験が大幅に改善する。
Wordfence 8.2.2 で前台に preg_replace() 非推奨エラーが出た時の対処

Wordfence 8.2.2 で前台に preg_replace() 非推奨エラーが出た時の対処

Wordfence 8.2.2 を導入した WordPress 7.0.3 のサイトで、前台ページやログイン画面に Deprecated: preg_replace() から始まる非推奨(デプリケーション)エラーが表示される場合、まず WP_DEBUG 設定を確認して本番環境でのエラー表示を抑制し、次に Wordfence 本体を最新版へ更新する のが最短の対策だ。

なぜ本番サイトの前台に非推奨エラーが表示されるのか

なぜ本番サイトの前台に非推奨エラーが表示されるのか

非推奨エラー(Deprecated 通知)は、PHP の将来のバージョンで廃止される古い書き方を使っている場合に発生する。Wordfence 8.2.2 内部のルール処理ライブラリ wf-waf が preg_replace() 関数に null を渡しており、PHP 8.1 以降でこの挙動が非推奨となったことが直接の原因だ。

本来このレベルの通知は、本番サイトでは表示されないよう WordPress が抑制する仕組みになっている。しかし 何らかの理由でデバッグモードが有効になっているか、エラー報告レベルが高く設定されている と、前台に生の PHP エラーメッセージが露出してしまう。これが今回のケースの本質的な問題だ。

さらにこのエラーが「headers already sent」という警告を誘発し、ログイン処理や Cookie 設定などの HTTP ヘッダー操作が失敗する二次被害も報告されている。前台の表示崩れや管理画面へログインできないトラブルに発展するため、早期の対応が欠かせない。

すぐに前台からエラー表示を消す応急処置

すぐに前台からエラー表示を消す応急処置

更新を待つ前に、まずは本番環境でエラーが一般人に見えている状態を解消する。手順は以下の3ステップだ。

STEP 1 wp-config.php で WP_DEBUG を false に設定する
↓
STEP 2 Wordfence プラグインを最新版に更新する
↓
STEP 3 サイトキャッシュとサーバーキャッシュをすべて削除して動作確認する

wp-config.php で WP_DEBUG を確認・修正する

FTP クライアントやサーバーのファイルマネージャーで WordPress をインストールしたルートディレクトリにアクセスし、wp-config.php を開く。WP_DEBUG が true になっていれば、以下のように false へ変更する。

define( 'WP_DEBUG', false );

開発用途でデバッグログだけは残したい場合は、WP_DEBUG_LOG と WP_DEBUG_DISPLAY を併用する。これで前台にはエラーを表示せず、ログファイルにだけ記録できる。

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );   // /wp-content/debug.log に記録
define( 'WP_DEBUG_DISPLAY', false ); // 画面には表示しない

ファイルを上書き保存したら、サイトをリロードして前台からエラーメッセージが消えたか確認する。

Wordfence を最新版に更新する

この preg_replace() の非推奨問題は、Wordfence 開発チームが認識して修正に取り組んでいる可能性が高い。管理画面にアクセスできるなら「プラグイン」→「インストール済みプラグイン」で Wordfence の更新を確認し、最新版がリリースされていれば即座に適用する。

管理画面すらエラーで開けないという報告も多い。その場合は FTP で /wp-content/plugins/wordfence/ ディレクトリを一時的にリネーム(例 wordfence_old)して無効化し、管理画面へアクセスできる状態にする。管理画面に入れたら、改めて Wordfence を最新版に更新して再有効化すればよい。

キャッシュを全削除して状態を確定させる

PHP 設定やプラグインを変更しても、キャッシュが残っていると古いエラー画面が表示され続ける。以下のキャッシュを徹底的にクリアする。

  • WordPress のキャッシュプラグイン(WP Rocket や W3 Total Cache など)の全キャッシュ削除
  • サーバー側の OPcache や Nginx FastCGI Cache のクリア
  • CDN を利用している場合は CDN のキャッシュもパージ
エラー表示あり 前台に PHP 非推奨エラーが丸見え
→
修正後 前台は通常表示・管理画面にもログイン可能

それでも改善しない場合の追加対応

それでも改善しない場合の追加対応

上記の手順を実行してもエラーが消えない、あるいは管理画面にまったく入れない状況が続くなら、次の手を試す。

Wordfence を手動で最新 ZIP に上書きする

WordPress 管理画面から更新できない場合、WordPress.org の Wordfence 公式ディレクトリから最新の ZIP ファイルをダウンロードし、FTP で展開する。手順は次のとおり。

  1. 現在の /wp-content/plugins/wordfence/ をリネームして退避
  2. ダウンロードした ZIP を解凍し、wordfence ディレクトリをアップロード
  3. 管理画面から Wordfence を有効化し、WAF の最適化を再実行

PHP のエラー報告レベルを一時的に緩和する

レンタルサーバーのコントロールパネルや php.ini で error_reporting を E_ALL & ~E_DEPRECATED に設定すれば、非推奨通知をまとめて抑制できる。ただしこの方法は問題の先送りになるため、あくまで Wordfence 更新が間に合わない場合の緊急措置と位置づける。

別のセキュリティプラグインへ一時的に切り替える

サイトのセキュリティを完全に落とせない事情があるなら、Wordfence を無効化している間だけ、別の軽量ファイアウォール系プラグインで防御を維持する手もある。ただし切り替えの手間と、切り戻し時に設定がリセットされる可能性は考慮が必要だ。

よくある質問

Wordfence を無効化したまま運用しても大丈夫か

無効化中はファイアウォールとマルウェアスキャンがすべて停止するため、できるだけ数時間以内に最新版へ更新して再開するのが望ましい。どうしても長引く場合は、サーバー側の WAF 機能や .htaccess によるアクセス制限で最低限の防御を維持する。

PHP バージョンを下げればこのエラーは消えるのか

PHP 7.4 など古いバージョンに戻せば preg_replace() の非推奨警告は出なくなるが、PHP 本体のセキュリティサポートが切れているバージョンを使うことは推奨しない。Wordfence の更新で根本対応し、PHP は 8.1 以降のサポート対象バージョンを維持するのが安全な選択だ。

他のプラグインで同様の Deprecated エラーが出た場合の対処は

まず該当プラグインが最新版かを確認する。最新で出るなら、開発元のサポートフォーラムに PHP バージョンとエラー全文を添えて報告する。本番環境では WP_DEBUG_DISPLAY を false にしつつ WP_DEBUG_LOG で記録を取る運用が基本だ。

Wordfence の代わりになる無料プラグインはあるか

無料で総合的な防御を提供する代替としては、Solid Security(旧 iThemes Security)や Sucuri Security が挙げられる。ただし機能や設定項目が異なるため、移行前に必ずテスト環境で動作検証を済ませる必要がある。

この記事のポイント

  • 前台に PHP 非推奨エラーが出る原因は WP_DEBUG 設定とプラグインの対応遅れ
  • まず wp-config.php で WP_DEBUG を false または WP_DEBUG_DISPLAY false にする
  • Wordfence を最新版に更新すれば preg_replace() 問題は解消に向かう
  • 管理画面に入れない場合は FTP でプラグインをリネームして緊急アクセスを確保する
  • PHP のエラー報告レベル操作はあくまで緊急避難であり恒久対応ではない
WooCommerce納品書印刷でFTPエラーが起きた時の原因と直し方

WooCommerce納品書印刷でFTPエラーが起きた時の原因と直し方

WooCommerceのマイアカウント画面から「納品書を印刷」や「領収書を印刷」をクリックした瞬間に「このサイトで重大なエラーが発生しました」と表示されてページが落ちる場合、原因はプラグインがFTPの認証情報なしにファイルシステムへアクセスしようとしたことにある。PHP 8環境で発生しやすいこの問題は、WP_Filesystem()の呼び出し方を修正すれば直る。

エラーの原因は何か

エラーの原因は何か

この問題は「Print Invoice & Delivery Notes for WooCommerce」などの納品書プラグインが、フロントエンドからWP_Filesystem()を呼び出す際にFTPの認証情報を渡していないことが根本原因だ。WP_Filesystem()はWordPressがサーバー上のファイルを操作するためのAPIで、通常は管理画面から操作するときに使われる。しかしプラグインのコードがこのAPIをバックグラウンドで実行しようとしたとき、必要なFTP接続情報が揃わず、接続オブジェクトがnullのままになってしまう。

PHP 8では関数の引数の型チェックが厳格化されたため、nullの接続オブジェクトをftp_nlist()などの関数に渡すと即座に致命的なTypeErrorが発生する。これが「Uncaught TypeError: ftp_nlist(): Argument #1 ($ftp) must be of type FTP\Connection, null given」というエラーの中身だ。

エラー発生時の状態
プラグインが WP_Filesystem() を引数なしで呼び出す
↓
FTP認証情報がないため接続オブジェクトが null になる
↓
PHP 8の型チェックで TypeError が発生し画面がクラッシュ
↓
修正後の正しい流れ
まず request_filesystem_credentials() で認証情報を取得
↓
取得した認証情報を WP_Filesystem( $credentials ) に渡す
↓
FTP接続が正常に行われ、ファイル操作が完了する
■ エラー発生時 ■ 修正後

上の図で見るとわかるように、修正前は認証情報の取得ステップが丸ごと抜け落ちている。WordPressが用意している標準的な手順は「まず認証情報を集め、それからファイルシステムを初期化する」という2段階だ。プラグインがこの流れを省略したことで、FTP接続が確立されないまま後続の処理が走り、致命的エラーに至っている。

自分のサイトでエラーが発生しているか確認する方法

自分のサイトでエラーが発生しているか確認する方法

まずはエラーの詳細を把握するためにWordPressのデバッグモードを有効にしよう。wp-config.phpに以下の行を追加する。

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

この設定をすると、エラーの内容が/wp-content/debug.logに記録されるようになる。ページが真っ白になる現象は本番環境では特に厄介だが、ログを見ればスタックトレースが残っているため原因を特定できる。スタックトレースの中にwp-admin/includes/class-wp-filesystem-ftpext.phpやwoocommerce-delivery-notesというパスが見つかれば、今回のケースに該当する可能性が高い。

プラグインのコードを修正してエラーを止める手順

プラグインのコードを修正してエラーを止める手順

根本的な修正はプラグイン本体のコードを書き換えることだが、これはプラグインが更新されるたびに変更が上書きされてしまう一時しのぎの対策だ。それでも今すぐエラーを止めたい場合には有効なので、まずは直接修正する手順を説明する。

修正対象のファイルとコードの場所

対象ファイルはプラグインディレクトリ内の includes/helpers/class-utils.php にある get_filesystem() メソッドだ。このメソッドが WP_Filesystem() を引数なしで呼び出している箇所が問題の中心になる。

// 修正前のコード
public static function get_filesystem() {
    global $wp_filesystem;
    if ( ! $wp_filesystem ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
        WP_Filesystem(); // ← 引数がないためFTP接続に失敗する
    }
    return $wp_filesystem;
}

修正後のコード

以下のように書き換える。request_filesystem_credentials() であらかじめ認証情報を取得し、それを WP_Filesystem() に渡す形にする。さらに、request_filesystem_credentials() が認証情報を問い合わせるHTMLフォームを出力してしまうのを防ぐために、出力バッファリングで囲んでおく。

// 修正後のコード
public static function get_filesystem() {
    global $wp_filesystem;
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    if ( ! $wp_filesystem ) {
        ob_start(); // バッファリング開始
        $credentials = request_filesystem_credentials( '' );
        ob_end_clean(); // バッファを捨てる(フォーム出力を抑制)
        WP_Filesystem( $credentials ); // 認証情報を渡す
    }
    return $wp_filesystem;
}
STEP 1 FTPまたはサーバーのファイルマネージャーでプラグインファイルにアクセス
↓
STEP 2 includes/helpers/class-utils.php をテキストエディタで開く
↓
STEP 3 get_filesystem() メソッドを見つけてコードを差し替え
↓
STEP 4 ファイルを保存してアップロードし、動作を確認

この修正を施すと、wp-config.phpに定義されたFTP定数(FTP_HOST、FTP_USER、FTP_PASS)や、WordPressがデータベースに保存している認証情報が自動的に使われるようになる。結果としてFTP接続が正常に確立され、exists()などのファイル操作メソッドが問題なく動作する。

プラグイン更新で修正が消えないようにする恒久対策

プラグイン更新で修正が消えないようにする恒久対策

プラグインのコアファイルを直接編集する方法は、アップデートがあるたびに上書きされてしまうため本番運用には不向きだ。より持続的な対策として、以下のいずれかの方法を選ぶとよい。

子テーマのfunctions.phpでフックを使って上書きする

プラグインが提供しているフィルターフックやアクションフックを利用し、テンプレートのレンダリング時にファイルシステムアクセスが発生する処理を迂回する方法だ。ただし、このプラグインではフックが十分に用意されていない可能性が高いため、テーマのCSSやテンプレートの上書きだけで対応しきれないこともある。

プラグインのIssueトラッカーやサポートフォーラムに修正を依頼する

今回の修正はすでにWordPress.orgのサポートフォーラムにも報告されている。プラグインの開発者がこの修正を取り込めば、次回以降のアップデートで公式に問題が解決される。開発者の対応を待つ間は、前述のファイル直接編集でしのぎつつ、アップデートのたびに修正を再適用する運用になる。

プラグイン全体をフォークして独自バージョンを使う

どうしても自前で管理したい場合は、プラグインのコードをコピーして別のプラグインとしてインストールし直す方法もある。ただし今後のアップデートやセキュリティパッチの追従をすべて自分で行う必要があるため、開発リソースに余裕がある場合に限った選択肢だ。

同じエラーが別のプラグインで出る場合の一般的な対処法

同じエラーが別のプラグインで出る場合の一般的な対処法

今回のエラーは「Print Invoice & Delivery Notes for WooCommerce」に限らず、フロントエンドから WP_Filesystem() を不用意に呼び出しているあらゆるプラグインで発生しうる。バックアップ系、インポート系、PDF生成系のプラグインで似たような「ftp_nlist()」を含むTypeErrorが出た場合は、以下の共通チェックポイントで原因を絞り込める。

  • スタックトレースの2〜3行目に表示されているプラグインのパスを特定する
  • 該当プラグインのファイルシステム呼び出し部分を探す(WP_Filesystem() または $wp_filesystem を grep する)
  • 認証情報の取得が行われているか確認する(request_filesystem_credentials() の有無)
  • PHPのバージョンを確認する(PHP 8.0未満では暗黙の型変換でエラーが表面化しないことがある)
エラーパターンの見分け方
パターンA 管理画面では動くがフロントエンドでだけ落ちる → FTP認証情報の未取得が原因
パターンB PHP 7.xでは問題なかったがPHP 8に上げたら発生 → 型チェック厳格化の影響
パターンC 特定のサーバー環境(ftpext方式)でのみ発生 → ファイルシステム方式の不一致

特に「ftpext」というファイルシステム方式を使用しているサーバー環境で顕著に発生する。多くのレンタルサーバーでは「direct」方式が使われるため問題が起きにくいが、FTP経由でファイル操作を行う設定になっているとこのエラーに遭遇しやすい。

よくある質問

エラーが出ているのに管理画面にはアクセスできるのはなぜか

管理画面ではWordPressがWP_Filesystem()を呼び出す前に自動的に認証情報を収集する仕組みが働くため、正常に動作する。フロントエンドではその仕組みが起動しないため、プラグインが自前で認証情報を取得しない限り接続に失敗する。これが「管理画面では動くのにマイアカウント画面では落ちる」という現象の理由だ。

出力バッファリングをしないとどうなるのか

request_filesystem_credentials() は認証情報が不足している場合にFTPのホスト名やユーザー名を入力するHTMLフォームを画面に直接出力してしまう。フロントエンドのページに突然フォームが表示されると、サイトのレイアウトが崩れたり、ユーザーを混乱させたりする。出力バッファリングでこのフォーム出力を捕捉して破棄することで、見た目に影響を与えずに認証情報だけを取得できる。

wp-config.phpにFTP定数を設定するだけで直らないのか

FTP定数(FTP_HOST、FTP_USER、FTP_PASS)を定義していても、プラグインがWP_Filesystem()を引数なしで呼び出している限り、WordPressは認証情報を探しに行かない。定数はあくまで「情報の置き場所」であり、「その情報を取りに行く処理(request_filesystem_credentials())」が実行されなければ意味がない。コードの修正が不可欠な理由はここにある。

PHP 8にアップグレードした直後から発生したのだが関係あるのか

大いに関係がある。PHP 8.0から関数の引数と戻り値の型が厳密にチェックされるようになり、それまで暗黙的に許容されていたnullの受け渡しがTypeErrorとして検出されるようになった。PHP 7.x時代は同じコードでも警告で済んでいたか、あるいはエラーが発生しても表面的に無視されていた可能性が高い。

この修正でセキュリティ上の問題は起きないのか

起きない。request_filesystem_credentials()はWordPressのコア関数であり、管理画面で日常的に使われている安全な方法だ。認証情報はwp-config.phpの定数やデータベースに保存された情報から取得され、フロントエンドの訪問者にFTPのパスワードが表示されることはない。出力バッファリングでフォームの表示を抑制するのも、余計な情報を露出させないための適切な処置だ。

この記事のポイント

  • WooCommerceの納品書印刷で重大エラーが発生するのは、プラグインがWP_Filesystem()を認証情報なしで呼び出しているのが原因
  • PHP 8の厳格な型チェックにより、FTP接続オブジェクトがnullのまま関数に渡されてTypeErrorが起きる
  • プラグインのclass-utils.phpにあるget_filesystem()メソッドを修正すれば即座に直る
  • request_filesystem_credentials()で認証情報を先に取得し、出力バッファリングでフォーム表示を防ぐのが正しい修正手順
  • プラグインのアップデートで修正が消えるため、恒久対応は開発者による公式修正を待つかフォークしての運用が必要
WC Vendors 2.7.0アップグレードでfatal errorが出た時の対処法

WC Vendors 2.7.0アップグレードでfatal errorが出た時の対処法

WC Vendors 2.7.0 にアップグレードした直後、サイト全体が「このサイトで重大なエラーが発生しました」の表示になり管理画面にもアクセスできなくなった場合、wcvendors_capability_product_types オプションの値が配列ではなく文字列で保存されていることが原因だ。WP-CLI を使えるかどうかで復旧手順を分けて対処する。

WC Vendors 2.7.0で管理画面にすら入れなくなる原因は何か

WC Vendors 2.7.0で管理画面にすら入れなくなる原因は何か

この問題は、プラグイン内部の関数 wcv_is_all_product_types_hidden() が array_diff() を呼び出す際、引数として文字列を渡してしまうことで発生する。PHP 8 環境では型宣言が厳密になったため、ここで TypeError(致命的エラー)が起こり、サイト全体が HTTP 500 エラーで停止する。

関数に渡されるのは wcvendors_capability_product_types というオプションの値だ。このオプションは本来、商品タイプの配列(['simple', 'variable'] など)を保持すべきだが、古いバージョンの WC Vendors(特に 1.x 系)から移行してきたサイトでは、内部的に「simple」のような文字列のまま残っているケースがある。2.7.0 で追加された新しい関数には配列へのキャスト処理が含まれておらず、長期間眠っていた不正なデータが一気に致命的エラーとして表面化した。

2.5.1.1 以前
(array) によるキャストがあり、文字列でも配列として扱われていた
→ エラーは表面化せず、サイトは通常通り稼働
↓
2.7.0 アップグレード後
array_diff() に文字列が直接渡され、PHP 8 で TypeError
→ サイト全体が HTTP 500 エラー、管理画面も開けない
■ エラー発生 ■ 正常動作

エラーのトリガーは wp_loaded フックで、これは WordPress の読み込みのかなり早い段階で実行される。そのため管理画面もフロントエンドも一切表示されず、ダッシュボードからプラグインを無効化するという通常の復旧手段が使えなくなってしまう。

管理画面にログインできない場合の応急処置(WP-CLI を使う)

管理画面にログインできない場合の応急処置(WP-CLI を使う)

サーバーに SSH でアクセスできる環境であれば、WP-CLI を使って不正なオプション値を直接修正するのが最も確実な回復方法だ。管理画面に入れなくても、この操作でサイトは即座に復旧する。

STEP 1 SSH でサーバーに接続する
↓
STEP 2 WordPress のインストールディレクトリに移動する
↓
STEP 3 以下のコマンドを実行し、オプションを配列形式で上書きする
↓
STEP 4 サイトの表示を確認する
wp option update wcvendors_capability_product_types '["simple"]' --format=json

上記コマンドは wcvendors_capability_product_types オプションの値を強制的に JSON 配列として上書きする。["simple"] の部分は、自サイトで実際に有効にしていた商品タイプのスラッグに置き換える。可変商品や外部商品を使っていた場合は、["simple","variable","external"] のようにカンマ区切りで列挙する。値は連想配列ではなく、単純なリスト形式でなければならない。

WP-CLI を使えない場合のエラー復旧手順(FTP/ファイルマネージャー)

WP-CLI を使えない場合のエラー復旧手順(FTP/ファイルマネージャー)

レンタルサーバーでは SSH が利用できず、WP-CLI も使えないケースが多い。その場合は FTP またはサーバー付属のファイルマネージャーを使ってプラグインを一時的に無効化し、管理画面にアクセスできる状態に戻す。

  1. FTP クライアントまたはファイルマネージャーで /wp-content/plugins/ ディレクトリにアクセスする
  2. wc-vendors ディレクトリを探し、名前を wc-vendors-disabled に変更する
  3. ブラウザで管理画面の URL(yourdomain.com/wp-admin/)を開く
  4. 管理画面にログインできたら、wc-vendors-disabled をもとの wc-vendors に戻す(ただし有効化はしない)
  5. 管理画面の「プラグイン」で WC Vendors が「無効」になっていることを確認し、データベースの該当オプションを修正する手順に進む

ディレクトリ名の変更によってプラグインが強制的に無効化される仕組みを利用している。ファイル自体は削除せず、リネームするだけなので元に戻せる。管理画面に戻ったら、次のセクションで説明する根本的なオプション修正を行う。現時点で WC Vendors を再度有効化すると、同じエラーが再発するので有効化してはいけない。

根本的な修正をプラグインのアップデート前に済ませておく

根本的な修正をプラグインのアップデート前に済ませておく

WC Vendors の開発チームはこの問題を認識しており、将来のリリースで修正が反映される見込みだ。しかしアップデートを待つだけでは、同じサイトで別の箇所が再び同様のエラーを起こす可能性を抱えたままになる。管理画面にログインできる状態にしたら、データベース上のオプション値を正規化してしまおう。

最も簡便な方法は、先ほど WP-CLI で実行したコマンドと同じ操作を、データベース管理ツール(phpMyAdmin など)やカスタムスクリプトで行うことだ。具体的には wp_options テーブルを開き、option_name が wcvendors_capability_product_types の行を探す。option_value カラムが文字列(例 simple)になっている場合、a:1:{i:0;s:6:"simple";} のようにシリアライズされた配列表現に書き換える。

PHP のシリアライズ形式を手書きするのはミスが怖い場合は、次のような安心な手順をとる。

  1. WC Vendors が無効化されていることを確認する
  2. 管理画面の「設定」→「WC Vendors」→「販売」→「商品タイプ」に進み、チェックボックスで適切な商品タイプを選んで保存する(これで正常な配列としてオプションが上書きされる)
  3. WC Vendors を有効化する

この方法は管理画面を使うため、データベースを直接触る必要がなく安全だ。ただしバージョン 2.7.0 では有効化した瞬間に wp_loaded フックのエラーが再発する可能性があるため、あらかじめプラグインファイルの該当行を一時的に修正するか、前述のキャストを適用しておくと確実だ。

よくある質問

エラーログを確認するにはどうすればよいか

サーバーのエラーログには wp-content/debug.log(WordPress のデバッグログ)や、Apache/Nginx のエラーログがある。WP-CLI が使えれば wp config set WP_DEBUG true --raw と wp config set WP_DEBUG_LOG true --raw でログ出力を有効にした上で、エラーを再現させて wp-content/debug.log を確認する。共有サーバーではホスティングの管理画面からエラーログが見られることも多い。

WC Vendors をアップグレードする前に何か注意すべきことはあるか

メジャーバージョンアップを行う場合は、本番サイトに適用する前に必ずステージング環境でテストする。どうしても本番で行うなら、事前にデータベースのバックアップを取り、wcvendors_capability_product_types オプションの現在の値を確認しておく。WP-CLI で wp option get wcvendors_capability_product_types --format=json を実行し、値が配列形式になっていることを確かめればリスクを大幅に減らせる。

PHP のバージョンが 7.x 系ならこの問題は起きないか

PHP 7.x では array_diff() に文字列を渡しても Warning は出るが Fatal Error にはならないため、サイトが完全停止することはない。ただしエラー自体の根本原因は同じであり、動作は予期しない結果になる可能性が高い。推奨される PHP のバージョンは 8.x 系へ移行することであり、その前に対処しておくことが望ましい。

WC Vendors Free と Pro のどちらでも発生するのか

この問題は WC Vendors の無料版(Free)のコア機能部分で発生している。Pro 版を併用している場合でも、同じオプション値を読み込むため影響を受ける可能性が高い。特に 1.x 系からバージョンを重ねてきたサイトは要注意だ。

この記事のポイント

  • WC Vendors 2.7.0 へのアップグレードでサイトが停止するのは array_diff() の型不一致が原因
  • 古いサイトでは wcvendors_capability_product_types が文字列で残っているケースがある
  • WP-CLI を使えるなら wp option update で即座に復旧できる
  • WP-CLI が使えない場合はプラグインディレクトリをリネームして管理画面に復帰する
  • 根本的には設定画面で商品タイプを保存し直すか、データベースの値を配列に正規化する
WordPressサイトがハッキングされた時の難読化PHPコードの見つけ方と完全駆除手順

WordPressサイトがハッキングされた時の難読化PHPコードの見つけ方と完全駆除手順

WordPressサイトのindex.phpなどのコアファイルに難読化された悪意あるPHPコードが埋め込まれ、不審なサイトへリダイレクトされる被害が発生した場合、感染ファイルの完全削除とコアファイルの再インストール、全認証情報の変更を同時に進める必要がある。

この種の改ざんは「goto文」や16進数エンコードした文字列、外部サーバーとの通信機能などを使って巧妙に隠されている。残骸を残さず駆除し、再侵入経路をすべて塞ぐ手順を具体的に追っていく。

なぜWordPressサイトに難読化された悪意あるコードが仕込まれるのか

なぜWordPressサイトに難読化された悪意あるコードが仕込まれるのか

攻撃者が難読化コードを埋め込む目的は大きく3つある。不正なリダイレクトによるアフィリエイト収益の横取り、サイト訪問者の個人情報やパスワードの窃取(フィッシング)、そしてサイト自体を踏み台にして別のサーバーを攻撃するスパム配信やDDoS攻撃の中継だ。

コードの難読化は、セキュリティプラグインや手動の目視検査をすり抜けるために施される。変数名や関数名をランダムな文字列にし、goto文で処理の流れを複雑に飛ばすことで、実際に何を実行しているのかを読み取りにくくしている。この手のコードは、単独のファイルに留まらず、複数のテーマファイルやアップロードディレクトリに分散して設置されることが多い。

ハッキングの兆候(BEFORE)
  • ● トップページが別ドメインにリダイレクトされる
  • ● index.phpの先頭に見慣れないgoto文や16進数文字列のブロックがある
  • ● 管理画面が重くなり、プラグインの一覧に覚えのない項目が増えている
↓
駆除後の正常状態(AFTER)
  • ● サイトのURLが正しく表示され、意図しない転送が発生しない
  • ● コアファイルが公式のチェックサムと完全一致する
  • ● 管理者アカウントやプラグインに不審な追加がない
■ 感染中 ■ 駆除後

上記の例は、攻撃者が検索エンジンのボットを判別し、Googleなどのクローラーには元の正常なページを見せ、一般の訪問者だけに不正サイトへ飛ばす「クローキング」という手法の典型的な挙動を示している。

感染したWordPressサイトを手動で完全に駆除する手順

感染したWordPressサイトを手動で完全に駆除する手順

感染が疑われるファイルとディレクトリを優先して検査する

改ざんされたコードは、WordPressのルートディレクトリ直下のindex.phpやwp-config.phpだけでなく、テーマやプラグインのディレクトリ、アップロードディレクトリにも潜む。次の場所を重点的に確認する。

  • ルートディレクトリの全.phpファイル(index.php、wp-load.php、wp-settings.phpなど)
  • /wp-content/themes/ 配下の全テーマ(特にアクティブなテーマのfunctions.php)
  • /wp-content/plugins/ 配下の全プラグインファイル
  • /wp-content/mu-plugins/(もし存在すれば)
  • /wp-content/uploads/ 配下のPHPファイル(本来PHPファイルが存在すべきではないディレクトリ)
  • /wp-includes/ 配下のコアファイルの改ざん有無(後述のチェックサム検証で判別可能)
  • .htaccess ファイル(リダイレクトルールや不正なコードブロックが追記されていないか)

WordPressコアファイルの再インストールとチェックサム検証

wp-content ディレクトリと wp-config.php を除き、WordPressのコアファイルをすべて公式の配布ファイルで上書きするのは安全かつ最も確実な駆除方法だ。管理画面の「ダッシュボード」→「更新」から「再インストール」を実行するか、手動で行う場合は次の手順を踏む。

STEP 1 WordPress公式サイトから最新のzipファイルをダウンロードし展開する
↓
STEP 2 現在のサイトから wp-content ディレクトリと wp-config.php をローカルにバックアップ(退避)する
↓
STEP 3 サーバー上の wp-admin と wp-includes を完全に削除し、展開した公式ファイルの同名ディレクトリをアップロード
↓
STEP 4 ルートディレクトリの全 .php ファイル(wp-config.php を除く)を公式ファイルで上書き
↓
STEP 5 退避しておいた wp-content と wp-config.php をサーバーに戻し、wp-config.php に手動で追記された不正な行がないか目視確認
↓
STEP 6 wp-includes/version.php を確認し、バージョン番号が正しいか検証。その後、プラグイン「WordPress Core Verify」などでチェックサムを検証する

管理画面にアクセスできるなら「ダッシュボード」→「更新」の「WordPressを再インストール」ボタンを押すだけで同等の処理が完了する。手動で行う場合も、wp-content と wp-config.php を保護している限り、データ消失の心配はない。

感染源を特定するためにサーバーログとタイムスタンプを照合する

どの脆弱性から侵入されたかを特定するには、改ざんされたファイルのタイムスタンプとサーバーのアクセスログ、エラーログを突き合わせる。ログに「POST /wp-admin/admin-ajax.php」や「POST /xmlrpc.php」への不自然な連続リクエストが記録されていれば、ブルートフォース攻撃やXML-RPCを経由した侵入が疑われる。

ホスティングのコントロールパネルから「Raw Access Logs」や「エラーログ」をダウンロードし、改ざん発生日時の前後を中心に調べる。プラグインやテーマのアップデートを長期間放置していた場合は、脆弱性情報データベース(CVE)で該当バージョンの既知の脆弱性を検索し、侵入経路の仮説を立てる。

適切なファイルパーミッションに設定し直す

ディレクトリは755、ファイルは644を基本とし、wp-config.phpだけは440または400に設定して読み取り権限を厳格にする。特にwp-content/uploads/ 配下にPHPファイルが置かれている場合は、そのディレクトリでPHPの実行を.htaccessで明示的に拒否しておくべきだ。

# .htaccess を wp-content/uploads/ に設置する場合
<FilesMatch "\.(php|php\.)$">
    Require all denied
</FilesMatch>

プラグインとテーマをクリーンな状態に置き換える

すべてのプラグインとテーマを、公式ディレクトリまたは購入元から再ダウンロードした完全に新しいファイルで上書きする。非公式サイトから入手したプラグインやテーマ、長期間更新が止まっているものは、この機会に削除するか代替に切り替える。

とくに mu-plugins ディレクトリは、手動で設置しない限り通常は存在しない。見慣れないディレクトリ名やPHPファイルがあれば、即座に削除する。

データベース内の不正なコードや管理者アカウントを検査する

phpMyAdminやWP-CLIを使って、wp_options テーブルの「siteurl」や「home」、wp_posts の投稿内容に不審なスクリプトタグやiframeが埋め込まれていないかを調べる。同時に、wp_users テーブルで身に覚えのない管理者アカウントが追加されていないかも必ず確認する。

データベース内の隠し管理者を一括で探すには、WP-CLIで次のコマンドを実行すると早い。

wp user list --role=administrator

全認証情報を変更しセキュリティキーを再生成する

駆除作業が完了したら、WordPress管理画面の全ユーザーパスワード、データベースパスワード、SFTP/SSHパスワード、ホスティングのコントロールパネルパスワードをすべて変更する。wp-config.php の「AUTH_KEY」「SECURE_AUTH_KEY」などのセキュリティ用ソルトも、WordPress.orgの公式ソルト生成ツールで新しいものに置き換える。

よくある質問

クリーンなバックアップがない場合、復旧の優先順位はどう決めればよいか

バックアップが存在しない、またはバックアップ自体が感染後のものである場合は、コアファイルの再インストールとプラグイン・テーマの全置き換えを最優先する。データベースの修復はその後で、不正な投稿やユーザーを手動で削除する。サイトのダウンタイムを最小限にするため、メンテナンスモードを有効にして作業するのが安全だ。

無料のプラグインやテーマが感染の原因になることはあるか

公式ディレクトリで配布されている無料プラグインでも、深刻な脆弱性が発見されて更新が滞れば攻撃の糸口になりうる。とくに、公式ディレクトリ以外のサイトから配布されている「nulled(クラック)」版の商用プラグインやテーマは、ほぼ確実にバックドアが仕込まれているため、絶対に使用してはいけない。

データベースを直接編集する際の注意点は何か

phpMyAdminなどでデータベースを直接操作する場合は、必ず事前にデータベース全体のダンプ(エクスポート)を取っておく。wp_options テーブルの「active_plugins」行を誤って削除すると全プラグインが無効化されるため、行の値を直接編集するよりも、WP-CLIのwp optionコマンドを使うほうが安全に操作できる。

cronジョブに疑わしいタスクが仕込まれていないか調べる方法は

WordPressの疑似cron(WP-Cron)はwp_options テーブルの「cron」オプションに格納されている。WP-CLIでwp cron event listを実行すると登録されている全タスクを一覧できる。ホスティング側の本物のcronジョブ(crontab)に不正なエントリが追加されていないかも、ホスティングの管理画面またはSSHで確認する。

攻撃者に検索エンジンのボット判定をすり抜けられた場合の追加対策は

難読化コードがボット判定を行っている場合、駆除後もクローキング用のキャッシュがCDNや検索エンジンに残っている可能性がある。サイト駆除後にGoogleサーチコンソールで「URL検査」→「インデックス登録をリクエスト」を実行し、CDNの全キャッシュをパージする。HTTPヘッダーを改ざんされていないかも、curlコマンドなどで確認しておくべきだ。

この記事のポイント

  • 難読化PHPコードは複数ファイルに分散して設置されるため、ルート直下、テーマ、プラグイン、アップロードディレクトリをすべて検査する
  • WordPressコアファイルは wp-content と wp-config.php を保護したうえで公式ファイルで完全に上書きする
  • 改ざんファイルのタイムスタンプとサーバーログを照合し、侵入経路を特定して永続的な対策を講じる
  • 全認証情報の変更とソルトの再生成を駆除と同時に行い、再侵入を防ぐ
  • クリーンなバックアップがある場合は、全ファイルとデータベースの削除後にバックアップから復元するのが最も確実な方法である
WooCommerce 決済手数料が大文字IDで効かない時の直し方

WooCommerce 決済手数料が大文字IDで効かない時の直し方

WooCommerce で「Checkout Fees for WooCommerce」などのプラグインを使い、支払い方法ごとに手数料を設定しているのに、特定の決済ゲートウェイだけルールがまったく反応しない。大文字を含む ID を持つ決済方法でこの症状が出ているなら、プラグイン内部の ID 比較処理で大文字と小文字が一致せずに無視されている可能性が高い。functions.php に1行のフィルターフックを追加するだけで即座に解消する。

なぜ特定の決済方法だけ手数料や割引が適用されないのか

なぜ特定の決済方法だけ手数料や割引が適用されないのか

Checkout Fees for WooCommerce は、支払い方法の ID をキーにして「どのゲートウェイに手数料をのせるか」を管理している。設定画面でルールを保存するとき、プラグインは WordPress の sanitize_key() 関数を通して ID をすべて小文字に変換し、データベースに格納する。ところが実際のチェックアウト画面で選択された決済方法の ID を取得する段階では、この小文字化(正規化)が行われない。

その結果、もともと ID が小文字のみで構成されているゲートウェイ(例 wc_zibal)は、保存値と取得値が同じ文字列になるため問題なく動く。一方で WC_Sep_Payment_Gateway や WC_Gateway_TorobPay のように大文字を含む ID の場合、保存された wc_sep_payment_gateway というキーと、実際にチェックアウト時に渡される WC_Sep_Payment_Gateway という文字列が一致せず、該当するルールが「存在しない」と判定されてしまう。

手数料設定が反応しない原因を視覚的に理解する

手数料設定が反応しない原因を視覚的に理解する
Before(大文字混在で不一致)
■ 保存値 wc_sep_payment_gateway
■ 取得値 WC_Sep_Payment_Gateway
→ 不一致。ルールは「なし」と判定される
↓
After(sanitize_key で小文字統一)
■ 保存値 wc_sep_payment_gateway
■ 取得値 wc_sep_payment_gateway
→ 一致。該当する手数料ルールが適用される
■ 不一致状態 ■ 正規化後

functions.php にフィルターフックを追加して即座に解決する手順

functions.php にフィルターフックを追加して即座に解決する手順

プラグイン本体のコードを直接編集しなくても、子テーマの functions.php に数行のコードを追加するだけでこの問題は修正できる。以下のフィルターフックは、チェックアウト時に渡されるゲートウェイ ID を sanitize_key() で小文字に揃え、プラグインが正しくルールを照合できるようにする。

STEP 1 子テーマの functions.php を開く(なければ作成する)
↓
STEP 2 以下のコードをファイル末尾に追加する
↓
STEP 3 保存してキャッシュをクリアし、実際のチェックアウトで動作を確認する

追加するコード

add_filter( 'alg_wc_add_default_gateway_on_cart', function ( $gateway ) {
    return sanitize_key( $gateway );
} );

このフィルターフックは、プラグインが決済ゲートウェイの ID を参照する直前に割り込み、ID を小文字だけの文字列に変換する。もともと小文字のゲートウェイには何の影響も与えず、大文字を含む ID だけが正規化される。コードを追加したあとは、WooCommerce のシステムステータス画面から Transients(一時データ)を削除するか、WP Rocket や W3 Total Cache などのキャッシュプラグインを使っているなら全キャッシュのクリアを忘れない。

どうしても functions.php を触りたくない場合の代替手段

コードの追加に抵抗がある場合、Code Snippets プラグインをインストールして同じコードをスニペットとして登録する方法も有効だ。管理画面の「スニペット」→「新規追加」から上記のコードを貼り付け、「サイトのフロントエンドで実行」を選択して有効化すれば、テーマファイルを直接編集せずに済む。

決済ゲートウェイごとの手数料が今後も安定して動くようにするために

決済ゲートウェイごとの手数料が今後も安定して動くようにするために

このバグは Checkout Fees for WooCommerce の無料版に限らず、同様の仕組み(sanitize_key で保存し、未正規化の ID と比較する)を持つ他の手数料系プラグインでも発生しうる。ID に大文字を使う決済ゲートウェイは海外のプロバイダーに多く見られ、特に中東やアジア圏のローカル決済サービスを WooCommerce に追加している場合に遭遇しやすい。

根本的にはプラグイン開発元が比較処理の前段で正規化を実装することが望ましいが、今回紹介したフィルターフックを適用しておけば、プラグインのアップデート後も変更が上書きされる心配はほぼない(functions.php または Code Snippets に追加したコードはプラグイン更新の影響を受けない)。

また、新しく決済ゲートウェイを追加したときにも同じ問題が起きるか事前にチェックする習慣をつけると、売上に直結するチェックアウト画面のトラブルを未然に防げる。テスト用の注文を入れて手数料が正しく加算されるか、割引が適用されるかを確認しておく。

よくある質問

この修正は Checkout Fees for WooCommerce の有料版でも必要か

本記事執筆時点では、無料版と有料版でゲートウェイ ID の正規化ロジックに違いは確認されていない。有料版でも同様に大文字混在の ID でルールが動作しなくなる場合があるため、症状が出たら同じフィルターフックを試して問題ない。

すべて小文字の ID しか使っていないが、念のためコードを追加しても害はないか

sanitize_key() はすでに小文字の文字列には何も変更を加えない。もともと正常に動いている環境にコードを追加しても、挙動が変わることは一切なく、安全に設置できる。

functions.php を編集したら画面が真っ白になった

PHP の構文エラーが原因で「このサイトで重大なエラーが発生しました」と表示されることがある。コードの貼り付け位置やセミコロンの抜けを確認する。FTP やホスティングのファイルマネージャーで functions.php を開き、追加したコードをいったん削除して復旧させてから、Code Snippets プラグイン経由で再度追加するほうが安全だ。

カスタマイズしたコードがプラグインのアップデートで消えたりしないか

子テーマの functions.php に書いたコードや Code Snippets プラグインに登録したスニペットは、プラグイン本体のアップデートとは完全に独立して保存される。アップデートのたびに再設定する必要はない。

別の手数料プラグインでも同じ現象が起きるか

ゲートウェイ ID を sanitize_key() で保存し、比較時に正規化していないプラグインであれば、同様の不具合が起こる。WooCommerce の決済ゲートウェイ関連プラグインは広くこの設計パターンをとっており、大文字を含む ID を持つ決済手段を導入したとたん手数料が効かなくなる、という報告は定期的に見られる。

この記事のポイント

  • 大文字を含む決済ゲートウェイ ID で手数料が効かないのは、保存時と取得時の文字列の不一致が原因
  • functions.php に1行のフィルターフックを追加すれば、即座に大文字・小文字の差を吸収できる
  • コード編集が不安なら Code Snippets プラグインを使うと同じ修正を安全に適用できる
  • プラグインのアップデートや新しい決済手段の追加時に備えて、テスト注文での動作確認を習慣化する