CakePHP2でコントローラー(レイアウト)ごとにエラーページを制御する
業務案件の方ではフレームワークを使う機会が多いのですが、最近はCakePHPが割と好きで、特に指定が無い場合はcake使っています。
さて、案件によって様々でしょうが、レイアウトの異なる複数のアプリケーションを作成するケースは割と多いと思います。
例えば以下のような要件のケース
会員登録ページ (デザインは本サイトに合わせる
マイページ (専用のデザイン
それぞれのコントローラーでレイアウトを指定すれば良いのですが、SQL処理でエラーが発生した場合など、CakePHPのデフォルトの処理だと、Layouts/default.ctp、またはErrors/error500.ctpが呼ばれてしまいます。※debugの設定値で変わる?
何が悪いの?って感じですが、SQLエラーが発生した事を伝えるページは、それぞれのページデザイン上に展開したいのです。デフォルトのレイアウトをエラー専用にしてしまうって手も有るかもしれませんが、どうにもスマートじゃない感じ。また、マイページにログインしている状態で発生したならば、マイページのコンテンツはそのまま表示しておきたい訳です。
エラーハンドリングをゴニョゴニョして…と色々と試行錯誤したのですが、どうもスマートじゃなく、別な手を考えてみました。
ということで、前置き長くなりましたが以下のようにしてみました。
コントローラー
マイページを mypage/ 会員登録を registration/ として各ファイルを作っていきます。
Controller/MypageController.php
class MypageController extends AppController { public $layout = 'Mypage'; //レイアウト public $uses = array( 'mypage' ,'registration' ); public $components = array('Session'); public function index() { $all = $this->mypage->find('all', array()); if($this->request->isPost()) { $this->mypage->begin(); //start Transaction $fields = array('str'); //フィールドは同じなので共用 // mypageテーブルへインサート $listdata = array('mypage' => array( 'str' => $this->data['str'] )); $check1 = $this->mypage->save($listdata, false, $fields); // registrationテーブルへインサート $listdata = array('registration' => array( 'str' => $this->data['str'] )); $check2 = $this->registration->save($listdata, false, $fields); if( $check1 === false OR $check2 === false ){ $this->mypage->rollback(); //エラーならロールバック }else{ $this->mypage->commit(); //コミット $this->Session->setFlash('正常に保存しました'); } } } }
Controller/RegistrationController.php
class RegistrationController extends AppController { public $layout = 'Registration'; //レイアウト public $uses = array( 'mypage' ,'registration' ); public $components = array('Session'); public function index() { $all = $this->mypage->find('all', array()); if($this->request->isPost()) { $this->mypage->begin(); //start Transaction $fields = array('str'); //フィールドは同じなので共用 // mypageテーブルへインサート $listdata = array('mypage' => array( 'str' => $this->data['str'] )); $check1 = $this->mypage->save($listdata, false, $fields); // registrationテーブルへインサート $listdata = array('registration' => array( 'str' => $this->data['str'] )); $check2 = $this->registration->save($listdata, false, $fields); if( $check1 === false OR $check2 === false ){ $this->mypage->rollback(); //エラーならロールバック }else{ $this->mypage->commit(); //コミット $this->Session->setFlash('正常に保存しました'); } } } }
モデル
データベースは以下の2つを作成しました。基本的に同じものですが、registration.strにはユニークを設定し、同じ値の場合はエラーを発生させるようにしておきます。
CREATE TABLE mypage ( id int(11) NOT NULL AUTO_INCREMENT, str varchar(20) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE registration ( id int(11) NOT NULL AUTO_INCREMENT, str varchar(20) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY str (str) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Model/mypage.php
class mypage extends AppModel { public $useTable = 'mypage'; public $primaryKey = 'id'; }
Model/registration.php
class registration extends AppModel { public $useTable = 'registration'; public $primaryKey = 'id'; }
ビュー
Layouts/Mypage.ctp
<!DOCTYPE html> <html> <head> <?php echo $this->Html->charset(); ?> <title>Mypage</title> <?php echo $this->Html->meta('icon'); echo $this->Html->css('cake.generic'); echo $this->Html->script('jquery.min'); echo $this->fetch('meta'); echo $this->fetch('css'); echo $this->fetch('script'); ?> <style type="text/css"> body{ background: #800000; } </style> </head> <body> <div id="header">Mypage</div> <div id="container"> <div id="content"> <script type="text/javascript"> //指定時間後にflashMessageをslideUp jQuery(document).ready(function(){ if( jQuery("#flashMessage").html() ){ setTimeout(function(){ jQuery("#flashMessage").slideUp(); },3000); } }); </script> <?php echo $this->Session->flash(); ?> <?php echo $this->fetch('content'); ?> </div> </div> <?php echo $this->element('sql_dump'); ?> </body> </html>
Layouts/Registration.ctp
<!DOCTYPE html> <html> <head> <?php echo $this->Html->charset(); ?> <title>Registration</title> <?php echo $this->Html->meta('icon'); echo $this->Html->css('cake.generic'); echo $this->Html->script('jquery.min'); echo $this->fetch('meta'); echo $this->fetch('css'); echo $this->fetch('script'); ?> <style type="text/css"> body{ background: #556b2f; } </style> </head> <body> <div id="header">Registration</div> <div id="container"> <div id="content"> <script type="text/javascript"> //指定時間後にflashMessageをslideUp jQuery(document).ready(function(){ if( jQuery("#flashMessage").html() ){ setTimeout(function(){ jQuery("#flashMessage").slideUp(); },3000); } }); </script> <?php echo $this->Session->flash(); ?> <?php echo $this->fetch('content'); ?> </div> </div> <?php echo $this->element('sql_dump'); ?> </body> </html>
Errors/error500.ctp *他省略
switch($this->request->params['controller']){ case 'mypage': $this->layout = 'Mypage'; break; case 'registration': $this->layout = 'Registration'; break; } ?> <h2>[500] <?php echo $name; ?></h2> <p class="error"> <strong><?php echo __d('cake', 'Error'); ?>: </strong> <?php echo __d('cake', 'An Internal Error Has Occurred.'); ?> </p> <?php if (Configure::read('debug') > 0): echo $this->element('exception_stack_trace'); endif;
MypageとRegistrationはbody背景色を変え、別なレイアウトである事を明確にして有ります。で、エラーテンプレートが肝で、どのコントローラーからのエラーかを最初に判定し、それに応じて使うレイアウトを変更しています。
switch($this->request->params['controller']){ //どのコントローラーからのレスポンスか case 'mypage': $this->layout = 'Mypage'; break; case 'registration': $this->layout = 'Registration'; break; }
動作確認
http://cake.studio-key.com/mypage/
http://cake.studio-key.com/registration/
それぞれポストすると2つのテーブルにインサートしますが、registration.strはユニークなので、同じ値を続けて送信するとロールバックし、エラー画面を表示します。ちゃんとそれぞれのレイアウト上に展開されます。
逆に、指定していないコントローラーで同様にエラーを発生させるとどうなるかという実験
http://cake.studio-key.com/other/
これはerror500.ctpでレイアウトの指定が無いので、デフォルト表示となります。
こういうのってみなさん、どうやってるんでしょう?コアファイルを外に追いやり、ビューやレイアウトを個別に設置するなど、やったことは無いですが、そんな感じなのでしょうか。そもそもこんな事で悩まない?のかもしれませんね(汗
そうそう、余談ですが、トランザクション。
ここではなぁなぁな書き方をしてますが、http://d.hatena.ne.jp/takami_hiroki/20101109/p1 で書かれているように複数クエリのトランザクションを明確にすると良いと思います。トランザクションに関してはcakephp トランザクションといったキーワードで検索すれば情報をたくさん得られますが、どのサイトも割と、こんな書き方で説明しています。
if( $xxx === false ){ $this->mypage->rollback(); }else{ $this->mypage->commit(); }
これだけ見るとロールバックのとき現在表示している画面にエラーメッセージを表示して修了、って出来そうなんですがねぇ・・そもそもSQLエラーを発生させないように、フォーム側でバリデーションしたり、Cakeのバリデーションを使うなど、事前にポストされた値を精査しておけばいいのですが、何度確認しても発生しなかったものが、リリースしてみるとエラー発生・・なんてケースはままあることでして。考えられる問題は全て潰しておこう!的な。
これが正解って事でもないですし、エラーハンドリングで綺麗に制御出来るかもしれません。まぁ参考までに。