MVCのコントローラーはリクエストされたURLに応じてアクションを実行します。コントローラーは継承を前提としたクラスなので、抽象クラスとして定義します。
アプリケーションの起点と終点を作成する
ApplicationBase.phpはアプリケーション本体のスーパークラスとしますのでアプリケーションの起点となるrun()メソッドを実装します。run()メソッドはルーティングに関する処理を行い、実行すべきコントローラーとアクションを取得し、getContent()メソッドを実行します。戻り値としてレンダリングされたHTMLデータが返されるのでそれをクライアントに送信します。アプリケーションの処理はrun()メソッドで始まりrun()メソッドで終了させます。
Controllerクラス
MVCのコントローラーはControllerクラスとして定義します。冒頭でも説明したように、コントローラーは継承を前提としています。アクションでどのような処理を実行するかはアプリケーションごとに異なるので、アプリケーション側でサブクラスを作成し、そこに定義します。
Controllerクラスでは以下のメソッドを定義します。
dispatch() | コントローラーのサブクラスのアクションを実行 |
urlNotFound() | 実行するアクションが見つからない時、例外オブジェクトを生成 |
isAuth() | リクエストされたページで認証しなければならないか見る |
render() | ビューファイルをレンダリングする |
redirect() | 画面遷移するためのメソッド |
getToken() | セッション中のユーザーを認証するためのトークンを生成 |
checkToken() | トークンがサーバー側で保存されているものと一致するか |
コントローラーとアクション
MVCでは、コントローラーがアクションを呼び出し、アクションがビューを生成する流れです。
プログラム上ではコントローラーはクラスに相当し、アクションはメソッドに相当します。
1つのアクションは1つの画面に対応するのが基本です。データの入力画面や一覧表示画面を作成する時、それぞれ1つのアクションに対応させます。コントローラーはいくつかのアクションを定義したクラスです。
Controllerクラスの作成
では、Controllerクラスにプロパティ、コンストラクター、アクションを実行するためのメソッドを定義していきます。
プロパティを定義
abstract class Controller {//抽象クラス
protected $_app;//アプリケーションクラスのインスタンス
protected $_controller;//コントローラーのクラス名
protected $_action;//アクション名
protected $_request;//Requestオブジェクト
protected $_response;//Responseオブジェクト
protected $_session;//Sessionオブジェクト
protected $_connectModel;//ConnectModelオブジェクト
protected $_authentication = array();//認証がいるか(TRUE/FALSE)
const PROTOCOL = 'http://';
const ACTION = 'Action';
const FILE_NOT_FOUND = '指定された処理を実行できませんでした';
}
コンストラクターを定義
abstract class Controller {
//プロパティ省略
public function __construct($app){//アプリケーションクラスのインスタンスが渡される
//アプリケーション本体のクラスのインスタンスを格納
$this->_app = $app;
//コントローラーのクラス名を格納
$this->_controller = strtolower(substr(get_class($this), 0, -10));
//アプリケーションクラスのインスタンスの各クラスのインスタンスを取得
$this->_request = $app->getReqObj();
$this->_response = $app->getResObj();
$this->_session = $app->getSesObj();
$this->_connectModel = $app->getConnModelObj();
}
}
dispatch()メソッドを定義
アクションを実行するメソッドを定義します。このメソッドはアプリケーションのスーパークラスApplicationBaseのgetContent()メソッドから呼ばれて、アクションメソッドを実行する処理を行います。
①アクション名をプロパティに格納し、アクションメソッド名を「アクション名+Action()」として生成し、格納します。
②アクションメソッドがない場合はエラー画面へ遷移しますが、遷移の処理はurlNotFound()メソッドで行います。メソッドが存在するかどうかを確認するにはmethod_exists()関数を利用します。
③該当のアクションが認証を必要としているが認証済みでない場合AuthException例外を投げます。
④アクションメソッドを実行して戻り値のコンテンツを呼び出し元へ返します。
ではdispatch()メソッドを定義します。
public function dispatch($action, $routes = array()) {
//アクション名をプロパティに格納してアクションメソッド名を生成
$this->_action = $action;
$actionMethod = $action . self::ACTION;
//アクションメソッドがない場合はエラー画面へ遷移
if(!method_exists($this, $actionMethod)) {
$this->urlNotFound();
}
//認証を必要とするが認証済みでなければ例外を投げる
if($this->Authentication($action) && !this->session->isAuth()){
throw new AuthException();
}
//アクションメソッドを実行。戻り値のコンテンツを呼び出し元に返す
$content = $this->$actionMethod($routes);
return $content;
}
エラーオブジェクトを生成するメソッド
実行すべきコントローラーやアクションがない場合に投げられるFileNotFoundExceptionオブジェクトを生成する処理を行います。
protected function urlNotFound(){
//エラーになったコントローラーとアクションの名前を表示
throw new FileNotFoundException('FILE_NOT_FOUND'.$this->_controller.'/'. $this->_action);
}
認証済みでないとアクションにアクセスできないか判定
認証が必要なアプリケーションはControllerクラスを継承したサブクラスで定義します。なので$_authenticationプロパティにアクションが登録されていればTRUE,されていなければFALSEを返します。
protected function isAuth($action){
if($this->_authentication === true || (is_array($this->_authentication)
&& in_array($action, $this->_authenication, true))){
return true;
}
return false;
}
in_array()関数で引数に指定した変数が、配列かどうか調べ、配列ならばtrueを返します。
レンダリングするrender()メソッド
Viewクラスのオブジェクトを生成し、コンテンツをレンダリング(表示)するrender()メソッドを実行します。①dispatch()メソッドから②アクションメソッド、③render()メソッドの順に呼び出します。
①Viewクラスのインスタンス化
Viewクラスのコンストラクターには、ビューファイルが格納されているディレクトリパスとRequestオブジェクト、ベースとなるURL、Sessionオブジェクトなどいろいろな情報を格納した配列をわたします。まずはレンダリングするための情報が格納されたViewクラスのインスタンス化を行います。
$viewInfo = array(
'request' => $this->_request,
'baseUrl' => $this->_request->getBaseUrl(),
'session' => $this->_session
);
$view = new View($this->_app->getViewPass(), $viewInfo);
②ビューファイルを読み込み、コンテンツをアクションメソッドに返す
render()メソッドを呼び出す際、第2パラメーターで$viewFileを用意して指定できるようにします。
$viewFileにファイル名が渡された場合設定し、何も渡さなければアクション名を設定します。ビューファイルへのパスを作ってViewクラスのrender()メソッドを呼び出してレンダリングします。
if(!is_null($viewFile)){
$viewFile = $this->_action;
}
$viewPath = $this->_controller . '/'. $viewFile;
//$paramにアクションメソッドから渡された配列
//$templateにレイアウトファイル名
$content = $view->render($viewPath, $param, $template);
return $content;
redirect()メソッドで指定されたURLへ移動
まずプロトコル、ホスト名にパス情報を結合してURLを生成します。
protected function redirect($url){
//ホスト名を取得
$host = $this->_request->getHomeName();
//ベースURLを取得
//URLを作成して$urlに格納
$url = self::PROTOCOL . $host . $base_url . $url;
}
リダイレクトのステータスコードである302をレスポンスヘッダーに設定します。
$this->_response->setStatusCode(302, 'Found');
$this->_response->setHeader('Location', $url);
redirect()メソッドはこうなります。
protected function redirect($url){
//ホスト名を取得
$host = $this->_request->getHomeName();
//ベースURLを取得
//URLを作成して$urlに格納
$url = self::PROTOCOL . $host . $base_url . $url;
$this->_response->setStatusCode(302, 'Found');
$this->_response->setHeader('Location', $url);
}
CSRF対策としてワンタイムパスワードを生成
トークンを生成するgetToken()メソッドを作成します。
①$_SESSION変数にトークンを格納する際のキーを作成する
②$_SESSION変数からトークンを取得し、トークンの数5個を越えれば調整
③password_hash()関数で生成したパスワードハッシュをトークンとし、返す
アクションを実行するメソッドで、Controllerクラスのrender()メソッドを呼び出してHTMLのレンダリングを行いますが、この時getToken()でトークンを作成して渡します。
protected function getToken($form){
$key = 'token/'. $form;//$formは呼び出し元から渡された「コントローラー名とアクション名」
$tokens = $this->session->get($key, array());
if(count($tokens) >= 5) {//トークンの数が5を超えたら実行する
array_shift($tokens);//先頭の一番古いトークンを削除
}
$password = $form . sessionId();
$token = password_hash($password, PASSWORD_DEFAULT);//パスワードをハッシュ化
$tokens[] = $token;
$this->_session->set($key, $tokens);
return $token;
}
セッションを継続できるかcheckToken()メソッドで確認
フォームに入力されたデータの送信時に実行されるアクションに呼び出されるトークンを確認するメソッドを作成します。
①トークンのキーを作成して、$_SESSION変数からトークンを取得する
②$tokensからパラメーターのトークンを検索し、見つけた場合は削除し、今回のキー名で再セットする
入力画面を表示するアクションでは、フォームの中にトークンを埋め込んで入力内容と一緒に送信します。次に実行されるアクションはクライアントから送信されたトークンを取得し、checkToken()メソッドを呼び出してチェックします。前回と同じキーを生成し、このキーを使ってトークンが格納された配列を取得して一致するものがあるか検索します。トークンは1回限りの使用ですので見つかったら削除します。
protected function checkToken($formName, $token){
$key = 'token/' . $formName;//キーを作成
$tokens = $this->_session->get($key, $array());//トークン配列を取得
if(($currentToken = array_search($token, $tokens, true) !== false)) {//トークンがあるか
unset($tokens[$currentToken]);//使ったトークンは削除
$this->_session->set($key, $tokens);//再セット
return true;
}
return false;
}
これまで記述したコードを基にコントローラークラスを完成させます。
abstract class Controller {
protected $_app;
protected $_controller;
protected $_action;
protected $_request;
protected $_response;
protected $_session;
protected $_connectModel;
protected $_authentication = array();
const PROTOCOL = 'http://';
const ACTION = 'Action';
const FILE_NOT_FOUND = '指定された処理を実行できませんでした';
public function __construct($app){
$this->_app = $app;
$this->_controller = strtolower(substr(get_class($this), 0, -10));
$this->_request = $app->getReqObj();
$this->_response = $app->getResObj();
$this->_session = $app->getSesObj();
$this->_connectModel = $app->getConnModelObj();
}
public function dispatch($action, $routes = array()) {
$this->_action = $action;
$actionMethod = $action . self::ACTION;
if(!method_exists($this, $actionMethod)) {
$this->urlNotFound();
}
if($this->Authentication($action) && !this->session->isAuth()){
throw new AuthException();
}
$content = $this->$actionMethod($routes);
return $content;
}
protected function urlNotFound(){
throw new FileNotFoundException('FILE_NOT_FOUND'.$this->_controller.'/'. $this->_action);
}
protected function isAuth($action){
if($this->_authentication === true || (is_array($this->_authentication)
&& in_array($action, $this->_authenication, true))){
return true;
}
return false;
}
protected function render($param = array(), $viewFile = null, $template = null) {
$viewInfo = array(
'request' => $this->_request,
'baseUrl' => $this->_request->getBaseUrl(),
'session' => $this->_session
);
$view = new View($this->_app->getViewPass(), $viewInfo);
if(is_null($viewFilew)) {
$viewFilew = $this->_action;
}
if(is_null($template)) {
$template = 'template';
}
$path = $this->_controller . '/' . $viewFile;
$content = $view->render($path, $param, $template);
return $content;
}
protected function redirect($url){
$host = $this->_request->getHomeName();
$base_url = $this->_request->getBaseUrl();
$url = self::PROTOCOL . $host . $base_url . $url;
$this->_response->setStatusCode(302, 'Found');
$this->_response->setHeader('Location', $url);
}
protected function getToken($form){
$key = 'token/'. $form;//$formは呼び出し元から渡された「コントローラー名とアクション名」
$tokens = $this->session->get($key, array());
if(count($tokens) >= 5) {//トークンの数が5を超えたら実行する
array_shift($tokens);//先頭の一番古いトークンを削除
}
$password = $form . sessionId();
$token = password_hash($password, PASSWORD_DEFAULT);//パスワードをハッシュ化
$tokens[] = $token;
$this->_session->set($key, $tokens);
return $token;
}
protected function checkToken($formName, $token){
$key = 'token/' . $formName;//キーを作成
$tokens = $this->_session->get($key, $array());//トークン配列を取得
if(($currentToken = array_search($token, $tokens, true) !== false)) {//トークンがあるか
unset($tokens[$currentToken]);//使ったトークンは削除
$this->_session->set($key, $tokens);//再セット
return true;
}
return false;
}
}
コメント