PHP

Magoとは?PHPのRust製リンター・静的解析の使い方とPHPStanとの違い

Magoとは?PHPのRust製リンター・静的解析の使い方とPHPStanとの違い

Mago(マーゴ)は、PHP向けのリンター・フォーマッタ・静的解析器をひとつのバイナリにまとめたツールチェーンです。Rustで書かれており、PHPランタイムを使わずに動きます。最新版は1.48.1(2026年9月11日リリース)。この記事では実際に1.48.1を動かし、Laravel本体のソース1,700ファイルに対する所要時間、PHPStanが素通りした型エラーの実例、Laravel向けルールの中身、そして導入を見送るべき条件までを、コマンド出力そのままで示します。

まとめ|Mago 1.48.1の到達点と導入判断の要点

先に結論を並べます。Magoは「CIの待ち時間を削る」用途では現時点でもはっきり有効ですが、「エディタ上でリアルタイムに型エラーを見る」用途にはまだ使えません。

  • 速度は誇張ではありません。laravel/framework v13.32.0のsrc(1,700ファイル / 270,093行)に対し、mago analyzeが2.46秒、mago lintが0.67秒、mago format --checkが0.54秒でした。同じ対象へのPHPStan 2.2.14(level 5)は109.74秒です。
  • メモリはMagoが小さいとは言えません。analyze時の最大RSSはMagoが355MB、PHPStanが295MBでした(いずれも単一プロセスの最大値で、複数プロセスの同時使用量の合計ではありません)。公式ドキュメントもlint・formatでは単一スレッドのPHP製ツールより多く使うと明記しています。
  • PHPStanのような「レベル」の概念はありません。既定のまま走り、PHPStanがlevel 7以下で見逃す?stringの受け渡しを検出しました。
  • Laravel向けにはintegrations = ["laravel"]でルールが10本増えます(final-controllerno-request-allなど)。
  • LSP(Language Server Protocol)実装は2.0.0の予定で、1.48.1には含まれていません。エディタ拡張も「作らない」と公式FAQが明言しています。PhpStorm上での常時表示を求めるなら、いまはPHPStan系の既存プラグインを残す判断になります。

それぞれの根拠を、実行したコマンドと出力で順に示します。

Magoの実体|Rust製シングルバイナリとサブコマンドの構成

MagoはCarthage Softwareが開発するOSSで、GitHubリポジトリはcarthage-software/magoです。2024年10月26日に作成され、2026年9月16日時点でスター3,448、実装言語はRustです。ライセンスはCargo.tomlの記載どおりMIT OR Apache-2.0のデュアルで、配布tarballにもLICENSE-MITとLICENSE-APACHEの両方が入っています。配布物はPHPランタイムに依存しない単一バイナリで、tarballを展開してそのまま実行できます。

$ tar xzf mago-1.48.1-x86_64-apple-darwin.tar.gz
$ ./mago-1.48.1-x86_64-apple-darwin/mago --version
mago 1.48.1

「mago check」は存在しない|1.48.1のサブコマンド一覧

Magoを紹介する日本語情報にはmago check .というコマンドが散見されますが、1.48.1にそのサブコマンドはありません。実際に用意されているのは、ヘルプを表示するhelpを除くと次の13個です。

サブコマンド 役割
lint スタイル・ベストプラクティス違反の検出
analyze 型エラー・論理バグの静的解析
format コード整形(別名 fmt)
guard アーキテクチャ境界・命名規約の検査
cst 構文木とトークン列の表示
init mago.toml の対話生成
config マージ後の実効設定とJSON Schemaの出力
list-files 処理対象ファイルの列挙
inspect-baseline ベースラインファイルの内訳表示
extension 外部拡張の検査・検証
self-update バイナリの自己更新
generate-completions シェル補完スクリプトの生成
version バージョン表示(--versionと同じ)

lint・analyze・format・guardが実務で使う4本で、残りは補助です。構文木を見るコマンドがastではなくcstである点も、ヘルプ本文が「Concrete Syntax Tree」と書いているとおりです。コメントや空白まで保持する具象構文木を扱います。公式の拡張ドキュメントもリンタールールを「concrete-syntax-treeのノードを検査するもの」と定義しており、整形と検査が同じ木の上で動く構成です。

