Article by: Richard C.
このガイドでは、PHP におけるエラーの仕組みと、それらをログ関数や Sentry を使って効率的にデバッグする方法を説明します。
このガイドの情報は PHP 8 に対して正確であり、将来の PHP バージョンの変更内容によっては、それ以降のバージョンにも適用できる可能性があります。
PHP のデバッグとロギングの前提条件
このガイドのすべてのコード例は、ホスト OS を問わず Docker 上で実行できます。Docker をお持ちでない場合は、こちらからダウンロードしてください。
すでに PHP がインストールされている場合、Docker は必須ではありません。ただし、Docker 上でコードを実行すると、以下のような利点があります。
- Docker サンドボックス内で実行される悪意あるコードから、マシンを保護できます。
- チーム内のすべてのプログラマーが、同じ IDE プラグインと PHP バージョンを使った共通の環境で作業できます。
- 物理マシンを再設定することなく、開発環境を本番サーバーと簡単に一致させることができます。
PHP の例外とエラー
まずは、PHP における例外の仕組みを見ていきましょう。
エラーとはPHP 本体またはその拡張機能内で発生する内部的な問題です。例外とはPHP 開発者(つまりあなた)が書いた外部の PHP コードで投げたり捕捉したりできるオブジェクトの一種です。エラーと例外はどちらも Throwable オブジェクトです。
PHP が生成するすべてのエラーには型が含まれます。以下の PHP エラーの種類の一覧を見てください。それぞれの型の動作と、その原因となる状況について簡単に説明されています。
エラーには主に 3 種類あります。
- Fatal(致命的エラー):スクリプトの実行が即座に停止する重大なエラー。
例:存在しないファイルを require しようとした場合。 - Warning(警告):深刻ではあるがスクリプトの実行は継続されるエラー。
例:存在しないファイルを include しようとした場合。 - Notice(注意):軽微な問題。
例:定義されていない変数へのアクセス。
例外は、多くのプログラミング言語で見られる try…catch…finally 構文を使って投げられ、捕捉されます。
未捕捉の例外を処理するには、set_error_handler 関数にエラーハンドリング用の関数を渡します。ただし、この関数はあまり役に立たない場合があります。というのも、E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARNING といった、最も深刻なエラータイプは処理できないためです。
エラーハンドラを設定しなかった場合、PHP は php.ini 内の error_reporting ディレクティブによって制御される設定に従ってエラーを処理します。デフォルトでは、すべてのエラーが(HTML 内に)出力されるようになっています。エラーレポートの仕組みは複雑で、特にデバッグ用フレームワークと組み合わせた場合には注意が必要です。また、PHP の設定における脆弱性を攻撃者が調べる手段としてもよく利用されます。
本番サイトでは、ユーザーに返す HTML ページ内にエラーを表示させたくない場合、php.ini ファイルでエラーレポートをすべて無効にすることができます。ただし、エラーレポートを完全に無効にするのではなく、error_log ハンドラを設定してすべてのエラーをファイルに記録し、php.ini 内の log_errors を有効にする方が望ましい対応です。
未捕捉の例外を処理するためには、set_exception_handler() 関数でグローバルな例外ハンドラを設定することができます。未捕捉の例外はこの関数に渡されます。
P.S. Sentry では、よくある PHP エラーとそれに対するステップごとの解決方法の一覧もこちらに公開されています。
エラーの例を作成する
簡単なスクリプトを作成して、http://localhost:8000 で閲覧できるページを用意し、エラーを発生させてみましょう。
コンピュータ上の任意のフォルダに、index.php という名前のファイルを作成し、次の内容を記述してください。
フォルダ内でターミナルを開きます。
ターミナルで、以下のコマンドを使ってスクリプトを Docker 上で実行してください。
このコマンドは、phpbox という名前のコンテナを起動し、終了時に削除されます。
コンテナは現在のフォルダへアクセスでき、ホストマシンがアクセスできるようにポート 8000 を公開し、app フォルダからファイルを提供しながら、ポート 8000 のすべての接続を受け付ける php コマンドを実行します。
これで、http://localhost:8000 にアクセスするとページの出力が確認できます。
ブラウザが HTML ページをリクエストすると、PHP 組み込みウェブサーバによって処理されます。このサーバはテスト目的で PHP スクリプトを実行しますが、マルチスレッドをサポートしておらず、本番環境での利用には適していません。本番環境では Apache や nginx を使用してください。
例外は Docker の標準出力に表示され、ログファイルに送ることも可能です。
PHP エラーを適切に処理する
現在、今回のサンプルコードはブラウザにエラーを返しています。例えば次のように表示されます。Fatal error: Uncaught Exception: boom in /app/index.php:3 Stack trace: #0 {main} thrown in /app/index.php on line 3.
このようなエラーの詳細がお客様に見えてしまうのを防ぐために、問題のあるコードを try-catch ブロックで囲んでみましょう。
これで、ブラウザにはエラーの詳細が表示されなくなります。
サーバーも標準出力ログには、一切エラーを出力しません。
PHP エラーをログに記録する
もし処理方法がわからないトップレベルのエラーに遭遇した場合、前のセクションのようにそれを無視すべきではありません。代わりに、エラーをファイルに記録して監視できるようにしましょう。
先ほど index.php に追加した try…catch のコードを削除してください。次に、以下の内容で php.ini ファイルを作成します。
行末にセミコロンが付いていないことに注意してください。nii ファイルでは、セミコロンはコメントの開始を意味し、行の終わりを示すものではありません。
Docker コンテナを停止し、以下の新しいコマンドで起動してください。このコマンドには設定ファイルへのリンクが含まれています。
これで、エラーはブラウザに送信されなくなります。
例外はサーバーのターミナルに出力されます。
例外は log.log ファイルにも記録され、後処理のために監視することができます。
PHP におけるテスト、ロギング、デバッグ、モニタリング
エラーの検出と防止に不可欠な、以下の関連する活動があります。
- テスト
- ロギング
- デバッグ
- モニタリング
テストはエラーが発生する前にそれを防ぐことを目的とし、人間またはプログラムによって、ユニットテストや統合テストを通じて行われます。テストは広範なトピックであり、このガイドでは扱いません。
ロギングとモニタリングは、既存のエラーを検出したり、アプリケーションのパフォーマンス改善の余地を見つけたりするための手段です。ロギングは、ログファイルに行を書き込むことで記録可能な履歴を作成することです。またモニタリングは現在のみを対象とし、アプリケーションの現在の状態をダッシュボードで表示したり、エラー発生時にアラートを出したりします。
デバッグは既に発生しているエラーの原因を調査し、修正することです。観測性、モニタリング、デバッグに関する当社のブログや、Sentry がロギングとどう差別化されているかに関する情報もぜひご覧ください。
PHP ロギング
前述のようにエラーをログファイルに記録することに加えて、ログメッセージを使ってエラーの原因を特定するのにも役立てることができます。PHP がテキストを出力するあらゆる方法は、ログとして利用可能です。
PHP では、PHP タグで囲まれていないテキストはそのまま出力されます。またPHP ファイルがウェブサーバーから呼び出された場合、テキストはブラウザに返され、HTML として表示されます。さらにPHP ファイルがウェブサーバーを介さずにターミナルから実行された場合は、テキストは標準出力に表示されます。
ブラウザへのログ出力
PHP にはテキストを出力する方法が多数あります。以下は、そのような出力を行う関数の例です。
上記のコードは次の結果を返します。
上記のいずれかの関数を使って、変数のテキスト値や現在のスタックトレースを好みの形式で表示することができます。
PHP スクリプトは 2 通りの方法で実行できます。
ターミナルから直接実行する方法と、Apache のようなウェブサーバーのモジュールとして実行する方法です。いずれの場合も、上記の関数はスクリプトを呼び出したアプリケーションに対してテキストを返します。スクリプトがターミナルから実行された場合、テキストはターミナルに出力されます。またApache によって実行された場合は、テキストがウェブサーバーに出力され、その後ブラウザに送信されます。
サーバーへのログ出力
通常、PHP はウェブサーバー経由で実行されるため、デバッグ情報をブラウザで顧客に返したくはありません。そのため、ローカルでデバッグ情報を記録する別の方法が必要になります。その唯一の方法は、ファイルに書き込むことですが、これはデバッグには扱いにくい方法です。そこで error_log() 関数を使用します。以下はその関数を使った例です。
このコードは次の出力を生成します。
上記の例では、オブジェクトを文字列に変換するために print_r を使用しており、その文字列がターミナルに出力されます。print_r 関数は人間が読みやすい形式で出力しますが、有効な PHP コードにはなりません。コピー&ペーストしてコード内でオブジェクトを再生成できるような、有効な PHP のオブジェクトリテラルとして文字列を書き出したい場合は、print_r の代わりに var_export を使用してください。
PHP におけるデバッグ
ロギングはエラーを検出するための手段です。変数の値を含むログ文をコードに追加することで、エラーを調査し、デバッグすることができます。しかし、ロギングは一般的に最適なデバッグ手法ではありません。
理由は以下の通りです。
- ログ文の記述と後での削除に時間がかかる
- 調査中にプログラムを繰り返し実行し、ログ文を修正しなければならない
- アプリケーションの実行を一時停止して変数の値を変更することができない
より優れた代替手段は、専用のデバッガーを使用することです。
VS Code でのデバッグ
Visual Studio Code(「VS Code」または「Code」)は、PHP のデバッグを可能にする無料の統合開発環境(IDE)です。詳細は公式の VS Code ドキュメントをご参照ください。
このセクションでは、PHP 用のデバッグ拡張である Xdebug を通じて、VS Code を PHP に接続する方法を学びます。続行する前に、VS Code をインストールしてください。
VS Code の代替となる商用 IDE である PhpStorm も Xdebug を使ったデバッグをサポートしており、このガイドで説明するのと同じ原則に従っています。
リモートデバッグと Docker Dev コンテナ
このガイドをどのオペレーティングシステムでも実行できるようにするため、VS Code を Docker コンテナ内で実行します。
すでにローカル環境に PHP と Xdebug がインストールされている場合は、サンプルをそのまま実行できます。Docker を使用したくない場合は、続行する前に Xdebug をインストールしてください。
VS Code と PHP を同じ Docker コンテナ内で実行するには、Dev Containers 拡張機能を使用します。公式ドキュメントはこちらをご覧ください。
- VS Code の拡張機能サイドバーで「Remote Development」と検索し、Dev Containers を含む拡張パックをインストールしてください。
- 作業用フォルダ内に .devcontainer という名前の新しいフォルダを作成します。
.devcontainer 内に dockerfile という名前のファイルを作成し、以下のコードを挿入してください。
この dockerfile は、標準の PHP Docker イメージに Xdebug をインストールします。
.devcontainer/devcontainer.json というファイルを作成し、以下のコードを挿入してください。
VS Code は、この JSON ファイルを使用して、dockerfile によって作成されたコンテナ内で実行される際の設定を行います。extensions プロパティは重要です。これは、VS Code が起動前にどの拡張機能をインストールすべきかを指定します(通常は VS Code のサイドバーから行う操作です)。ここでは、補完機能や Xdebug 拡張を含む PHP 拡張パックを使用しています。
設定ファイルに追加する拡張機能の名前を確認するには、対象の拡張機能を開き、設定アイコン(歯車)をクリックしてCopy Extension ID(拡張機能 ID をコピー)を選んでください。
.devcontainer フォルダを含むフォルダを VS Code で開くと、コンテナ内で再オープンするよう通知が表示されます。ボタンをクリックして確認してください。
初回のコンテナビルドが完了して開かれると、物理マシン上で作業するのと同様に VS Code を使用できます。コードフォルダは /workspaces 以下のサブフォルダに配置されます。
コンテナの初回ビルドは時間がかかりますが、ビルド完了後はローカルマシンで作業するのと同じくらい素早く VS Code が起動します。
起動構成ファイル
Docker を使用しているかどうかにかかわらず、PHP スクリプトをターミナルアプリとして、またはウェブサーバー経由で実行するには、起動構成ファイルを作成する必要があります。VS Code では、.vscode/launch.json というファイルを作成し、以下のコードを挿入してください。(これは、「実行とデバッグ」パネルのオプションから作成できるデフォルトファイルを一部変更したものです。)
このファイルにより、VS Code の実行とデバッグサイドバー、または F5 キーを使ってスクリプトを実行およびデバッグできるようになります。上記の構成では、スクリプトを実行する際に選択できる 3 つのオプションが用意されています。
- Listen for Xdebug:VS Code 内でスクリプトを実行するのではなく、指定されたポートで動作している既存の Xdebug インスタンスに接続しようとします。このチュートリアルではこのオプションは使用しません。
- Launch currently open script:現在フォーカスしているファイルを Xdebug とともに起動し、VS Code の PHP デバッグ拡張が Xdebug のポート 9003 に接続することを試みます。ポート 9003 は Xdebug のデフォルトポートです。毎回同じスクリプトを実行したい場合は、launch.json の program プロパティに特定のファイルを指定することができます。
- Launch built-in web server:上記同様にスクリプトをターミナルアプリとしてではなく、ウェブサーバーで実行します。この構成セクションでは 2 つのポートが使われます。ウェブアプリケーションが動作するポート 8000 と、Xdebug が公開されるポート 9003 です。なお、この構成では localhost の代わりに 0.0.0.0 を使用しており、Docker 内部だけでなく任意のマシンからのブラウザリクエストも受け入れられるようになっています。
次に、VS Code で index.php ファイルを開き、マージン部分をクリックしてブレークポイントを設定し、F5 を押して、Launch built-in web serverを選択してください。http://localhost:8000 にアクセスすると、VS Code がスクリプトに接続されるはずです。
これで、VS Code の標準的なデバッグツールを使用できるようになります。
- F10 キーや画面上部の再生バーのボタンを使って、コードを一行ずつステップ実行できます。
- 左側のサイドバーで、スコープ内の変数を確認したり、ウォッチしたい変数を追加したりできます。
- 画面下部のTERMINALタブでは、error_log() の出力など、デバッグ用の出力が表示されます。
- DEBUG CONSOLEタブでは、変数の値を変更したり、即興の PHP コードを実行したりできます。
- ブレークポイントを右クリックして条件式を入力することで、条件付きブレークポイントを設定できます。その条件が真のときだけ実行が一時停止されます。
このように、コードを一歩ずつ実行しながら変数の値を確認・変更していくことで、顧客が直面しているエラーを再現・修正できるはずです。
オンラインサービスによる PHP エラーのモニタリング
これまでは無料ツールを使ってきましたが、アプリケーションのモニタリング、つまりエラーの通知を受け取ったり、アプリケーションのパフォーマンスを常に確認できるダッシュボードを持つことは、より複雑になります。Prometheus や Elastic のような無料ツールをサーバー上にセットアップすることもできますが、これらは複雑で、このガイドの範囲を超えています。さらに、モニタリング用のサーバー自体がダウンした場合、問題が発生しても通知を受け取れません。その代わりに、シンプルなセットアップで常時利用可能なモニタリングサービスを提供する、有料サービスやその無料スタータープランを使うことができます。
このセクションでは、例外処理に特化したサービスである Sentry の無料プランを使ってエラーをモニタリングする方法を紹介します。以下の手順に従って、Sentry アカウントを作成し、アプリを Sentry に接続し、Sentry のウェブインターフェース上でアプリを監視しましょう。
- https://sentry.io/signup にアクセスし、アカウントを作成
- オンボーディングはスキップ
- Create project(プロジェクトを作成)をクリック
- PHPを選択
- プロジェクト名を入力
- Create Project(プロジェクトを作成)をクリック
- 次のページで表示されるセットアップ手順の中から、dsn URL の値をメモ。(この値は秘密にし、GitHub などにコミットしないでください。)
コンピュータ上の任意の空のディレクトリに dockerfile という名前のファイルを作成し、以下の内容を記述してください。PHP の Docker イメージには Composer が含まれていないため、それをインストールする必要があります。その後、Sentry プラグインと、それが依存する PHP サンプリング拡張機能である Excimer をインストールします。
- 以下の内容で index.php を作成し、あなたの dsn 値を使用してください。最初に require ‘/app/vendor/autoload.php’; という行を定義し、ライブラリからクラスを自動的に読み込めるようにします。その後、Sentry を起動し、Sentry が捕捉するための例外を投げます。
- これらのファイルと同じフォルダ内で、以下のコマンドをターミナルで実行してください。最初のコマンドは、Composer を含む PHP ベースの Docker イメージをビルドします。2 番目のコマンドは、そのイメージをもとに一時的なコンテナを実行し、Sentry をインストールして PHP のウェブサーバーを起動します。
- http://localhost:8000 にアクセスしてください。
- Sentry のウェブインターフェースに戻ると、エラーが検出されているはずです。Take me to Issues(課題一覧へ移動)をクリックしてください。
以下の画像のように、エラーに関する情報が表示されるはずです。
アプリのパフォーマンスやエラーに関するダッシュボードに加えて、Sentry はアプリでエラーが発生した際に自動的にメールで通知も行います。
PHP のエラーおよびパフォーマンス監視に関する詳細は、当社のドキュメントでご覧いただけます。
PHP におけるデバッグとロギング
このガイドでは、PHP におけるエラーの仕組みを解説し、簡単なデバッグのためにメッセージをブラウザやターミナルにログ出力する方法、Xdebug と Visual Studio Code を使って対話的にデバッグする方法、Sentry を使ってエラーモニタリング用のダッシュボードと通知システムを構築する方法を紹介しました。
これらの概念に慣れてきたら、PHP におけるアプリケーションのセキュリティ、パフォーマンス最適化、自動テストにも取り組むことをおすすめします。
Original Page: How to Debug and Log in PHP
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。