WordPressのプラグイン開発は、たいてい同じ道をたどります。予約フォーム、フィードの取り込み、決済画面の追加項目が必要になり、開発者がそれを書き、動き、みんな次の仕事へ移ります。二年後、そのサイトは古いバージョンのWordPressに取り残されています。更新してもそのプラグインが無事でいられると誰も確信できず、書いた本人はもういないからです。

原因がコアの変化の速さにあることはまれです。WordPressは互換性を壊すことに保守的で、五年前に書かれた出来のよいプラグインの多くは、今もWordPress 7.1の上で手を入れずに動いています。プラグインが壊れるのは、最初の一週間で下された判断のせいです。機能をテーマに置いたこと、フックせずにコアのファイルを書き換えたこと、データをいちばん手近な構造に放り込んだこと、そして誰もリリース候補版で試さなかったことです。

カスタムWordPressプラグインがコアの更新に耐えるには何が必要でしょうか。 四つです。コードがテーマではなくプラグインの中にあること。コアのファイルを書き換えるのではなく、アクションとフィルターを通してWordPressを拡張していること。データの一つひとつを、その形に合った構造に保存していること。そして、リリース候補版が出るたびに、それが正式に公開される前に誰かが試していること。


なぜWordPressプラグイン開発はテーマではなくプラグインに属するのか

カスタムコードの既定の置き場所は、テーマのfunctions.phpです。すでにそこにあり、すでに動いているからです。そしてそれは、次のリニューアルで消えるファイルでもあります。

テーマは見た目です。テーマを入れ替えれば、古いテーマがやっていたことはすべて止まります。カスタム投稿タイプは登録されなくなり、コンテンツは管理画面もパーマリンクもないままデータベースに残ります。ショートコードはページの途中に生の文字列として現れます。アクセス解析のタグも、構造化データも、CRMへの夜間の連携も一緒に消え、エラーは一つも出ません。

規則は、発注書に書けるほど単純です。リニューアルのあとも成り立っていなければならないものは、すべてプラグインに入れます。カスタム投稿タイプとタクソノミー、外部との連携、ショートコードとブロック、業務ルール、定期実行の処理、そしてデータベースに書き込むもの全部です。テーマにはテンプレートとスタイルとテンプレートパーツを残します。

請求は遅れてやってきます。次のリニューアルの時点で、すでに存在していたものを作り直す費用をもう一度払うか、古いfunctions.phpをそのまま引き継ぐかのどちらかです。何年もスニペットを積み上げてきたサイトでは、これは避けられたはずの作業として数千ポンド規模になり、リニューアルの見積もりが依頼側の想定の二倍で返ってくる理由でもあります。子テーマもテーマであることに変わりはありません。

拡張の仕組みと、たった一つの絶対的な規則

WordPressは外側から変えられるように作られています。その仕組みがフックです。フックのドキュメントは、フックを、あるコードが別のコードに干渉したり手を加えたりできる、あらかじめ決められた地点だと説明しています。アクションは決まった瞬間に発火し、そこで何かをさせてくれます。投稿の公開後に通知を送る、投稿タイプを登録する、といった具合です。フィルターは値を渡してきて、変えるかそのままにするかを求め、そして返すことを求めます。

そこから導かれる規則は絶対です。wp-adminwp-includes、あるいは他のプラグインのディレクトリの中のファイルを編集しているなら、その時点で負けています。その変更は次の更新で消し飛びます。警告もエラーもなく、たいていは何かが動かなくなったと顧客から連絡が来るまで誰も気づきません。採用する前に、開発者に直接これを聞いてください。

必要なフックが存在しないときは、置き換えるのではなくふるまいを包む、もっと広いフックまで上がる、バージョン管理の下でサードパーティ製のプラグインをフォークして差分を文書に残す、あるいは上流にフックの追加を頼む、という手があります。今あるフックの多くは、そうやって追加されてきました。

命名、接頭辞、そして非常に混み合った一つの名前空間