グローバルオプションはサブコマンドより前|終了コードは3段階

取り違えやすい仕様が2つあります。ひとつはオプションの位置です。--workspace--config--php-version--threads--colorsはグローバルオプションで、サブコマンドより前に置く必要があります。公式CLIリファレンスもmago --colors=never lintが正しくmago lint --colors=neverは誤りだと明示しています。

もうひとつは終了コードです。0が成功、1が「対応が必要な指摘あり」、2が設定ミスやI/O失敗といったツール側のエラーを表します。CIで||を使って握り潰すと、設定ファイルの書き間違いによる2番も一緒に隠れます。品質ゲートを作るなら1と2は分けて扱ってください。

インストール経路の実務的な違い|Composer版は薄いラッパー

導入方法は複数ありますが、性質がかなり違います。公式が推奨するのはシェルインストーラで、GitHub CLIがPATHにあればビルド来歴(attestation)の検証まで自動で行います。

curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/mago.sh | bash

# バージョンを固定する場合
curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/mago.sh | bash -s -- --version=1.48.1

検証を必須にする--always-verifyを付けると、ghが無い環境や署名が一致しない場合はPATHに触れる前に中断します。検証は--signer-workflowでリリースワークフローのファイルパスまで固定されているため、同一リポジトリ内の別ワークフローを起動できるトークンが漏れても検証は通りません。

Composer導入はバイナリを後からダウンロードする|CIでの落とし穴

PHPプロジェクトならcomposer require --dev "carthage-software/mago:^1.48.1"が手軽です。ただしこのパッケージはPHPコードでMagoを実装しているわけではありません。Packagist上の1.48.1はbincomposer/bin/magoを持つだけの薄いラッパーで、vendor/bin/magoを叩いた時点で対応バイナリがキャッシュに無ければ、GitHubリリースから取得してキャッシュします。

この仕様がCIで問題になります。共有ランナーではGitHubの匿名レート制限に当たって初回ダウンロードが失敗しやすく、公式ドキュメントもGITHUB_TOKENまたはGH_TOKENを渡すよう案内しています。GitHub Actionsではトークンが自動でエクスポートされないため、明示的に環境変数へ入れる必要があります。

- run: vendor/bin/mago lint
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

なお必須要件はPHP 8.1以上(1.48.1のrequireは~8.1から~8.6まで)で、依存はrevolt/event-loop1本だけです。この依存はバイナリ取得のためではなく、同梱されるPHP製の拡張SDKが非同期処理に使うものです(後述の拡張機能で触れます)。PHPのバージョン要件はPHP 8.5の変更点と新機能まとめとあわせて確認しておくと移行計画が立てやすくなります。

Homebrew・Cargo・Dockerの使い分け

Homebrewの式は公式管理ではなくコミュニティ管理で、リリースから遅れます。公式ドキュメントのコマンドはbrew install mago(タップ指定なし)で、インストール直後にmago self-updateを走らせて最新バイナリへ揃える手順が案内されています。cargo install magoとWinGetも同じ扱いです。

CIでPHPを用意したくない場合はDockerイメージが選択肢になります。scratchから作られた約26MBのイメージで、linux/amd64とlinux/arm64に対応します。ただしイメージにはPHPもComposerも入っていません。フォーマッタとリンターは問題なく動きますが、解析器はComposerの依存を読んでシンボルを解決するため、analyzeを回すならホスト側でcomposer installを済ませる構成にしてください。

Laravel本体1,700ファイルで測った所要時間|PHPStanとの実測差

速度の比較は、公開されている数字をそのまま引かず手元で測りました。計測環境はmacOS 26.6.2(x86_64・16論理CPU・16GB RAM)、PHP 8.5.8、Mago 1.48.1、PHPStan 2.2.14です。対象はlaravel/framework v13.32.0のsrc配下、1,700ファイル・270,093行。計測対象はvendor/laravel/framework/srcをパス指定で渡したもので、時間とメモリは/usr/bin/time -lが報告した値です。PHPStanは-d memory_limit=-1を付け、キャッシュが無い状態から1回走らせています。

