PHPの基礎 - 例外処理
概要
例外処理は、プログラムの実行中に予期せぬエラーが発生した場合、通常の処理フローを中断して特別な対応を行うための仕組みである。
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());
}
?>