WordPressの中のPHPは、コアとも、有効なテーマとも、他のすべての有効なプラグインとも共有する単一のグローバル名前空間で動きます。get_settings()という関数を両方が宣言する二つのプラグインは、行儀よく譲り合ったりしません。二つ目は致命的なエラーになり、サイトは真っ白になります。

接頭辞は思っているより長い

ハンドブックのプラグインのベストプラクティスのページは、グローバルに参照できるものすべてに固有の接頭辞を求めています。最低四文字、できれば五文字、ありふれた英単語を避け、wp__WordPressそのものは絶対に使わないこと。数万本のプラグインが流通している以上、顧客の頭文字三つは運任せです。

名前空間とオートローディング

現代的なやり方は、問題の半分を解決します。PHPの名前空間を宣言し、一つのファイルに一つのクラスを置き、PSR-4のオートローダーに探させます。手書きのrequireは不要になり、クラス名が他のプラグインとぶつかる余地もなくなります。テストしやすくもなります。コンストラクタで依存を受け取るクラスは、WordPressを読み込まなくても生成できるからです。

オートローディングは、二つのプラグインが同じライブラリの別のバージョンを同梱する問題までは解決しません。先に読み込まれたほうが勝ちます。配布するものについては、ビルドの時点でベンダーの名前空間に接頭辞を付けてください。

名前空間では守れない文字列

名前空間が覆うのはPHPのシンボルです。プラグインが登録するものの多くはPHPのシンボルではなく、共有のレジストリに置かれる文字列であり、そちらには昔ながらの接頭辞の慣習が今も必要です。フック名、オプションとトランジェントのキー、投稿メタのキー、投稿タイプとタクソノミーの名前、ショートコードのタグ、cronイベントの名前、RESTの名前空間、そして独自テーブルの名前。これらは平坦な空間に住んでいて、最後に登録したものが勝つか、二つのプラグインが黙って状態を共有するかのどちらかになります。

何かに名前を付ける前に知っておくべき制限が二つあります。投稿タイプのキーは20文字を超えてはならず、タクソノミーのキーは32文字を超えてはなりません。どちらも小文字の英数字にハイフンとアンダースコアだけです。五文字の接頭辞を使えば投稿タイプ名に残るのは15文字で、これは聞いた印象よりずっと窮屈です。

データをどこに置くかを選ぶ

これは最も長く尾を引く判断です。間違えたプラグインは公開時には問題なく動き、データが増えるにつれて毎月遅くなり、誰かが気づいたころには修正が編集ではなく移行になっています。

オプションとトランジェント

オプションはサイト全体の設定のためのものです。キーは数えるほど、値は小さく、ほとんどのリクエストで読まれます。落とし穴は自動読み込みです。自動読み込みに設定されたオプションは、それを使うかどうかにかかわらず、admin-ajaxやRESTの呼び出しも含めたすべてのリクエストで取得されるからです。

WordPress 6.6でこの仕組みが変わりました。大きなオプションの自動読み込みを無効にする件についてのMake WordPress Coreの記事が、そのとおり述べています。保存される値はonoffautoのいずれかになり、150,000バイトを超えるオプションは既定では自動読み込みされません。しきい値はwp_max_autoloaded_option_sizeフィルターで調整できます。これは上限であって目標ではないと考えてください。トランジェントは期限付きのオプションであり、外から取ってきたものを置く正しい場所です。

投稿メタはキーバリューストアではない

投稿メタは一つの投稿の属性のためのものです。サブタイトル、価格、仕入先の参照番号。汎用のキーバリューストアではなく、その理由はテーブルの定義を見ればわかります。wp_postmetaテーブルには四つの列と三つのキーがあります。索引が張られているのはpost_idmeta_keyの先頭191文字だけです。meta_value列はlongtextで、索引はまったくありません。