コマンド 実行時間 最大RSS 検出件数
mago format --check 0.54秒 299MB 1,062ファイルが未整形
mago lint 0.67秒 126MB 8,809件
mago analyze 2.46秒 355MB 25,158件
phpstan analyse --level=5 109.74秒 295MB 3,468件

解析時間の比は約45倍です。PHPStan 2.2.14は同梱の既定設定でparallel.maximumNumberOfProcesses: autoとなっており、並列実行は最初から有効です。この計測でもCPU時間923秒に対して実時間109.74秒(約8.4倍の並列度)まで縮んだうえでの数字なので、「PHPStanが直列だから遅い」という説明では足りません。結果キャッシュが残った2回目の実行でも99.34秒で、桁は変わりませんでした。

ただしこの表を「Magoが45倍優秀」と読むのは誤りです。検出件数が25,158件と3,468件で大きく違うとおり、両者は同じ検査をしていません。Magoはinvalid-param-tagのようなドキュメントコメントの不整合まで既定で報告し、PHPStanはlevel 5の範囲に絞って報告しています。比較できるのは「1回まわすのに何秒かかるか」だけです。

メモリは表のとおりで、Magoが少ないという結果にはなりませんでした。ただしこの数値はtimeが報告する単一プロセスの最大RSSです。Magoは1プロセス内のスレッド並列、PHPStanは複数プロセスでの並列なので、プロセス群の同時使用量まで比べるには別の測り方が要ります。公式ドキュメントはメモリの使い方を設計上の選択だと説明しています。スレッドごとのアリーナアロケータで大きな領域を先に確保して並列度を上げる方式のため、ピーク時のメモリは増える。静的解析ではPsalm比で約3.5分の1に収まるものの、lintとformatでは単一スレッドのPHP製ツールより多く使う、と明記されています。メモリ制限の厳しいコンテナでCIを回している場合は、この点を先に確認してください。

mago analyzeとPHPStanの検出範囲|レベル制が無い設計

PHPStanは0から10のレベル(2.2.14でlevel 11を指定すると設定ファイルが無いと言われて止まります)で厳しさを切り替えますが、mago analyzeにレベルはありません。--minimum-fail-level--minimum-report-levelはありますが、これは検出した指摘の深刻度(error、warning、note、help)で絞る仕組みで、検査項目の段階的な有効化ではありません。既定のまま走らせて全部出す設計です。

この差が実際の検出結果に出ます。次のコードは?stringのプロパティをstrtoupper()へ素通しで渡しています。

<?php

declare(strict_types=1);

namespace App;

final class Order
{
    public function __construct(private ?string $code) {}

    public function label(): string
    {
        return strtoupper($this->code);
    }
}

Magoは設定ファイルを置かず、対象PHPバージョンだけを指定して検出しました。ルールの有効化や厳しさの指定は一切していません。

$ mago --php-version 8.4 analyze src
 INFO Overriding PHP version with 8.4.0.
src/Order.php:13:27: error[possibly-null-argument]: Argument #1 of function `strtoupper` is possibly `null`, but parameter type `string` does not accept it.
 = Help: Add a `null` check before this call to ensure the value is not `null`.
error: found 1 issues: 1 error(s)

一方PHPStan 2.2.14は、level 0・5・6・7のいずれでも指摘ゼロでした。検出されたのはlevel 8以上です。

$ php phpstan.phar analyse src --level=7 --no-progress --error-format=raw
$ echo $?
0

$ php phpstan.phar analyse src --level=8 --no-progress --error-format=raw
/private/tmp/mago-demo/src/Order.php:13:Parameter #1 $string of function strtoupper expects string, string|null given.
$ echo $?
1

この例に限れば、Magoの既定はPHPStanのlevel 8を指定して初めて出る指摘を、設定なしで出したことになります。段階的に厳しくしながら既存コードを片付けたいチームにとっては、この「最初から全部出る」挙動が導入のハードルになります。逆に新規プロジェクトなら、レベル設計を考えずに済む分だけ設定が軽くなります。PHPStanの基本的な仕組みと役割を押さえたうえで、どちらの運用が自分たちの既存コード量に合うかで選んでください。

PHPStan・Pint・PHP-CS-Fixerとの役割の重なり

Magoは単体で複数ツールの領域を覆います。乗り換えの判断材料として、対応関係を整理します。

