PHPの基礎 - 例外処理

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

例外処理は、プログラムの実行中に予期せぬエラーが発生した場合、通常の処理フローを中断して特別な対応を行うための仕組みである。
PHPでは、エラーが発生した箇所から即座に処理を中断して、適切なエラー処理ルーチンにジャンプすることができる。

tryブロックではエラーが発生する可能性のあるコードを配置して、catchブロックでは発生した例外を受け取って適切な処理を行う。
オプションでfinallyブロックを追加することができ、例外の発生有無に関わらず必ず実行する処理を記述する。

例外を投げる場合は、throwキーワードを使用する。

PHPには、状況に応じた様々な例外クラスが用意されている。

  • LogicException
    プログラムのロジックに関するエラー
  • RuntimeException
    実行時のエラー


独自の例外クラスを定義することにより、アプリケーション固有のエラー状況をより適切に表現できる。
例えば、データベース関連のエラーにはDatabaseException、ファイル操作のエラーにはFileOperationException等、目的に応じた例外クラスを定義できる。

例外処理では、より具体的な例外から順に処理を記述して、最後に一般的な例外を捕捉するという階層的な方法が推奨される。
これにより、エラーの種類に応じて適切な対応が可能になる。

捕捉した例外オブジェクトからは、エラーメッセージ、エラーコード、発生場所 (ファイル名と行番号)、スタックトレース等の重要な情報を取得できる。
これらの情報は、デバッグやエラーログの記録に活用される。

重大な例外が発生した場合は、システムのログにその内容を記録することが推奨される。
ただし、セキュリティ上の観点から、エンドユーザに表示するエラーメッセージには詳細な技術情報を含めないよう注意が必要である。

開発時には、予期できる例外は明示的にキャッチして、適切な対応を実装することが重要である。
また、例外を投げ直すことで、より上位の層で統一的なエラー処理を行うことも可能である。

※注意
例外処理は、プログラムの堅牢性を高めるための重要な機能であるが、過度な使用は避けるべきである。
通常の制御フローとして扱えるケースでは、条件分岐等の一般的な制御構文を使用することが推奨される。


基本的な例外処理

PHPでは、try-catchブロックを使用して例外を処理する。

例外オブジェクトには役立つメソッドが用意されており、
getMessageメソッドでエラーメッセージ、getCodeメソッドでエラーコード、getFileメソッドおよびgetLineメソッドでエラーが発生したファイルと行番号を取得できる。

 try {
    // 例外が発生する可能性のある処理
    throw new Exception("エラーが発生");
 }
 catch (Exception $e) {
    // 例外を処理する処理
    echo $e->getMessage();
 }
 finally {
    // 必ず実行される処理
 }


以下の例では、複数の例外をキャッチしている。

 try {
    // 危険な処理
 }
 catch (DatabaseException $e) {
    // データベース関連の例外処理
 }
 catch (FileNotFoundException $e) {
    // ファイル関連の例外処理
 }
 catch (Exception $e) {
    // その他の例外処理
 }


以下の例では、PDOの例外をキャッチして、ログに記録した後、より適切なカスタム例外を投げ直している。
これにより、システム全体で一貫した例外処理が可能になる。

 try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "password");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
    $stmt->execute(['id' => $userId]);
 }
 catch (PDOException $e) {
    error_log("データベースエラー: " . $e->getMessage());
    throw new DatabaseException("データベース接続に失敗");
 }



カスタム例外クラス

独自の例外クラスを定義することにより、より具体的なエラー処理が可能になる。

 class DatabaseException extends Exception
 {
    public function __construct($message, $code = 0) {
       parent::__construct($message, $code);
    }
 }



バックスラッシュの有無

  • バックスラッシュなし(Exception)の場合
    まず、現在の名前空間内でExceptionクラスを探す。
    グローバル名前空間のExceptionクラスにフォールバックする。
  • バックスラッシュあり(\Exception)の場合
    直接グローバル名前空間のExceptionクラスを参照する。
    より明示的で安全な記述である。

    特に名前空間を使用している場合は、バックスラッシュを付けることを推奨する。
    • 意図しない名前解決の衝突を防げる。
    • コードの意図が明確になる。
    • 実行速度がわずかに速くなる。(名前空間の解決プロセスをスキップできるため)


 try {
    // 処理
 }
 catch (Exception $e) {
    // エラー処理
 }


 try {
    // 処理
 }
 catch (\Exception $e) {
    // エラー処理
 }


以下の例のような場合、バックスラッシュを付けることにより、確実にPHPの標準Exceptionクラスを参照することができる。

 namespace MyApp\Controllers;
 
 try {
    // 処理
 }
 catch (\Exception $e) {  // グローバル名前空間のExceptionを明示的に指定
    // エラー処理
 }