したがって、メタの値で絞り込むクエリは索引を使えません。メタクエリの条件が一つ増えるたびに結合が一つ増え、50,000件の投稿がそれぞれ20行のメタを抱えるサイトでは、テーブルは百万行になります。条件が三つなら、ページを表示するたびに百万行への結合が三回です。一年目は速かったサイトが三年目には使い物にならなくなる、最もありふれた理由の一つであり、WooCommerceの性能改善の現場では絶えず出てきます。

カスタム投稿タイプとタクソノミー

カスタム投稿タイプが正しいのは、その対象がコンテンツであるときです。独自の一覧画面、パーマリンク、リビジョン、編集の流れが必要で、誰かが訪れられるページとして意味を持つときがそれにあたります。カスタムタクソノミーが正しいのは、それらをまとめる共通の語彙が必要で、その語彙自身のアーカイブページに値するときです。

どちらも仕組みを無料で連れてきます。管理画面、権限、検索、ブロックエディター、そしてREST APIです。show_in_restをtrueにしないとブロックエディターはその投稿タイプを扱えません。また、どちらもinitフックで登録し、それより前に登録してはいけません。

本当に独自テーブルが必要なとき

独自のテーブルが正しいのは、そのデータがコンテンツでないときです。イベントログ、取り込みのキュー、価格の履歴、監査の証跡のような、追記だけの大量の記録。あるいは、投稿の列にない項目で絞り込んだり並べ替えたりするものです。数十万行を超え、自分の項目で問い合わせる規模になると、適切な索引を持つテーブルは投稿メタを桁違いに上回り、増えても挙動が読めます。

代償は、すべてを自分で持つことです。テーブルの作成とバージョン管理された移行、uninstall.phpでの後始末、独自の管理画面、RESTのエンドポイント、そしてキャッシュ。だからこそ、ほとんどのプラグインにとって正直な答えは、やはりカスタム投稿タイプなのです。

セキュリティは四つの習慣であり、そのうち三つが飛ばされる

WordPressのセキュリティハンドブックは原則をはっきり述べています。利用者の入力も、サードパーティのAPIも、すでに自分のデータベースに入っているデータも信用しないこと。リスクのほとんどは四つの習慣が背負っていて、私たちが監査するプラグインでは、決まった順番で飛ばされています。まず権限チェック、次にnonce、三番目に出力のエスケープ。プリペアドステートメントが最後なのは、抜けているとレビューで見つかるからです。

権限チェック

何かを変更するハンドラーはすべて、この利用者にそれが許されているかを尋ねなければなりません。つまりcurrent_user_can()に具体的な権限を渡し、それを呼び出すボタンの周りだけでなく、ハンドラーの内側で確認するということです。

is_admin()は権限チェックではありません。リクエストがサイトのどちら側に来ているかを報告するだけで、admin-ajaxのエンドポイントを叩いているログイン済みの購読者に対してもtrueを返します。権限チェックのないadmin_post_wp_ajax_のアクションは、登録済みのすべての利用者から到達できます。ショップならば、一度でも注文したことのある顧客全員です。この周辺のサイト単位の対策は、WordPressのセキュリティ強化チェックリストで扱っています。

nonce

nonceは、利用者が意図していないリクエストからフォームやURLを守ります。フォームではwp_nonce_field()を、ハンドラーではcheck_admin_referer()を、AJAXならcheck_ajax_referer()を使ってください。名前に反して一度きりのものではありません。一定の時間だけ有効なハッシュであり、既定では一日、二段階の刻みを使う仕組みのため、実際の寿命は12時間から24時間のあいだになります。

nonceのドキュメントは、認証、認可、アクセス制御をnonceに頼ってはならないと明言しています。nonceが示すのは、そのリクエストが自分のフォームから来たということだけです。その人物にその操作が許されているかどうかについては、何も語りません。

入るときに無害化し、出るときにエスケープする

できるところでは検証してください。検証は具体的だからです。郵便番号は形式に合うか合わないかのどちらかです。検証できないところでは無害化します。項目に応じてsanitize_text_field()sanitize_email()sanitize_key()absint()wp_kses_post()を使い分けます。

