smallPictのWordPressプラグインは、アップロードされた画像をWebPとAVIFに変換して訪問者に配信します。リリースにタグを付けると、GitHub ActionsがそれをWordPress.orgのSVNリポジトリにコミットし、そこからプラグインを導入しているすべてのサイトに届きます。自動更新を有効にしているサイトでは、誰も変更内容を確認しないままインストールされます。プラグインはRequires PHP: 7.4とRequires at least: 5.9を宣言しているため、1つのビルドが、いまだにPHP 7.4の共有ホスティングでも、PHP 8.5のコンテナでも動作しなければなりません。
プラグインのfatal errorはwp-adminを丸ごと停止させることがあります。設定画面でエスケープされていない値が1つあれば、それは全サイトに同時に存在するXSSバグです。nonceチェックが1つ抜けていれば、管理者操作がCSRFの標的になります。いずれもこっそり修正することはできません。修正には次のリリースが必要で、それが届くまで壊れたバージョンを使い続けるサイトも残ります。
この記事では、私たちのWordPressプラグインCI/CDパイプラインを説明します。7つのPHPバージョンで実行するWordPress Coding Standards(WPCS)、PHPStan、自動テスト、そしてWordPress.orgの手前に置いたデプロイ前ゲートです。以下の内容はすべて、リリース1.2.9時点のsmallpict-wpリポジトリに基づいています。
要点
masterへのすべてのpull requestで、WPCS、PHPStan、9本の自動テストスクリプトを、GitHub Actions上でPHP 7.4、8.0、8.1、8.2、8.3、8.4、8.5に対して並列実行します。- WPCSは入力のサニタイズ、出力のエスケープ、nonce、プリペアドクエリを強制します。PHPStanはレベル5で、WordPressのstubをもとに型エラーと到達しない分岐を、WordPressを実行せずに検出します。
- リリースタグを打つと7つのPHPバージョンすべてで再度WPCSが実行され、WordPress.orgのSVNにコミットするジョブは7つすべてが成功した後にのみ開始します。
WordPressプラグインCI/CDパイプラインの全体像
プラグインは2つのGitHub Actionsワークフローで守られています。
| ワークフロー | トリガー | ジョブ | チェック内容 |
|---|---|---|---|
phpcs.yml(PHP Code Quality & Standards) | masterまたはdevelopへのpush、masterへのpull request | PHPバージョンごとに1ジョブ:7.4、8.0、8.1、8.2、8.3、8.4、8.5 | WPCS、PHPStan、自動テスト |
deploy-all.yml(Deploy to WordPress.org) | v*タグのpush | 7つのPHPバージョンそれぞれでWPCS、その後deploy | WPCS、その後ビルドとSVNコミット |
PHPバージョンの対応範囲について: この記事のスクリーンショットは、PHP 7.4〜8.3を対象としていた以前のCI実行のものです。現在、GitHub Actionsのテストmatrixは対象をPHP 7.4〜PHP 8.5まで広げており、レガシー環境でのリグレッションをゼロに保ちながら、最新のPHPランタイムとの前方互換性を継続的に確保しています。
ツールはすべてComposerのdev依存関係なので、ユーザーに配布されるものには一切含まれません。
"require-dev": { "wp-coding-standards/wpcs": "^3.4.1", "phpcsstandards/php_codesniffer": "^3.0", "phpstan/phpstan": "^2.1", "szepeviktor/phpstan-wordpress": "^2.0 || ^1.3.5", "phpstan/extension-installer": "^1.4"}GitHub ActionsでのPHPマルチバージョンmatrixテスト
PHP 7.4とPHP 8.5で何が変わるか
PHP 7.4とPHP 8.5では、同じプラグインのソースでも動作が異なります。WordPressプラグインのコードに影響する変更の一部を挙げます。
- PHP 8.0では、多くの内部関数が誤った型の引数に対して
TypeErrorをスローするようになりました。7.4では警告とともにnullを返していました。strlen()に配列を渡すコードは、7.4では動き続け、8.0では停止します。 - PHP 8.0では、数値と非数値文字列の比較方法が変わりました。
0 == "abc"は7.4ではtrue、8.0ではfalseとなり、オプション値に対する一部の緩い比較の結果が何の兆候もなく反転します。 - PHP 8.0では
str_starts_with()と関連関数が追加されました。私たちの画像処理クラスはstr_starts_with()を呼び出しています。PHP 7.4でこれが動くのは、WordPress 5.9以降にpolyfillが含まれているからで、それはRequires at least: 5.9によって保証されています。 - PHP 8.1では、内部関数のnullableでない引数に
nullを渡すことが非推奨になりました。strtolower( wp_parse_url( $url, PHP_URL_HOST ) )は7.4では問題ありませんが、$urlが相対URLでホストがnullになる場合、8.1では非推奨の通知がログに記録されます。ホスティングによっては非推奨の通知がページにそのまま出力されます。 - PHP 8.2では動的プロパティが非推奨になりました。自分で書いたクラスの未宣言プロパティに代入すると、リクエストのたびに非推奨の通知が記録されます。
いずれも、挙動の異なるPHPバージョンでコードを解析または実行して初めて表面化します。そのため、パイプラインはmasterへのすべてのpull requestを7つのバージョンで同時に実行します。
GitHub Actionsのmatrix