Magoの機能 置き換え対象 乗り換えの現実性
mago analyze PHPStan / Psalm 既存ベースラインは移行不可・再生成が必要
mago format PHP-CS-Fixer / Laravel Pint 整形規則に合わないファイルに差分が出る
mago lint PHP_CodeSniffer 既定113ルール / pedanticで190ルール
mago guard deptrac / arkitect 層依存と命名規約の検査に相当

整形については注意が要ります。実測でLaravel本体の1,700ファイル中1,062ファイルが「未整形」と判定されました。既存プロジェクトでPintから乗り換えると、既存の整形規則との差の分だけ一度に差分が出ます。差分の量はコードと設定次第で、規則が近ければ小さく収まります。整形ツールの切り替え判断はLaravel Pintのプリセット選定とCI差分チェック運用で扱った論点と同じで、差分の大きさそのものより「いつ誰がその差分を取り込むか」を先に決められるかが分かれ目です。

mago guardは、リンターや解析器とは別に依存関係と命名の規約を検査するコマンドです。ドメイン層が他層へ依存していないかを検査する境界ルール(perimeter guard)と、「App\Http\Controllers配下のクラスはすべてfinalControllerで終わる」といった命名・修飾子の規約(structural guard)の2系統に分かれ、--perimeter--structuralで片方だけ走らせることもできます。

Laravelでの設定|integrationsで増える10ルール

Magoはフレームワーク別のルールセットを持っており、mago.tomlで有効化します。Laravelプロジェクトで最初に入れる設定は次の形です。

version = "1"
php-version = "8.4"

[source]
paths = ["app"]

[linter]
integrations = ["laravel"]

この設定で有効ルールが何本増えるかを、mago lint --list-rules --jsonの差分で確認しました。上記の設定(php-version = "8.4")では110本から120本へ、次の10本が追加されます。

  • final-controllermiddleware-in-routesno-implicit-model-queryno-request-all
  • prefer-anonymous-migrationprefer-array-validation-rulesprefer-casts-method
  • prefer-dedicated-status-assertionprefer-fake-helperprefer-view-array

実際にコントローラへ当てた結果が次です。$request->all()への指摘は、入力値を丸ごと受け取る危険性を理由として説明し、only()への置き換えを促します。

$ mago lint
app/UserController.php:9:7: error[final-controller]: Controller classes should be declared as `final`.
 = Help: Add the `final` keyword to the class declaration.
app/UserController.php:13:17: warning[no-request-all]: Avoid using `$request->all()` or `Request::all()`.
 = Help: Use `$request->only([...])` to specify the inputs you need explicitly, ensuring better security and validation.
error: found 2 issues: 1 error(s), 1 warning(s)
 = 1 issues contain auto-fix suggestions

有効ルールの本数はphp-versionの設定で変わります。同じ環境で数えたところ、8.1が106本、8.3が108本、8.4が110本、8.5が113本でした。設定ファイルを置かない場合の既定は8.5.0なので113本です。増える分はDeprecationカテゴリのルールです。8.4から8.5へ上げるとdeprecated-castdeprecated-shell-execute-stringdeprecated-switch-semicolonの3本が加わります。本数が合わないときはまずmago configで実効のphp-versionを確認してください。

Laravel以外にもSymfony、CakePHP、Laminas、Spiral、Tempest、Yiiのフレームワーク、DoctrineやCycleといったORM、PHPUnit・Pest・Behat・Codeceptionといったテストツール、Drupal・Magento・WordPressのCMSが用意されています。ただし公式ドキュメントは「一部は将来のルールのためのプレースホルダ」と断っているため、有効化したら--list-rulesで本当に増えたかを確認してください。テスト側のルールを併用するならPestPHPとPHPUnitの違いと導入手順で扱ったテスト基盤の選定を先に固めておくと重複が減ります。

既存プロジェクトへの導入手順|ベースラインによる既存指摘の抑制

数千件の指摘が一度に出る既存プロジェクトでは、ベースライン機能が前提になります。現時点の指摘をファイルに記録し、以降は新規に発生したものだけを報告させる仕組みです。リンターと解析器はそれぞれ別のベースラインを持ちます。

mago lint --generate-baseline --baseline lint-baseline.toml
mago analyze --generate-baseline --baseline analysis-baseline.toml