そして出力する場所で、毎回エスケープします。esc_html()esc_attr()esc_url()wp_kses_post()のいずれかです。エスケープのドキュメントは、これをできるだけ遅く行うよう求めています。そうすればレビューする人は、エスケープと出力を同じ行の上で見られます。

エスケープは何よりも飛ばされます。飛ばしても何もおかしく見えないからです。誰かが入力欄にscriptタグを入れるまで、ページは完璧に表示され続けます。

プリペアドステートメント

自分で書いたクエリはすべて$wpdb->prepare()を通します。整数には%d、浮動小数点数には%f、文字列には%s、テーブル名や列名のような識別子には%iを使います。プレースホルダーは引用符で囲まず、パーセント記号そのものは二つ重ねて書き、LIKEのワイルドカードはクエリに直接書かずに置換する引数の側に入れます。変数をSQLに連結するのは書き方の好みの問題ではなく、それ自体が脆弱性です。

REST APIとブロックエディター

今年書かれるプラグインは、データをREST APIで公開し、設定を手作りのオプションページではなくエディターから扱えるようにすべきです。

ルートはrest_api_initフックでregister_rest_route()を使って登録します。WordPress 5.5以降、permission_callbackの引数は必須で、省略すると該当するルートを名指しした_doing_it_wrong()の通知が出ます。本当に公開したいエンドポイントには__return_trueを使います。それがこの設計の狙いで、ルートを公開にすることが、書き忘れではなく意図的な一行になるわけです。カスタムエンドポイントのドキュメントは引数のスキーマも扱っています。無害化と検証のコールバックはそこに置くもので、そうすれば不正な入力がハンドラーに届くことはありません。

設定はregister_setting()で登録し、show_in_restをtrueにします。それだけでコアの設定エンドポイントに載るので、ブロックエディターや外部のスクリプトが、認証と権限と検証をすでに面倒みてくれるインターフェース越しに読み書きできます。オプションページも、そのnonceも、そのフォームのハンドラーも、そこに住んでいたバグも消えます。

ブロックはblock.jsonファイルから登録します。WordPress 5.8以降、これが推奨される正式な方法です。ブロックのメタデータのドキュメントは利点を説明しています。そこで宣言された資産は、プラグインが有効だからといってサイト全体で読み込まれるのではなく、そのブロックが現れるページでだけ読み込まれます。

プラグインの内側での性能の規律

監査で見つかるプラグイン起因の遅さの大半は、四つのことで説明がつきます。四つとも避けるのは安く、あとから直すのは高くつきます。一つ目は自動読み込みのオプションです。すべてのリクエストで、この先ずっと代償を払い続けるからです。

二つ目は、ページの読み込み中に行われるキャッシュなしの外部リクエストです。仕入先のAPIへのキャッシュなしのwp_remote_get()は、訪問者の全員がその仕入先を待つということです。仕入先が遅ければ自分のサイトが遅くなり、落ちていればタイムアウトが切れるまでサイトが固まります。応答はトランジェントにキャッシュし、明示的なタイムアウトを設定し、呼び出しが失敗したときにページに何を表示するかをあらかじめ決めておいてください。

三つ目はループの中のクエリです。200行のそれぞれに対してget_post_meta()を呼べば、メタのキャッシュが温められていない限り往復は200回です。WP_Queryは、任せておけばそれを温めてくれます。修正はたいてい、何かを無効にするのをやめることであり、これはCore Web Vitalsの監査で見つかるものの大半に当てはまります。

四つ目は、誰かのページのリクエストの内側で行われる仕事です。WP-Cronはシステムのcronではありません。ページの読み込みで起動されるので、定期実行の処理は訪問者のリクエストの中で走り、人の少ないサイトでは二時の処理が、五時に誰かが訪れるまで走りません。DISABLE_WP_CRONを定義し、wp-cron.phpを本物のシステムのスケジューラーから叩き、処理は短く、何度実行しても同じ結果になるように保ってください。

コアの更新を生き延びる、誰も予算に入れない工程