図1:1.2.0の作業をmasterにマージした際のPHP Code Quality & Standardsワークフロー。PHPバージョンごとに独立したジョブになっており、PHP 8.3のジョブには各ジョブが実行するステップ(PHPCS、PHPStan、自動テスト)が表示されています。
PHPバージョンの対応範囲について: 上のスクリーンショットは、PHP 7.4〜8.3を対象としていた以前のCI実行のものです。現在、GitHub Actionsのテストmatrixは対象をPHP 7.4〜PHP 8.5まで広げており、レガシー環境でのリグレッションをゼロに保ちながら、最新のPHPランタイムとの前方互換性を継続的に確保しています。
jobs: code-quality: name: WPCS & PHPStan (PHP ${{ matrix.php-version }}) runs-on: ubuntu-26.04 strategy: matrix: php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer:v2, cs2pr ini-values: memory_limit=2G coverage: none - run: composer install --prefer-dist --no-progress --no-interaction - name: Run PHPCS (WordPress Coding Standards) run: vendor/bin/phpcs -q --standard=phpcs.xml.dist --report=checkstyle | cs2pr - name: Run PHPStan (WordPress Static Analysis) run: vendor/bin/phpstan analyse --memory-limit=2G - name: Run the standalone tests run: | for t in tests/test_*.php; do echo "== $t" php "$t" || exit 1 donefail-fastはデフォルトのtrueのままなので、1つのバージョンが失敗すると、GitHubは実行中の残りのジョブをキャンセルします。壊れたpull requestのコストは小さく済みますが、その代わり、同じ実行の中ですべてのバージョンの結果を確認することはできません。
matrixで証明できること、できないこと
3つのチェックがmatrixから得る価値は同じではありません。PHPStanは実行中のPHPバージョンを前提にコードを解析し、テストはそのバージョンでコードを実行するため、どちらもジョブごとに異なる結果を返す可能性があります。PHPCSはトークン単位で動作し、実際には7つのバージョンで同じ結果を返します。matrixの中でPHPCSが証明しているのは、主にツールチェーンが各バージョンでインストールでき、動作するということです。PHPのバージョン範囲に対して構文と関数を一度にチェックするのはPHPCompatibilityの役割ですが、私たちのルールセットにはまだ含まれていません。
WordPress Coding Standards(WPCS)の自動化
プラグインにとって重要なセキュリティsniff
WPCSは、WordPress向けに書かれたPHP_CodeSnifferのsniff群です。プラグインのセキュリティにとって最も重要なルールは、WordPressルールセット自体に含まれています。
WordPress.Security.ValidatedSanitizedInputは、unslashとサニタイズを経ていない$_POST、$_GET、$_REQUEST、$_COOKIEの読み取りをすべて検出します。WordPress.Security.EscapeOutputは、esc_html()、esc_attr()、esc_url()などのエスケープ関数を通していない値のechoやprintをすべて検出します。WordPress.Security.NonceVerificationは、nonceを検証せずにリクエストデータを読み取るハンドラーを検出します。WordPress.DB.PreparedSQLとPreparedSQLPlaceholdersは、$wpdb->prepare()ではなく文字列連結で組み立てたクエリと、引数と一致しないプレースホルダーを検出します。
私たちのWPCSルールセット
phpcs.xml.distはWordPress-ExtraとWordPress-Docsを追加し、翻訳関連sniffのテキストドメインを固定し、グローバルな関数、クラス、定数すべてに私たちのプレフィックスを付けることを必須にしています。これにより、同じサイト上の他のプラグインと名前が衝突することはありません。
<rule ref="WordPress"> <exclude name="Universal.Arrays.DisallowShortArraySyntax"/></rule><rule ref="WordPress-Extra"/><rule ref="WordPress-Docs"/>
<rule ref="WordPress.WP.I18n"> <properties> <property name="text_domain" type="array"> <element value="smallpict"/> </property> </properties></rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"> <properties> <property name="prefixes" type="array"> <element value="smallpict"/> <element value="SmallPict"/> <element value="SMALLPICT"/> </property> </properties></rule>WPCSを通過するAJAXハンドラー
プラグインのこのハンドラーは、オフロードしたメディアをバッチ単位で復元します。入力の境界はすべて冒頭の数行で処理しています。
public function ajax_restore_offloaded() { if ( ! check_ajax_referer( 'smallpict_restore_offloaded', 'nonce', false ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'forbidden' ), 403 ); } global $wpdb; $after = isset( $_POST['after'] ) ? absint( wp_unslash( $_POST['after'] ) ) : 0; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin action, the rows change on every call. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s AND post_id > %d ORDER BY post_id ASC LIMIT %d", '_smallpict_offloaded', '1', $after, self::RESTORE_BATCH ) ); // ...}nonceとcapabilityは一緒にチェックします。有効なnonceが証明するのは、リクエストが私たちの管理画面から送られたことであって、そのユーザーが操作を実行してよいことではないからです。$_POST['after']はクエリに届く前にunslashされ、absint()で整数に変換されます。値がすでに整数であっても、クエリは$wpdb->prepare()を通します。
抑制にはsniff名と理由を必須にする
上のphpcs:ignoreの行は意図的なものです。WPCSはオブジェクトキャッシュを使わない直接のデータベースクエリに警告を出します。フロントエンドでの読み取りには妥当な助言ですが、呼び出しのたびに行が変わる管理者操作には当てはまりません。私たちのルールでは、ignoreには必ず正確なsniff名を書き、--の後に理由を記します。理由のない// phpcs:ignoreはレビューを通りません。
CIでは、PHPCSがcheckstyleレポートを出力し、cs2prがそれをpull requestのdiff上のアノテーションに変換します。レビュアーは各違反をログの中ではなく、原因となった行の上で確認できます。
PHPStan静的解析でロジックの欠陥を見つける
WPCSはコードを1行ずつ読みます。echoにエスケープ呼び出しがないことはわかりますが、変数がどの型を保持しているか、ある分岐が実行されうるかどうかはわかりません。PHPStanは各関数を流れる型のモデルを構築し、それと矛盾するコードを報告します。存在しないメソッドの呼び出し、stringが必要な箇所へのstring|falseの受け渡し、決して等しくならない値の比較、常にtrueまたは常にfalseになる条件などです。
WordPressを実行せずにPHPStanをWordPressで使う
szepeviktor/phpstan-wordpressは、WordPressコアの関数とクラスのシグネチャをstubファイルとしてPHPStanに提供し、WordPress固有の型拡張もいくつか追加します。phpstan/extension-installerがこれを自動的に登録します。PHPStanはこれらの宣言を読むだけで、WordPressやデータベースを読み込むことはありません。
私たちのPHPStan設定
parameters: level: 5 paths: - smallpict.php - includes/ - admin/ treatPhpDocTypesAsCertain: false bootstrapFiles: - phpstan-bootstrap.phpレベル5では、下位レベルで行う未知のクラス、メソッド、プロパティ、戻り値の型、デッドコードのチェックに加えて、関数やメソッドに渡すすべての引数の型をチェックします。treatPhpDocTypesAsCertain: falseは、私たち自身が書いた@paramのdocblockを確定情報として扱わないようPHPStanに指示します。WordPressはフィルターが返したものをそのまま渡すため、intと記述したパラメーターでも、他のプラグインから文字列として届くことがあります。私たちは、そのケースを処理する防御的なチェックをPHPStanに残してほしいのです。bootstrapファイルでは、通常は実行時に設定されるプラグインの定数(SMALLPICT_PATH、SMALLPICT_URLなど)を定義しています。
![GitHub ActionsでのWordPress向けPHPStan静的解析ログ:WPCSステップは問題なし、PHPStanはプラグインファイル11件中11件を解析し「[OK] No errors」](/blog-assets/02_phpcs_phpstan_log.png)
図2:PHP 8.3ジョブのPHPCSとPHPStanのステップ。PHPCSは報告すべきものがなければ何も出力しません。PHPStanは11個のプラグインファイル(メインのプラグインファイルとincludes/内の10クラス)を解析し、エラーは報告されていません。
実際のPHPStanの検出例:画像処理クラスの626行目
2026年10月2日、パイプラインがPHPStanのエラー1件で失敗しました。