毎回フラグを渡さずに済むよう、mago.toml側へ書いておけます。

[linter]
baseline = "lint-baseline.toml"

[analyzer]
baseline = "analysis-baseline.toml"

ベースラインには2つの形式があります。既定のlooseは(ファイル・コード・メッセージ)の組み合わせで件数を記録するため、行がずれても一致し続けます。strictは行範囲まで記録するので正確ですが、整形やコード追加で行番号が動くたびに再生成が必要です。フォーマッタを同時に導入するなら、最初はlooseで運用するのが現実的です。

mago initの端末必須という制約と設定ファイルの手書き

設定ファイルはmago initで雛形を生成できますが、このコマンドは対話式です。DockerのRUN命令やCIスクリプトのように端末が割り当たらない場所では次のように落ちます。

$ mago init < /dev/null
 Mago
 ⬩ Welcome! Let's get you set up.
ERROR Failed to interact with the user: IO error: not a terminal

自動化する場合はmago.tomlを直接コミットしてください。設定ファイルはTOMLのほかYAML・JSONでも書け、同一ディレクトリ内の優先順位はtoml、yaml、yml、jsonの順です。各リリースはJSON Schemaを公開しているため、#:schema https://mago.carthage.software/1.48.1/schema.jsonの1行を先頭に置けばエディタ側で補完と検証が効きます。Composer導入時はvendor/carthage-software/mago/schema.jsonへの相対パスを指定すると、バージョン更新時にURLを書き換える手間が消えます。

GitHub Actionsへの組み込み|3ステップに分ける理由

公式のGitHub Actionsレシピは、format・lint・analyzeを別ステップに分けます。1つのrun:にまとめると最初の失敗で後続が実行されず、他の2つの指摘が見えなくなるためです。

name: Mago Code Quality

on:
  push:
  pull_request:

jobs:
  mago:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: nhedger/setup-mago@v2
      - name: Check formatting
        run: mago format --check
      - name: Lint
        if: success() || failure()
        run: mago lint
      - name: Analyze
        if: success() || failure()
        run: mago analyze

条件のsuccess() || failure()は、ジョブがキャンセルされていない限りステップを実行するという意味です。前のステップが失敗していても走るので、3つの結果が揃います。always()との違いはキャンセル時の扱いだけで、セットアップ失敗時にはどちらも実行されます。セットアップが失敗したときだけ後続を止めたいなら、セットアップのステップにidを付けてsteps.setup.outcome == 'success'を条件に加えてください。

ただしこのままではLaravelプロジェクトのmago analyzeが正しく動きません。解析器はComposerの依存からシンボルを解決するため、actions/setup-phpでPHPを用意しcomposer install --no-interaction --prefer-distを済ませたうえでanalyzeを走らせてください。formatとlintだけならPHPもComposerも不要です。

整形チェックには--checkを使ってください。--dry-runは差分を表示するだけで常に0を返すため、品質ゲートになりません。この取り違えはCIが「常に緑」になる典型的な原因です。

出力形式の指定は原則不要です。Magoは環境変数GITHUB_ACTIONSを見て自動的に--reporting-format=githubへ切り替え、プルリクエスト画面へネイティブのアノテーションを出します。ただしこの自動判定は1.18.0以降の挙動で、1.17.0以前を使っている場合はmago lintmago analyzeへ手動でフラグを渡す必要があります。CIでの静的解析とレビュー自動化の役割分担については静的解析と動的テストの違いもあわせて確認してください。

導入を見送るべき条件|LSP未提供とエディタ拡張の不在

ここは明確に線を引きます。「公式のLSPサーバーをエディタにつないで型エラーを常時表示したい」という要件があるなら、現時点のMagoは採用しないでください。CLI側には未保存バッファを渡す--stdin-inputとファイル変更を監視する--watchがあるため、エディタ側で自前のラッパーを書く道は残っていますが、その工数は自分たちで持つことになります。

公式FAQはLSP実装を2.0.0の予定としています(1.48.1の時点では未提供です)。もともと1.0.0に入れる計画でしたが、最小構成で出すより機能が揃ってから出す方針に変えて後ろへ動かした、と説明されています。さらにVS CodeやPhpStorm向けのエディタ拡張については「作らない」と明言しており、LSP標準の実装に集中し、エディタ固有のラッパーはコミュニティに委ねる方針です。

