PHP 例外処理 (try...catch)
PHPにおける例外処理(Exception handling)は、スクリプトの実行中に発生する予期せぬエラーや異常な状況を管理するための、構造化されたオブジェクト指向的な手法を提供します。従来の、スクリプト実行を直接中断させたり警告を記録したりするだけのエラーレポートとは異なり、例外処理では開発者がこれらのイベントを「キャッチ(catch)」して特定のリカバリロジックを実行できるため、アプリケーションの堅牢性とユーザーエクスペリエンス(UX)を向上させることができます。try...catch ブロックは、PHPで例外処理を実装するための核となる基礎構造です。
1. try...catch ブロックの仕組み
try...catch ブロックは、例外が発生する可能性のあるコード領域を定義することで機能します。try ブロックの内部で例外が「スロー(thrown)」されると、プログラムの実行フローは即座に対応する catch ブロックへジャンプします。catch ブロックでは、処理する例外のタイプを指定し、その例外をキャッチした際に実行するコードを記述します。
try ブロックの後には、少なくとも1つの catch ブロック、または finally ブロック(後の章で詳しく説明します)が続く必要があります。
1.1 基本的な構文
基本的な構文は try キーワードで始まり、監視対象のコードを囲む波括弧 {} が続きます。try ブロックの直後に、1つ以上の catch ブロックを定義します。各 catch ブロックには、処理可能な例外タイプの型ヒンティング(Exception、RuntimeException、またはカスタム例外クラスなど)と、例外オブジェクトを受け取るための変数を指定します。
<?php
try {
// 例外が発生する可能性のあるコード
// 例:ゼロ除算の試行
$numerator = 10;
$denominator = 0;
if ($denominator === 0) {
throw new Exception("ゼロによる除算は許可されていません。");
}
$result = $numerator / $denominator;
echo "結果: " . $result;
} catch (Exception $e) {
// 例外を処理するコード
// $e 変数に例外オブジェクトが格納される
echo "エラーが発生しました: " . $e->getMessage() . "\n";
echo "エラー発生行: " . $e->getLine() . "\n";
echo "エラー発生ファイル: " . $e->getFile() . "\n";
// デバッグ時にはスタックトレースの記録が必要な場合もあります:$e->getTraceAsString()
}
echo "例外処理の終了後、スクリプトの実行を継続します。\n";
?>この例では、try ブロック内で除算を実行しようとしています。除算の前に分母がゼロかどうかをチェックし、ゼロであれば説明的なメッセージを含む新しい Exception オブジェクトを作成し、throw キーワードでスローします。これにより、制御権が即座に catch (Exception $e) ブロックに移ります。catch ブロック内では、$e オブジェクトからメッセージ、行番号、ファイル名を抽出して表示しています。try...catch 構造の実行後、スクリプトは後続の処理を続行します。
2. throw キーワード
throw キーワードは、例外を明示的に発生(スロー)させるために使用されます。例外がスローされると、コードの通常の実行フローは中断されます。その後、PHPはそのタイプの例外を処理できる、最も近くにある catch ブロックを探し始めます。一致する catch ブロックが見つからない場合、PHPスクリプトは終了し、「未キャッチの例外 (uncaught exception)」エラーが表示されます。
PHPの Throwable インターフェースを実装した任意のオブジェクトをスローできます。最も一般的に使用される例外の基底クラスは Exception です。
<?php
function processUserData($data) {
if (!is_array($data)) {
throw new InvalidArgumentException("ユーザーデータは配列である必要があります。");
}
if (empty($data['username'])) {
throw new Exception("ユーザー名は必須です。"); // デモ用として汎用的な Exception を使用
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new Exception("無効なメールアドレス形式です。");
}
// データ処理のシミュレーション
echo "ユーザー " . $data['username'] . " のデータを正常に処理しました。\n";
}
try {
// 例 1:有効なデータ
processUserData(['username' => 'john.doe', 'email' => '[email protected]']);
// 例 2:無効なメールアドレス形式
processUserData(['username' => 'jane.doe', 'email' => 'invalid-email']);
} catch (InvalidArgumentException $e) {
echo "InvalidArgumentExceptionをキャッチ: " . $e->getMessage() . "\n";
} catch (Exception $e) { // 他のすべてのタイプの Exception をキャッチ
echo "汎用的な Exceptionをキャッチ: " . $e->getMessage() . "\n";
}
echo "ユーザーデータの処理試行後、スクリプトの実行を継続します。\n";
?>このデモでは、processUserData 関数が入力バリデーションの結果に基づいて特定の例外をスローします。try...catch ブロックは異なる入力を使用してこの関数を実行します。catch ブロックの順序に注意してください。具体的な例外(InvalidArgumentException など)は、より汎用的な例外(Exception など)よりも先にキャッチする必要があります。これは非常に重要です。なぜなら Exception は多くの例外の基底クラスであるため、先に記述するとすべての例外をインターセプトしてしまい、後続の具体的なエラー処理コードが実行されなくなるからです。
3. 複数の例外タイプのキャッチ
1つの try ブロックに対して複数の catch ブロックを続けることができ、それぞれ異なるタイプの例外を処理するように設計できます。PHPは、スローされた例外のタイプと一致する(またはその親クラスである)最初に宣言された catch ブロックを実行します。
<?php
// カスタム例外クラス
class DatabaseConnectionException extends Exception {}
class QueryExecutionException extends Exception {}
function connectToDatabase($server) {
if ($server !== "localhost") {
throw new DatabaseConnectionException($server . " にあるデータベースに接続できません。");
}
echo "データベースに正常に接続しました。\n";
}
function executeQuery($query) {
if (strpos($query, "DROP TABLE") !== false) {
throw new QueryExecutionException("禁止されたクエリです: " . $query);
}
echo "クエリを実行しました: " . $query . "\n";
}
try {
// 試行 1:正常な操作
connectToDatabase("localhost");
executeQuery("SELECT * FROM users");
// 試行 2:データベース接続エラー
// connectToDatabase("remotehost");
// 試行 3:クエリ実行エラー
// executeQuery("DROP TABLE users");
} catch (DatabaseConnectionException $e) {
echo "データベースエラー: " . $e->getMessage() . "\n";
// ログを記録、管理者に通知など
} catch (QueryExecutionException $e) {
echo "クエリ入力エラー: " . $e->getMessage() . "\n";
// 機密データの漏洩を防ぐため、危険なクエリの試行を記録
} catch (Exception $e) {
echo "予期せぬエラーが発生しました: " . $e->getMessage() . "\n";
// 他のあらゆる例外に対する汎用的なバックアッププラン
}
echo "データベース操作完了後、アプリケーションの実行を継続します。\n";
?>この詳細な例では、Exception を継承した2つのカスタム例外クラス、DatabaseConnectionException と QueryExecutionException を定義しています。これにより、よりきめ細かなエラー処理が可能になります。try ブロックでデータベース操作を試行し、異なる catch ブロックが特定の問題に対応する準備を整えています。これは、さまざまなタイプの問題に対して制御された方法で分類し応答する方法を示しています。
4. 実践的なユースケース
try...catch ブロックの実装は、予期せぬ事態に優雅に対応できる堅牢なアプリケーションを構築するために不可欠です。
4.1 ファイル操作における例外処理
ファイルシステムの操作は、ファイルが存在しない、権限が不足している、ディスク容量が不足しているなど、さまざまなエラーが発生しやすい領域です。try...catch ブロックは、これらの状況を管理するための明確な手法を提供します。
<?php
function readFileContents($filePath) {
if (!file_exists($filePath)) {
throw new Exception("ファイルが見つかりません: " . $filePath);
}
if (!is_readable($filePath)) {
throw new Exception("ファイルの読み取り権限がありません: " . $filePath);
}
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new Exception("ファイルを開けませんでした: " . $filePath);
}
$contents = fread($handle, filesize($filePath));
if ($contents === false) {
throw new Exception("ファイル内容の読み取りに失敗しました: " . $filePath);
}
fclose($handle);
return $contents;
}
// テスト用の仮想ファイルを作成
file_put_contents('test_file.txt', 'これはテストコンテンツです。');
// 読み取り不可のファイルを作成(環境により動作が異なる場合があります)
// chmod('unreadable_file.txt', 0000);
try {
echo "--- 'test_file.txt' を読み込み中 ---\n";
$content = readFileContents('test_file.txt');
echo "ファイル内容: " . $content . "\n\n";
echo "--- 'non_existent_file.txt' を読み込み中 ---\n";
$content = readFileContents('non_existent_file.txt'); // ここで 'ファイルが見つかりません' をスロー
echo "ファイル内容: " . $content . "\n\n"; // この行は実行されない
} catch (Exception $e) {
echo "ファイルエラーをキャッチ: " . $e->getMessage() . "\n\n";
} finally {
// 例外が発生したかどうかにかかわらず、テストファイルをクリーンアップ
if (file_exists('test_file.txt')) unlink('test_file.txt');
}
echo "ファイル操作の試行が完了しました。\n";
?>この例では、ファイル操作を関数内にカプセル化し、異なる失敗ポイントに対して throw new Exception を使用しています。その後、try...catch ブロックがこれらの特定の状況を処理し、生のPHP警告を出すのではなく、意味のあるフィードバックを提供します。finally ブロックは、例外の有無にかかわらずクリーンアップ処理が実行されることを保証します。
4.2 カスタム例外を使用したバリデーション
カスタム例外クラスを作成することで、エラー処理のセマンティクス(意味論)が明確になり、コードの可読性とメンテナンス性が向上します。
<?php
class InvalidInputException extends Exception {}
class MissingFieldException extends InvalidInputException {}
class InvalidFormatException extends InvalidInputException {}
function registerUser($userData) {
if (!is_array($userData)) {
throw new InvalidInputException("登録データは配列である必要があります。");
}
if (empty($userData['username'])) {
throw new MissingFieldException("ユーザー名は必須項目です。");
}
if (empty($userData['password'])) {
throw new MissingFieldException("パスワードは必須項目です。");
}
if (!preg_match('/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/', $userData['password'])) {
throw new InvalidFormatException("パスワードは8文字以上で、少なくとも1つの英字と1つの数字を含む必要があります。");
}
// ユーザー作成プロセスのシミュレーション
echo "ユーザー '" . $userData['username'] . "' の登録に成功しました。\n";
}
try {
// 正常な登録
registerUser(['username' => 'dev_user', 'password' => 'SecureP@ss1']);
// ユーザー名の欠落
registerUser(['password' => 'AnotherP@ss2']);
} catch (MissingFieldException $e) {
echo "登録エラー (フィールド欠落): " . $e->getMessage() . "\n";
} catch (InvalidFormatException $e) {
echo "登録エラー (形式無効): " . $e->getMessage() . "\n";
} catch (InvalidInputException $e) {
// 上記でキャッチされなかった一般的な InvalidInputException をキャッチ
echo "登録エラー (無効な入力): " . $e->getMessage() . "\n";
} catch (Exception $e) {
// 最終的なバックアップ
echo "予期せぬ登録エラーが発生しました: " . $e->getMessage() . "\n";
}
echo "登録の試行が終了しました。\n";
?>ここでは、Exception を継承して InvalidInputException を作成し、さらにそれを継承して MissingFieldException と InvalidFormatException を作成しました。これにより例外の階層構造が構築されます。registerUser が MissingFieldException をスローすると、特定の catch (MissingFieldException $e) ブロックが実行されます。これにより、異なるタイプの入力エラーに対してきめ細かな制御が可能になります。