図3:PHP 8.2のジョブが、includes/class-smallpict-image-processor.phpの626行目でbooleanAnd.rightAlwaysTrueを検出し、PHPStanのステップで停止しました。当時、解析対象のパスにあるプラグインのファイルは9個でした。
このコードは変換処理の最後のステップです。プラグインの出力形式の設定がWebPまたはAVIFの場合、変換後のファイルが添付ファイルとなり、ディスク容量を節約するため元のアップロードは削除されます。それ以外の選択肢である「AVIF (WebP をバックアップに使用)」と「元の形式を維持」では、元の JPEG・PNG ファイルが残ります。削除の分岐は次のとおりでした。
// C. Pure 1-to-1 Mode: Delete raw uncompressed source file to free 100% hosting space.if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) { wp_delete_file( $main_file_path );}PHPStanの指摘は、この時点でfile_exists( $main_file_path )が常にtrueになるというものでした。その理由は150行前、同じメソッドの冒頭にあります。
$main_file_path = get_attached_file( $attachment_id );if ( ! $main_file_path || ! file_exists( $main_file_path ) ) { return new WP_Error( 'file_missing', 'File not found on disk.' );}このメソッドはファイルが存在する場合にのみ先へ進み、PHPStanはその事実を626行目まで引き継ぎます。
私たちはこの指摘をそのまま受け入れたわけではありません。file_exists()はファイルシステムに問い合わせるため、2回の呼び出しの間に結果が変わる可能性があります。PHPStanはデフォルトでこうした呼び出しの結果を記憶します(rememberPossiblyImpureFunctionValuesオプション)。その間にディスクを変更するものは何もない、という前提です。チェックを削除する前に、コードを読んで次の2点を確認しました。
- ガードから626行目までの間に、元ファイルを削除または移動する処理はありません。変換処理は各出力を一時ファイルに書き込んでから新しいパスに移動し、そのパスに対する
wp_delete_file()の呼び出しは一時ファイルを削除するものだけです。ローカルファイルを実際に削除するオフロード処理は、626行目より後で実行されます。 wp_delete_file()は、いずれにせよファイルが存在しなくても問題を起こしません。パスをwp_delete_fileフィルターに通してから@unlink()を呼び出すため、ファイルがすでに消えていても害はありません。
両方を確認できた時点で、この条件はデッドコードでした。削除の分岐にあるデッドコードは、メソッドの途中でファイルが消える可能性があると次の読み手に誤解させます。修正ではこの条件を削除しました。
-if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {+if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path ) { wp_delete_file( $main_file_path ); }この検出はクラッシュではありませんでした。価値があったのは、このメソッドについて明らかになった点です。ユーザーのファイルを削除する唯一の分岐で、コードの前提がその構造からずれていました。私たちが得た教訓は「解析ツールを信頼せよ」よりも限定的なものです。ファイルシステム、ネットワーク、時刻を扱う関数について常にtrueまたは常にfalseと報告された場合は、条件を削除する前に、2回の呼び出しの間で何が起きるかを手作業で確認する必要があります。
PHPUnitを使わないWordPressプラグインの自動テスト
各matrixジョブの最後のステップでは、tests/内のファイルを実行します。これらはPHPUnitのテストではありません。各ファイルは素のPHPスクリプトで、必要なWordPress関数(get_option()、update_option()、sanitize_key()など)の最小限のstubを定義し、テスト対象のプラグインクラスを読み込んで実行し、最初の失敗で0以外の終了コードを返します。
for t in tests/test_.php do echo "== $t" php "$t" exit 1done現在、このようなスクリプトが9本あります。対象は、ミスが顧客のデータや金銭の損失につながる処理です。WebPとAVIFの変換パイプラインとそのフォールバック、画像配信、クラウドストレージへのファイルのオフロードと削除、オフロードしたファイルのバッチ復元、アカウントの支払いが止まった際の挙動、ストレージ容量の上限、アンインストールです。アンインストールのテストでは、実際のアンインストール用クエリをインメモリのSQLiteデータベースに対して実行し、何が残るかを確認します。
check( array( '_smallpict_cdn_base', '_smallpict_cdn_key', '_smallpict_offloaded', '_smallpictx', '_wp_attached_file' ) === $meta, 'offload markers and foreign meta remain (got ' . implode( ',', $meta ) . ')');このアサーションは、うっかり覆されやすい設計判断を守っています。プラグインをアンインストールすると設定は削除されますが、どのファイルがクラウドストレージにしか存在しないかを記録したpost metaは残します。これにより、プラグインを再インストールしたサイトでも画像を見つけられます。不用意なDELETE ... LIKE '_smallpict%'はこのマーカーを消してしまいますが、そうなればこのテストが失敗します。
WordPressのPHPUnitテストスイートではなく素のスクリプトを選んだのは、速度とセットアップのコストのためです。データベースサーバーもWordPressのチェックアウトも不要で、9本すべてが1秒未満で終わるため、7つのPHPバージョンで実行してもpull requestにかかる時間はほとんど増えません。トレードオフとして、stubは私たちが書き込んだ内容しか再現しません。メディアライブラリ自身のメタデータ処理のように、実際のWordPressコアに依存する挙動はこれらのテストではカバーされません。
WordPress.orgへのデプロイ前ゲート
リリースは別のワークフローから公開されます。v1.2.9のようなタグをpushするとワークフローが起動し、WordPress.orgのSVNにコミットするジョブはlint matrixへの依存関係を宣言しています。
jobs: lint: name: WPCS Check (PHP ${{ matrix.php-version }}) runs-on: ubuntu-26.04 strategy: matrix: php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - run: composer install --prefer-dist --no-progress --no-interaction - name: PHP CodeSniffer Check run: vendor/bin/phpcs --standard=phpcs.xml.dist
deploy: name: Deploy needs: lint runs-on: ubuntu-26.04 permissions: contents: read steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: '7.4' - name: Build WP.org Version run: | chmod +x build-wporg.sh ./build-wporg.sh - name: Deploy to WordPress.org uses: 10up/action-wordpress-plugin-deploy@stable with: generate-zip: false env: SVN_USERNAME: ${{ secrets.SVN_USERNAME }} SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SLUG: smallpict BUILD_DIR: build-wporg/smallpict ASSETS_DIR: .wordpress-orgneeds: lintにより、deployジョブは7つのlintジョブがすべて成功した場合にのみ開始します。いずれかのバージョンでWPCSが失敗すると、GitHubはdeployをスキップし、SVNへのコミットは行われません。ビルドステップは、サポートする最も古いバージョンであるPHP 7.4で実行します。ジョブのトークンはリポジトリへの読み取り専用アクセスしか持たず、書き込みはWordPress.orgのSVNへのものだけで、その認証情報はリポジトリのsecretとして保管しています。