結果として、1.48.1の時点でPhpStorm向けの公式プラグインは存在しません。JetBrainsはMagoのスポンサーに名を連ねていますが、それはプラグイン提供を意味しません。PhpStormで静的解析の結果を常時表示したいなら、PhpStormの入力支援機能とPHPStan系の既存プラグインを残したまま、MagoはCIとコミット前フックに限定して使うのが現実的な折衷案です。

もうひとつの見送り条件は、PHPStanの拡張に依存している場合です。Larastanのようなフレームワーク固有の型拡張を前提にしたコードベースでは、同等の拡張をMago側で書き直さない限り検出精度が落ちる箇所が出ます。

拡張の仕組み自体はすでに用意されています。carthage-software/magoにはPHP製の拡張SDKが同梱されており、リンタールールの追加と、サービスコンテナの戻り値型の推論やフレームワークのライフサイクルメソッドによる初期化をMagoへ教える解析器プラグインを書けます。実体はMagoが起動する外部ワーカープロセスで、言語非依存のバイナリプロトコルで通信します。ただしフォーマッタとguardには拡張APIがありません。また拡張ワーカーはサンドボックス化されておらず、ホストの権限と環境変数をそのまま持って動く点は導入前に確認が要ります。

よくある質問

Magoの読み方は何ですか?

公式FAQによると発音記号は/ˈmɑːɡoʊ/で、「マーゴ」と2音節で読みます。「ma」はママの「マ」、「go」は英語のgoと同じ音です。名前は古代カルタゴの著述家マゴに由来します。プロジェクトは当初fennec(キツネの一種)という名前でしたが、別ツールとの名称衝突で改称した経緯があります。スペイン語・イタリア語で「魔術師」を意味する語でもあり、ロゴは魔法使いの帽子をかぶったフェネックギツネです。

MagoはPHPStanを完全に置き換えられますか?

検査項目が同一ではないため、無条件の置き換えにはなりません。実測では?stringを非nullパラメータへ渡すコードをMagoは既定で検出し、PHPStanはlevel 8以上で検出しました。拡張を書く仕組み自体はPHP SDKとして同梱されていますが、Larastanのように「入れるだけで効く」既製パッケージのエコシステムはまだ育っていません。当面は、CIの速度が問題になっている場合にlintとformatをMagoへ寄せ、analyzeは両方走らせて差分を見る移行が安全です。

mago lintとmago analyzeはどう使い分けますか?

lintはスタイルとベストプラクティス、analyzeは型と論理の誤りを見ます。設定ファイルを置かない既定(php-version 8.5.0)で有効なリンタールールは113本で、--pedanticを付けると190本まで増えます。既定113本の内訳はRedundancy(26本)、Consistency(16本)、Clarity(16本)が上位で、Security(8本)やDeprecation(7本)も含まれます。日常のコミット前フックではlintだけを回し、analyzeはプッシュ時やCIに置く構成が待ち時間の面で無理がありません。

WindowsでMagoは使えますか?

使えます。リリースにはx86_64-pc-windows-msvc.zipx86_64-pc-windows-gnu.tar.gzが含まれており、公式ドキュメントはWindowsでは手動ダウンロードを推奨経路としています。パッケージマネージャ経由ならwinget install CarthageSoftware.Magoも用意されていますが、公開スケジュールがGitHubリリースより遅れるため、インストール後にmago self-updateを実行してください。

ComposerでインストールするとCIでダウンロードに失敗するのはなぜですか?

Composerパッケージは初回実行時にGitHubリリースからバイナリを取得する仕組みのため、匿名アクセスのレート制限に当たっている可能性が高いです。共有CIランナーでは頻発します。GITHUB_TOKENまたはGH_TOKENを環境変数として渡してください。GitHub Actionsではトークンが自動的にエクスポートされないため、ステップのenv:へ明示的に書く必要があります。対応バイナリがキャッシュに残っている間はネットワークアクセスが発生しません。逆にランナーが使い捨てだったりMagoのバージョンを上げたりすると、そのたびに取得が走ります。

関連記事

資料請求

RELATED POSTS 関連記事