ClassLoaderクラスはオートロードに関する処理をまとめたクラス。
- PHPにオートローダークラスを登録する処理(register()メソッド)
- オートロードが実行された際にクラスファイルを読み込む処理(loadClass()メソッド)
作成したClassLoaderをオートロードに登録するための設定ファイルです。
現段階では明示的にrequireを用いてクラスファイルを読み込みます。
registerDir()で coreディレクトリと modelsディレクトリをオートロードの対象ディレクトリに設定し、register() でオートロードに登録します。
HTTPメソッド(GET/POST)の判定や $_GET、$_POST などの値の取得、リクエストされたURLの取得、サーバのホスト名やSSLでのアクセスかどうかと行った判定などの機能の実装をしているクラスです。
isPost() メソッドで、HTTPメソッドが POST かどうかの判定を行います。また、getGet() メソッドや getPost() メソッドでそれぞれ $_GET、$_POST 変数からの値の取得を行います。
getHost() メソッドで、サーバのホスト名の取得をします。isSsl() メソッドでは HTTPS でのアクセスかどうかの判定を行います。getRequestUri() メソッドではリクエストされたURLのホスト部分以降を返します。
getBaseUrl() メソッドでベースURL(ホスト部分より後ろから、フロントコントローラまでの値)を取得し、getPathInfo() メソッドで PATH_INFO(ベースURLより後ろの値)を取得します。これらの値を利用してURLの制御を行って行きます。
Routerクラスはルーティング定義配列とPATH_INFOを受け取り、ルーティングパラメータを特定する役割をもちます。
compileRoutes() メソッドで、受け取ったルーティング定義配列中の動的パラメータ指定を正規表現で扱える形式に変換します。resolve() メソッドで、変換済みのルーティング定義配列とPATH_INFOマッチングを行い、ルーティングパラメータの特定を行います。特定できなかった場合は False を返します。
Responseクラスはレスポンスを表すクラスで、HTTPヘッダとHTMLなどのコンテンツを返すのが主な役割です。
setContent() メソッドで、$contentプロパティにHTMLなどの実際にクライアントに返す内容を格納します。setStatusCode() メソッドで、$status_codeプロパティ、$status_textプロパティにHTTPのステータスコードを格納します。setHttpHeader() メソッドで、$http_headersプロパティにHTTPヘッダを格納します。send() メソッドで各プロパティに設定された値を元にレスポンスの送信を行います。
DbManagerクラスでは、PHPに標準で付属するデータベース抽象化ライブラリであるPDO(PHP Data Object)クラスを使って、データベースへのアクセスを管理します。
DbRepositoryクラスは、実際にはデータベース上のテーブルごとにこのクラスを継承したクラスを作成し、そこにデータベースアクセス処理を記述して行きます。userテーブルがあれば、UserRepositoryクラスを作成する、と行った具合です。
DbManagerクラスとDbRepositoryクラスの関係性としては、DbManagerクラスの内部にテーブルごとのRepositoryクラスを保持するようにします。DbManagerクラスのインスタンスが $db_manager 変数に入っているとして、$db_manager->get('User')
のようにして UserRepository クラスを取得するようにします。
$db_manager = new DbManager();
$db_manager->connect('master', array(
'dsn' => 'mysql:dbname=mydb;host=localhost',
'user' => 'myuser',
'password' => 'mypass',
));
$db_manager->getConnection('master');
$db_manager->getConnection(); #=> master が返ってくる
DbManagerクラス内、getConnection() には引数の指定がなければ最初に作成したコネクションを取得するようにしてある。それ以外のものを使用する場合に備え、getConnectionForRepository()メソッドの実装も行っている。
$repository_connection_mapプロパティにテーブルごとのRepositoryクラスと接続名の対応を格納し、getConnectionForRepository()メソッドでRepositoryクラスに対応する接続を取得しようとした際に、$repository_connection_mapプロパティに設定されているものは getConnection()メソッドに接続名を指定し、そうでなければ最初に作成したものを取得するという機能になる。
DbRepositoryクラスは後ほど記述するが、1度インスタンスを作成したら、それ以降インスタンスを生成する必要は無いように実装してあり、全てのインスタンスをDbManagerクラスで管理できるようにしている。それらは全て $repositories
に格納している。
DbRepositoryクラスはデータベースへのアクセスを行うクラスで、テーブルごとにDbRepositoryクラスの子クラスを作成するようにします。
各Repositoryクラスには、例えばuserテーブルであればUserRepositoryクラスを定義し、userテーブルへレコードの新規作成を行う insert()
メソッドや id というカラムを元にデータを取得する fetchById()
メソッドなどを必要に合わせて作成していくことを想定しています。
それぞれのメソッド内部ではSQLを実行することになりますが、SQLの実行時に頻繁に出てくるような処理をDbRepositoryに抽象化しておきます。
__construct()メソッドとsetConnection()メソッドはDbManagerクラスからPDOクラスのインスタンスを受け取って内部に保存するメソッドです。ここで受け取ったPDOクラスのインスタンスに対してSQL文を実行します。
プリペアドステートメントについても補足しておきます。
$name = $_POST['name'];
$sql = "INSERT INTO user (name) VALUES('" . $name . "')";
上記の例はSQLに脆弱性があります。ユーザの入力に対しては下記の例のように適切に入力値をエスケープする必要があります。
$sql = 'INSERT INTO user (name) VALUES(:name)';
$stmt = $con->prepare($sql);
$params = array(':name' => $_POST['name']);
$stmt->execute($params);
SQLには直接変数を入れずに、「:name」のような文字列を指定します。:nameは動的パラメータが入るという指定で、プレースホルダと呼びます。プリペアドは準備済みという意味です。このプリペアドステートメントを用いると、プレースホルダ部分に入ってくる値は適切にエスケープされます。
Sessionクラスはセッション情報を管理するクラスです。$_SESSION 変数のラッパークラスに相当します。
set()メソッドと get()メソッドで $_SESSION への設定と取得を行います。remove()メソッドは $_SESSION から指定した値を削除するメソッドです。clear()メソッドは$_SESSIONを空にするメソッドです。
setAuthenticated()メソッドと isAuthenticated()メソッドはユーザがログイン状態を制御するメソッドです。セッション内に _authenticated というキーでログインしているかどうかのフラグを格納し、これを用いてログイン状態の判定を行います。
あくまで認証の機構としてセッションを利用するだけなので、このメソッドがこのクラスにあるのは少々不適切かもしれませんが、フレームワークの簡略化のため今回はSessionクラスに実装します。
セッション固定攻撃に対してフレームワーク側で多少なりとも対策できるよう、ログイン状態の変更を行う setAuthenticated() メソッド内で regenerate()を実行するようにします。
Applicationクラスはフレームワークの中心となるクラスです。このクラスではRequestクラスやRouterクラス、Responseクラス、Sessionクラスなどのオブジェクトの管理を行うほか、ルーティングの定義、コントローラの実行、レスポンスの送信など、アプリケーション全体の流れを司ります。
また、管理するのはオブジェクトだけではなく、アプリケーションの様々なディレクトリへのパスの管理なども行います。この他にも、デバッグモードで実行できるような機能も持たせます。
まず1つめが setDebugMode()メソッドです。デバッグモードに応じてエラー表示処理を変更するようにしています。
次に呼び出すのが initialize()メソッドで、クラスの初期化処理を行います。Routerクラスはインスタンスを作成する際にルーティング定義配列を渡すように実装している為、ルーティング定義配列を返す registerRoutes() メソッドを呼び出すようにしています。
registerRoutes() メソッドは抽象メソッドとして定義しています。このように抽象メソッドとして定義しておけば呼び出し側は変える必要がなく、個別のアプリケーションで registerRoutes() メソッドを定義しないとエラーになるため定義漏れも無くなります。
initialize() メソッドの直後に呼び出されるのが configure() メソッドです。これは空のメソッドとして定義しています。個別のアプリケーションで様々な設定をできるように定義しています。
コンストラクタで呼び出しているメソッドの他にも、様々なメソッドを定義しています。設定したプロパティの値を取得するメソッドとコントローラやモデルのディレクトリへのパスを返すメソッドです。
getRootDir() メソッドはアプリケーションのルートディレクトリへのパスを返すメソッドで、これはアプリケーションごとに設定するように抽象メソッドとして定義しています。パスを返すメソッドをわざわざ定義しているのは、必要があればディレクトリ構成を変更できるようにするためです。
Applicationクラスでは全体の処理の流れも管理します。コントローラを呼び出してアクションを実行する処理を実装します。
Routerからコントローラを特定しレスポンスの送信を行うまでを管理する run()
メソッド、実際にアクションを実行する runAction()
メソッド、runAction() メソッドの中でコントローラクラスを生成する findController()
メソッドの3つでコントローラの呼び出し実行を行います。
Routerクラスの resolve()メソッドを呼び出してルーティングパラメータを取得し、コントローラ名とアクション名を特定します。それらの値を元に runAction()メソッドを呼び出してアクションを実行します。
run() メソッドはアプリケーションがユーザのリクエストに対応するためのトリガーとなるメソッドです。これを実行することで、Applicationクラス内部に持つオブジェクトを流れに沿って呼び出し、レスポンスを返します。
runAction()メソッドは実際にアクションを実行するメソッドです。コントローラのクラス名はコントローラ名に Controller
を付けるというルールにします。またルーティングにはコントローラ名の先頭を小文字で指定するようにしたいので、ucfirst()
関数を用いて先頭を大文字にしています。
runAction()メソッドはコントローラ名とアクション名を直接受け取るため、内部で別のアクションを呼び出したい場合にコントローラ名を直接指定して呼び出すことも可能です。
findController()メソッドではコントローラクラスが読み込まれていない場合に、クラスファイルの読み込みを行います。クラスファイルの読み込みが完了したらコントローラクラスを生成しています。Controllerクラスの実装時には、コンストラクタにApplicationクラス自身を渡すようにします。
コントローラの生成が完了したらrunAction()メソッドに戻って、Controllerクラスのrun()メソッドを呼び出しています。この戻り値として受け取った値をレスポンスに設定しています。
アクションは1つの画面に対応します。例えばデータの入力画面や一覧画面はそれぞれ1つのアクションを定義します。コントローラはいくつかのアクションのまとまりです。実際に作成する際は画面ごとにアクションを作成します。
アクションが呼び出される時点では既に、フレームワークの初期化やデータベースとの接続、ルーティングが行われています。そのため画面を追加するたびにそういった処理を呼び出す必要はなく、それらの情報が整った状態で開発を進めていくことが可能です。
Controllerクラスの処理の流れとしては run()メソッドの中で個別のアクションを呼び出し、アクションの内部でモデルにあたるDbRepositoryクラスからデータの取得などの処理を行った後、Viewクラスを生成してアクションに応じたHTMLのレンダリングを行い、run()メソッドの実行結果としてレンダリングしたHTMLを文字列として返す、といったようになります。
なお、一般的なMVCモデルでのコントローラはビューファイルにデータを渡す部分までを担い、ビューファイルのレンダリングは別のクラスが行います。
今回のフレームワークでコントローラにビューファイルをレンダリングする機能を持たせています。これはビューファイルのレンダリングをコントローラ内で行えた方が、例えばレンダリング結果をファイルに保存してリダイレクトする、などといった処理を行う上で都合が良いためです。
Controllerクラスは画面を実際に制御するための重要なクラスなので、いくつかの機能に分割して説明をします。Controllerには主に次のような機能を実装します。
- アクションを実行する
run()
メソッド - ビューファイルをレンダリングする
render()
メソッド 内部でViewクラスを呼び出す - リダイレクトを行う
redirect()
メソッド - 404エラー画面に遷移する
forword404()
メソッド - CSRF対策を行う
generateCsrfToken()
メソッドとcheckCsrfToken()
- ログイン状態の制御機構 run() メソッドの内部で行う
Controllerクラスはアプリケーションごとに子クラスを作成し、その中にアクションを定義するようにするため、Controllerクラス自体は抽象クラスとして定義します。
get_class()
関数は指定したオブジェクトのクラス名を取得する関数です。get_class($this)
で自分自身のクラス名を取得し、substr()関数で10文字分である Controller を取り除いています。最終的にstrtolower()
関数で小文字に変換しています。
このフレームワークではアクションはControllerクラスに実装されたメソッドにあたります。アクションにあたるメソッド名は「アクション名+Action()」というルールで扱います。
run()メソッドの第1引数としてアクション名を受け取ります。アクション名は $action_name プロパティに格納します。これを元にアクションの名前を $action_method変数に格納し、method_exists()関数を用いてメソッドが存在しているかチェックします。
このフレームワークではViewクラスがビューファイルの読み込みなどの役割を担います。Viewクラスでは、Viewクラス内にHTMLを書くのではなく、HTMLファイルの読み込みと、HTMLに変数を渡すということを行います。
HTMLはルートディレクトリにあるviewsディレクトリ内に配置します。その際、viewsディレクトリ直下にHTMLをたくさん置くのではなく、コントローラごとにディレクトリを分けるようにします。例えば、userコントローラのshowアクションなら、views/user/show.php
となります。アクションとの関連性を明確にすることで管理しやすくするためにこうしますが、アクションに指定できるようにもします。
HTMLを記述する際に毎回、<html><head>....
のように全体のHTML構造を書くのは少々煩わしいので、全体での共通の部分と、アクションごとに変わる部分を分離します。
全体で共通の部分をレイアウトと呼ぶようにします。レイアウトはviews/layout.php
に配置するようにします。こうすることでビューファイルを配置する手間を抑えられます。
Viewクラスはビューファイルの読み込みや、ビューファイルに渡す変数の制御を行います。
ビューファイルの読み込みはrequireを用いれば実行可能です。しかしその際、読み込んだファイル内で出力が行われていると、読み込んだ時点で出力が行われてしまいます。フレームワークでは出力情報を文字列として読み込み、レスポンスに設定する必要があります。
そこで アウトプットバッファリング という仕組みを用いて、出力を文字列として取得します。また、requireを用いてファイルを読み込むと、requireを実行した側でアクセス可能な変数に対し、読み込まれた側のファイルでもアクセスすることができます。この仕組みを用いてビューファイルでも変数を参照できるようにします。
しかし、例えば、アクション内で利用している変数すべてを読み込むようにすると、ビューファイル内でどのような値を出力されているのかがわかりづらくなり、管理コストが上がってしまいます。そこでビューファイルで必要な変数のみを明示的に指定できる仕組みを作成します。
render()メソッドは実際にビューファイルの読み込みを行うメソッドです。第1引数はビューファイルへのパスを指定します。これを $base_dirプロパティの中から探します。今回ビューファイルの拡張子は.php
で固定します。第2引数はビューファイルに渡す変数を指定します。これは連想配列で指定します。第3引数はレイアウトファイル名を指定します。レイアウトファイルの指定が必要なのはControllerクラスから呼び出された際のみなので、ここではデフォルト値をFalseにしています。Falseの場合、レイアウトの読み込みは行いません。
変数展開について
<?php
//連想配列のキーと値を元に変数低回が行われる
extract(array('foo' => 'bar'));
//変数$fooには"bar"が入っている
echo $foo; // => "bar"
ビューファイルでは1つ1つの値を変数として利用できる方が扱いやすいため変数展開を行います。連想配列のそれそれの要素を変数展開する最も簡単な方法がextract()
関数を利用することです。この状態でビューファイルを読み込むと、ビューファイルから展開した変数にアクセスすることが可能です。
ここまででフレームワークの作成はすべて完了です。 作成したファイルは以下のようになっています。※順番はアルファベット順
application/
bootstrap.php
controllers/
core/
Application.php
ClassLoader.php
Controller.php
DbManager.php
DbRepository.php
HttpNotFoundException.php
Request.php
Response.php
Router.php
Session.php
UnauthorizedActionException.php
View.php
models/
views/
web/
.htaccess
index.php
このフレームワークを用いて開発する際は次の項目を実際に作っていく必要があります。
-
Applicationクラス
- ルートディレクトリの指定
- アクションにあわせたルーティング定義
- 接続するデータベースの指定(DbManagerクラスを操作)
- ログインアクションの指定
-
index.php
- Applicationの呼び出しと実行
-
Controllerクラス (C:コントローラ)
- 役割にあわせて子クラスの作成
- 作成する画面にあわせたアクション定義、処理の実装
- ログインが必要なアクションの指定
-
DbRepositoryクラス (M:モデル)
- データベース上のテーブルごとに子クラスの作成
- データベースアクセス処理の実装
-
ビューファイル (V:ビュー)
- アクションにあわせたHTMLの記述
- レイアウトファイルの記述
フレームワークはMVCモデルを採用しているため、モデル/ビュー/コントローラに相当する箇所をアプリケーションに応じて作成を行います。
データベースへの接続やログイン管理など、フレームワークに様々な機能が備わっているため、必要な作業に注力できるようになります。