図4:Deploy to WordPress.orgワークフロー。最初に5つのWPCS Checkジョブが実行され、その下のDeployジョブは5つすべてが成功した後にのみ実行されます。
このゲートは、以前のpull requestでの実行結果を信用せず、タグを付けたコミットに対してWPCSを再実行します。タグは任意のコミットを指すことができ、直接pushされたコミットや、チェックが完了しなかったコミットも含まれます。リリースワークフローには、これから公開するものが何かを知る手段がほかにありません。
CIのチェックをローカルで実行する
ComposerのスクリプトはCIと同じコマンドをラップしているため、pushする前に失敗を再現できます。
composer installcomposer lint # vendor/bin/phpcs --standard=phpcs.xml.distcomposer lint-fix # vendor/bin/phpcbf: fixes spacing and formatting sniffscomposer phpstan # vendor/bin/phpstan analysefor t in tests/test_.php do php "$t" break done特定のmatrixジョブの失敗を再現するには、そのPHPバージョンで同じコマンドを実行します。たとえばcomposer installの後、公式イメージを使います。
docker run --rm -v "$PWD":/app -w /app php:7.4-cli \ sh -c 'vendor/bin/phpstan analyse --memory-limit=2G && for t in tests/test_*.php; do php "$t" || exit 1; done'phpcbfが修正するのは、sniffが機械的に直せるものだけです。エスケープ、サニタイズ、nonceに関する指摘は、文脈に合う関数を人が判断する必要があるため、自動修正はしていません。
パイプラインがまだカバーしていないこと
対応予定の課題が3つあります。
- デプロイ前ゲートで実行しているのはWPCSだけです。PHPStanと自動テストは
masterとdevelopへのpushおよびpull requestで実行されますが、リリースタグでは実行されません。デプロイワークフローのlintジョブに両方のステップを追加すれば、この穴は塞がります。 - 私たちのルールセットにはPHPCompatibilityが含まれていません。
phpcompatibility/phpcompatibility-wpを追加し、testVersionを7.4-に設定すれば、7.4に存在しない構文や関数を1回の実行で検出できます。 - テストは実際のWordPressのインストールではなくstubを使っています。WordPress 5.9(私たちの最小要件)と最新リリースに対する統合テストのジョブがあれば、stubでは再現できないコアの挙動をカバーできます。
FAQ
WordPressプラグインにとって、PHP 7.4〜8.5のmatrixテストが重要なのはなぜですか?
WordPress.orgは同じリリースをすべてのサイトに配信し、どのPHPバージョンにインストールされるかを絞り込むのはプラグインヘッダー(Requires PHP: 7.4)だけです。7.4から8.5までの間に、PHPは内部関数が誤った引数の型を扱う方法、数値と文字列の比較方法、非推奨の通知を出すパターンを変更しました。そのため、あるバージョンで正しいコードが、別のバージョンではfatal errorになったり警告を記録したりします。単一のPHPバージョンでのCI実行が証明するのは、そのバージョンでプラグインが動くことだけです。サポートする各バージョンでPHPStanとテストを実行すれば、リリースがユーザーに届く前にこうした違いを捕捉できます。
WPCSは従来のセキュリティ監査の代わりになりますか?
なりません。WPCSがチェックするのは、パターンが現れたその行です。サニタイズせずに読み取ったリクエスト変数、エスケープのないecho、nonceチェックのないハンドラー、$wpdb->prepare()を使わないクエリなどです。ハンドラーが正しいcapabilityをチェックしているか、エスケープ関数が出力の文脈に合っているか、サーバー側のリクエストがユーザーの制御するURLに送られていないか、あるユーザーが別のユーザーのデータにアクセスできないか、といった点は判断できません。また、phpcs:ignoreコメントでsniffを黙らせることもできます。私たちはWPCSを既知のミスを防ぐための最低ラインと位置づけ、セキュリティに関わる変更は人の手でレビューしています。
PHPStanはコードを実行せずにWordPressコアの関数をどう扱うのですか?
PHPStanは解析対象のコードを一切実行しません。宣言とdocblockを読んで型のモデルを構築します。WordPressについては、szepeviktor/phpstan-wordpressがstubファイルを提供します。WordPressコアの関数とクラスのシグネチャ、引数の型、戻り値の型を、本体を含まない形で記述したものです。PHPStanはこれらのstubをWordPressそのものとして読むため、wp_delete_file()やget_attached_file()の呼び出しで正しい型を渡し、受け取っているかをチェックできます。WordPressやプラグインが実行時に定義する定数はbootstrapファイルに記述し、PHPStanは解析の前にそれを読み込みます。
プラグインはWordPress.orgでsmallPictとして公開しています。インストールと出力形式の設定については、インストールガイドと画像設定リファレンスで説明しています。