コアが何かをいきなり削除することはめったにありません。関数は非推奨になり、動き続け、通知を出します。だからこそWP_DEBUGを有効にしたステージング環境を動かしておくことが、手に入るなかで最も安上がりな早期警報になります。非推奨の通知は、まだ安いうちに直しておけという日付入りの招待状です。

不意打ちを防ぐ手順は、四半期に一時間ほどで済みます。コア開発のブログを追って、ベータとリリース候補版がいつ出たかを知ること。リリース候補の時期に公開されるフィールドガイドを読むこと。そこにはそのリリースの開発者向けの新機能と、互換性を壊す変更が並んでいます。そのうえでリリース候補版をステージングの複製に載せ、プラグインの実際の機能について書き出したスモークテストを流します。

対応バージョンの扱いは、コードと同じくらい重要です。WordPressが要求するPHPは絶対的な下限が7.4で、推奨は8.3以降、あわせてMariaDB 10.11またはMySQL 8.0です。プラグインのヘッダーのRequires PHPRequires at leastを正直に書き、そのうえで開発者のノートパソコンで動いているものではなく、自分が宣言した最も低いバージョンで試してください。

自分のプラグインにはセマンティックにバージョンを付け、それを本気で守ってください。パッチは何かを直し、マイナーは何も壊さずにふるまいを足し、メジャーは壊してもよいがその代わり何を壊したかを述べます。自動更新に任せている顧客は、その約束を頼りにしています。

配布、ライセンス、そして更新がサイトに届くまで

WordPressはGPLバージョン2以降のもとで公開されており、wordpress.orgのライセンスのページは、プラグインとテーマはライセンスを受け継ぐ二次的著作物であるというプロジェクトの立場を示すと同時に、何が二次的著作物にあたるかについて法的にはあいまいな領域があることも認めています。

ソースは必ず手に入り、他の誰にでも改修を頼めます。GPLがしないのは、それを公開させることです。だから一社のために作られたプラグインは非公開のままでいられます。開発者が同じ成果物を別の誰かに売ることも止めません。独占が重要なら、それはライセンスではなく契約の条項です。

プラグインを公式のディレクトリに載せるなら、プラグインディレクトリのガイドラインを満たす必要があります。全部で18項目です。一つ目は、画像も含めてパッケージ内のすべてにGPLと両立するライセンスを求めています。ほかにも、支払いやアップグレードの向こう側に機能を閉じ込めるトライアルウェアを排除し、難読化されたコードを禁じ、同意なく利用者を追跡することを禁じ、許可なく公開サイトにリンクやクレジットを足すことを禁じています。

非公開のままにするなら、更新は自分の問題になります。Update URIヘッダーを設定してください。これは、非公開のプラグインが同じような名前のディレクトリ上のプラグインで上書きされるのを防ぐために存在します。そのうえで自分のエンドポイントから更新を配信します。これを最後に回すと、顧客がFTPで更新する羽目になります。

カスタムWordPressプラグインの費用

以下の価格帯は、ここで説明した水準で納品する仕事に対する、英国の制作会社のGBPでの価格です。つまりテストと文書があり、公開後に責任を持つ担当者が決まっている状態のものです。WordPressとPHPをこなせる開発者の請求はおおよそ一日£400から£600なので、これは単価ではなく範囲についての話になります。

小さなユーティリティ系のプラグインは£1,500から£3,000です。仕事は一つ、フックはいくつか、設定の切り替えが一つあるかどうか。リダイレクトの処理、注文への項目の追加、仕入先への夜間の書き出しといったものです。

中規模の連携は£3,000から£15,000です。認証と再試行とエラー処理を伴うサードパーティのAPI、カスタム投稿タイプ、管理画面、そして背後で走る処理。最もよく発注される規模であり、最も見積もりを外される規模でもあります。連携そのものは一週間でも、失敗の扱いに二週間かかるからです。

