データベース上の制約によるエラーをキャッチして、いい感じにユーザ側に日本語メッセージとして返したい時があります。この需要は例えば、ユニークなメールアドレスが保存されるシステムにあります。より具体的には次です。
ユニークなメールアドレスが保存されるシステムのデータベースには、保存されるメールアドレスがユニークであることを確約するためのユニーク制約をつけることになります。このユニーク制約に引っかかった場合は PHP 中の PDO が SQL についての例外を投げるので、その例外をキャッチして適切にハンドリング、エラーメッセージとしてユーザに表示したくなります。
多くの場合においてこの例の様な例外のハンドリングを行わずとも、都度 SELECT 文を実行して既に保存されているメールアドレスであるか否かをバリデーションとして確認し、確認結果によって保存する、しないを決定する、といった処理でも問題なくユニークな値が保存されます。この例外処理が重要なのは次の様な保存処理のタイミングとバリデーション処理のタイミングの妙によってバリデーションが機能しないパターンです(このパターンがレアケースな原因は確率のみにありますので同時接続者数や運用期間によって問題が起きやすくなります)。
バリデーションを通ったリクエストAについての処理が進んでいる間、同時に進行している別のリクエストBによってリクエストAがバリデーションが通らない状態に変化してしまい、バリデーション通過後のユニーク制約などのデータベース上の制約によって例外が発生します。この例外は通常、単に SQL エラーして扱われます。このエラーがユニーク制約エラーならば”メールアドレスが重複しました。”の様なエラーメッセージを、そうでなければ”サーバエラー”の様なユーザにはふんわりとしたエラーメッセージを表示したくなります。
これは次の様にできます。
try { $member = new Member(); $member->email = '重複するメールアドレス'; $member->save(); } catch (\PDOException $e) { if ($e->errorInfo[1] === 1062) { // ユニーク制約によるエラー例外をキャッチした場合の処理 }else{ throw $e; } }
より実際のコードに近づけると次の様になります。
class HogeController extends AppController { public function store() { try { $member = new Member(); $member->email = '重複するメールアドレス'; $member->save(); } catch (\PDOException $e) { if ($e->errorInfo[1] === SqlErrorCode::ER_DUP_ENTRY) { // ユニーク制約によるエラー例外をキャッチした場合の処理 // ここではエラーレスポンスを返す独自の適当なメソッドに渡す return $this->makeErrorResponse($message='メールアドレスが重複しています。', $httpsStatus=HttpStatus::BAD_REQUEST); }else{ throw $e; } } return $this->makeSuccessResponse($message='メールアドレスを登録しました。'); } } class SqlErrorCode { public const ER_DUP_ENTRY = 1062; // 他にも SQL のエラーコードを列挙する }
やっていることは PHP の PDO から投げられる例外である PDOException から例外の原因である SQL のエラーのコードを取得し、特定のエラーコードの場合のみの例外処理を実行する、というものです。
PHP: PDOException – Manual
PHP: PDO::errorInfo – Manual
参照すべきエラーコードはデータベースの種類依存ですが MySQL ならば次のページが参考になります。
MySQL :: MySQL 5.6 リファレンスマニュアル :: B.3 サーバーのエラーコードおよびメッセージ
MySQL :: MySQL 8.0 Error Reference :: 2 Server Error Message Reference
この様に SQL の例外を適切にハンドリングできるとユーザに対して原因不明に見えるエラーメッセージを表示する必要が少なくなり、ユーザに優しいアプリケーションを作りやすくなります。