コンストラクタでの例外処理

PHPでは、コンストラクタでも例外処理が可能である。

ただし、以下に示すような注意がある。

  • コンストラクタで例外が発生した場合、オブジェクトは正しく初期化されない。
  • デストラクタは呼ばれない可能性がある。
  • 例外が発生した場合、リソースの適切な開放が必要である。


そのため、上記を考慮した安全な実装が必要となる。

  • 段階的な初期化
    コンストラクタは単純にして、初期化処理は別メソッドに委譲する。
    各初期化ステップを明確に分離する。
  • 適切な例外処理
    具体的な例外クラスを使用する。
    例外の連鎖による詳細なエラー情報を保持する。
    カスタム例外メッセージによる明確なエラー
  • リソース管理
    クリーンアップ処理を実装する。
    デストラクタでのリソース解放


 class DatabaseConnection
 {
    private ?PDO $pdo = null;
    private array $config = [];
    private string $iniPath;
    private string $section;
 
    /**
     * DatabaseConnectionの初期化
     * 
     * @param string $iniPath 設定ファイルのパス
     * @param string|null $section 使用するセクション名
     * @throws RuntimeException 設定ファイルの読み込みに失敗した場合
     * @throws InvalidArgumentException 無効なセクションが指定された場合
     */
    public function __construct(string $iniPath = 'sample.ini', ?string $section = null)
    {
       $this->iniPath = $iniPath;
       $this->section = $section ?? getenv('APP_ENV') ?: 'development';
 
       // 初期化処理を別メソッドに委譲
       $this->initialize();
    }
 
    // 接続の初期化処理
    private function initialize(): void
    {
       try {
          $this->loadConfiguration();
          $this->establishConnection();
       }
       catch (Exception $e) {
          // 初期化中に発生した例外を適切にラップして再スロー
          $this->cleanup();
          throw new RuntimeException("データベース接続の初期化に失敗: " . $e->getMessage(), 0, $e);
       }
    }
 
    // 設定ファイルの読み込み
    private function loadConfiguration(): void
    {
       if (!file_exists($this->iniPath)) throw new RuntimeException("設定ファイル {$this->iniPath} が存在しない");
       $this->config = parse_ini_file($this->iniPath, true);
       if ($this->config === false) throw new RuntimeException('設定ファイルの読み込みに失敗');
 
       if (!isset($this->config[$this->section])) {
          throw new InvalidArgumentException("指定されたセクション '{$this->section}' が存在しない");
       }
    }
 
    // データベース接続の確立
    private function establishConnection(): void
    {
       $sectionConfig = $this->config[$this->section];
 
       // 必須パラメータのチェック
       $requiredParams = ['host', 'dbname'];
       foreach ($requiredParams as $param) {
          if (empty($sectionConfig[$param])) {
             throw new InvalidArgumentException("必須パラメータ {$param} が設定されていない");
          }
       }
 
       $host = $sectionConfig['host'];
       $dbname = $sectionConfig['dbname'];
       $username = $sectionConfig['username'] ?? 'root';
       $password = $sectionConfig['password'] ?? '';
       $charset = $sectionConfig['charset'] ?? 'utf8mb4';
 
       $dsn = "mysql:host={$host};dbname={$dbname};charset={$charset}";
 
       $options = [
          PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
          PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
          PDO::ATTR_EMULATE_PREPARES => false,
       ];
 
       try {
          $this->pdo = new PDO($dsn, $username, $password, $options);
       }
       catch (PDOException $e) {
          throw new RuntimeException("データベース接続に失敗: " . $e->getMessage(), 0, $e);
       }
    }
 
    // リソースのクリーンアップ
    private function cleanup(): void
    {
       $this->pdo = null;
       $this->config = [];
    }
 
    // デストラクタ
    public function __destruct()
    {
       $this->cleanup();
    }
 
    // PDO接続を取得
    public function getConnection(): PDO
    {
       if ($this->pdo === null) throw new RuntimeException('データベース接続が初期化されていない');
       return $this->pdo;
    }
 
    // 接続テスト
    public function testConnection(): bool
    {
       try {
          $this->getConnection()->query('SELECT 1');
          return true;
       }
       catch (Exception $e) {
          return false;
       }
    }
 }


 // 使用例
 
 try {
    // データベース接続のインスタンスを作成
    $db = new DatabaseConnection();
 
    // 接続テスト
    if ($db->testConnection()) {
       echo "データベースへの接続に成功";
 
       // データベース操作の例
       $pdo = $db->getConnection();
       $stmt = $pdo->query('SELECT version()');
       $version = $stmt->fetchColumn();
       echo "MySQL バージョン: " . $version;
    }
 }
 catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
    error_log($e->getMessage());
 }
 ?>