本格的な製品としてのプラグインは£20,000から£75,000、あるいはそれ以上です。独自のテーブル、ブロックエディターの画面、ライセンスと更新の仕組み、マルチサイトへの対応、そして公開の初日から始まるサポートの負担があります。

安いほうを選ぶことが、自動的に間違いなのではありません。間違いになるのは、その価格が、下に挙げる納品物を黙って外した範囲から出てきたときです。見積もりの読み方はWordPress開発者の単価と確認すべきことの記事で扱っており、この仕事の範囲の決め方はWordPress開発のページに書いています。

納品物に含まれているべきもの

以下はすべて、着手の前に書面で求めてください。どれも含めるのは安く、あとから足すのは高くつきます。まずソース。自分が所有するリポジトリに、履歴を保ったまま置かれていること。最終日にメールで送られてくるzipファイルではありません。次に、業務ルールに対する単体テストと、データベースに書き込むものや外部のサービスを呼ぶものに対する結合テスト。これがあるからこそ、二年後にその場にいなかった誰かが安心して手を入れられます。

そしてreadme。プラグインが何をするか、何にフックするか、何をどこに保存するか、どの外部サービスを呼び、それぞれが失敗したときに何が起きるか。二ページもあれば十分で、これがないことこそ、プラグインが保守されずに置き換えられる理由です。オプション、テーブル、cronのイベント、メタを削除するuninstall.php。そして、コアの各リリースに対する試験と、その試験で見つかったものの修正を含む、名前の付いたサポートの取り決めです。

作ってもらうには

Mecanikは上の三つの規模すべてでプラグインを作り、他社が書いたプラグインの引き継ぎも行っています。多くの場合、そちらのほうが役に立つ仕事です。範囲の決め方と納品の仕方はWordPress開発者ソフトウェア開発のページで説明しています。すでに誰も触りたがらないプラグインを抱えているなら、上の慣行に照らした監査は一日ほどで終わり、それが直せるものなのか、作り直すものなのかを教えてくれます。



よくある質問

カスタムの機能はプラグインとテーマのどちらに置くべきですか? 純粋に見た目のものでない限り、プラグインです。テーマは次のリニューアルで入れ替えられ、そのテーマがやっていたことはすべて止まります。カスタム投稿タイプは管理画面を失い、ショートコードは生の文字列として表示され、外部との連携は黙って動かなくなります。リニューアルのあとも成り立っていなければならないものは、すべてプラグインに属します。

英国でカスタムWordPressプラグインを作るといくらかかりますか? 小さなユーティリティ系のプラグインはおおむね£1,500から£3,000、サードパーティのAPIと管理画面を伴う中規模の連携は£3,000から£15,000、独自テーブルと更新の仕組みを持つ本格的な製品としてのプラグインは£20,000から£75,000かそれ以上です。この仕事をこなせる開発者の請求は、おおよそ一日£400から£600になります。

WordPressのコアや他のプラグインのファイルを編集してよい場合はありますか? ありません。その変更は次の更新で消え、エラーも出ず、たいていは何かが動かなくなるまで誰も気づきません。代わりにアクションとフィルターを使ってください。必要なフックが存在しないなら、ふるまいを包むか、バージョン管理の下でプラグインをフォークするか、上流にフックの追加を頼んでください。

お金を払って作ってもらったプラグインは自分のものになりますか? 自分の複製と、契約に書かれたものは自分のものです。GPLはソースと、それを改修する権利と、他の誰にでも保守を頼む権利を与えますが、公開を義務づけないので、一社のために作られたプラグインは非公開のままでいられます。開発者が同じ成果物を売り直すことは止められないので、独占が重要なら契約に入れてください。

WordPressの更新でプラグインが壊れるのをどう防ぎますか? そのバージョンが公開される前に、リリース候補版をステージングの複製で必ず試し、WP_DEBUGを有効にしたステージングを動かして非推奨の通知を早めに拾い、対応するPHPとWordPressのバージョンをヘッダーに宣言してください。WordPressはPHP 7.4を下限として要求し、8.3以降を推奨しています。