MVCフレームワークを作ってみた。

とある案件でテンプレートエンジンを使ってサイトを構築していたのですが、どうも処理が複雑になってくると処理と表示の分離がイマイチになってしまうのと、ディレクトリ構造の美しさが保てなくなってきたので、MVCモデルを導入してみようと。PHPなので、CakePHPあたりが良いのでしょうけども、覚えるコストが結構かかりそうなので、とりあえず簡易なものを自作してみました。

MVCとは

Model View Controller(モデル・ビュー・コントローラ; MVC)は、コンピュータ内部のデータをユーザに提示し、それに対してユーザが何らかの指示を出すタイプの、独自のユーザーインタフェースをもつアプリケーションソフトウェアを、以下に述べるようなmodel・view・controllerの3つの部分に分割して設計・実装するという技法、又はそのような構造をいう。

MVCは、デザインパターンの1種と扱われる場合もあるが(MVCパターンと呼称される)、MVC自体が他の小さなデザインパターン(Observer パターン・Command パターン・Factory Method パターン・Facade パターンなど)を利用して実装されることが多いところからすると、デザインパターンというより、さらに粒度の大きい1種のソフトウェアアーキテクチャという方が適当であろう[1][2]。

各モジュールが比較的截然と分かれ、プログラムの見通しがよくなるとともに、ユーザインタフェース (UI) 部分を別のモジュールに取り替えることが容易となるのが利点である。自動プログラミングなどにも適している。

Model View Controller – Wikipedia

だそうです。どのサイトを見ても上記のような感じで説明してありますが、もともとオブジェクト指向から入ったわけではないので、いまいちバチッと頭に入ってきませんでした…。今回自作することで少しわかるようになりました。

ディレクトリ構造

ディレクトリ構造はCakePHPを参考に、以下のような感じにしてみました。

  • サイトルート
    • app
      • config
        • define.php // 設定ファイル
      • controllers
        • hoge_controllers.php
      • models
        • hoge.php
      • views
        • teplates // テンプレートエンジンで使用するテンプレートを格納
          • hoge.php
      • webroot // 静的ファイルを格納
        • css
          • default.css
    • vendors // 各種クラスファイル等を格納
      • fuga.class.php
    • index.php
    • .htaccess

.htaccessでリライトさせ、全てのアクセスをindex.phpに集め、そこでどのControllerに処理させるか振り分けます。

各ファイルについて

.htaccess

原則として全てのアクセスをindex.phpにリライトさせますが、静的ファイルはwebrootに格納したいので、一旦全てのアクセスをwebrootに回し、webrootに存在しないファイルだったらindex.phpに回すようにしました。
ただ、リクエストが行ったり来たりしてかなり無駄がある処理だと思うので、もう少し修正が必要かと思います(webrootをドキュメントルートにしてしまえば簡単かも。今回はサブディレクトリ内に展開する仕様なので少し混乱してこんな感じになっております)。

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /	# ここは設置するディレクトリに合わせて変更
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ app/webroot/$1 [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

index.php

.htaccessによってリクエストされたURLがurlという引数で渡ってくるので、それを取得して処理していきます。
命名規則として、「第2階層はController名」「第3階層はメソッド名」に利用することとしています。それ以降はクエリとして処理させたいと思っていますが、これはまだ実装できていません。
(例)http://example.hoge/controller/method/key/param/key/param/…
ちなみに、controller、methodの初期値はindexとしています。

また、ControllerでModelを指定して実行させるのを毎回書くのが面倒臭いので、Controllerを読み込むと同時に、Modelも読み込むようにしています。
動的にController、メソッド、Modelを読み込ませたたいので、下記のような命名規則を用いています。

Controller
ファイル名:hoge_controller.php
クラス名:HogeController.php
メソッド名:fuga(小文字)
Model
ファイル名:hoge.php
require_once( dirname(__FILE__).'/app/config/define.php' );
$param = preg_replace( '/\/$/', '', $_SERVER['REQUEST_URI'] );
$param = strtolower( preg_replace( '/\/?$/', '', $param ) );
$params = array();
if ( !empty( $param ) ) {
	// パラメーターを / で分割
	$params = explode( '/', $param );
	$params = array_clean( $params );
	// ルートディレクトリは消す
	if ( '/'.$params[0] == ROOT ) {
		array_shift( $params );
	}
}

// 1番目のパラメーターをコントローラーとして取得
$controller = ( !empty( $params[0] ) ) ? $params[0] : 'index';
// 2番目のパラメータをメソッドとして取得
$methodName = ( !empty( $params[1] ) ) ? $params[1] : 'index';
// パラメータより取得したコントローラー名によりクラス振分け
$classFile = $controller.'_controller.php';
$className = ucfirst( $controller ).'Controller';
// モデルも読み込む
$modelFile = $controller.'.php';

if ( !is_file( APPPATH.'/controllers/'.$classFile ) ) {
	header("HTTP/1.0 404 Not Found");
	exit;
}
// クラスファイル読込
require_once( APPPATH.'/controllers/'.$classFile );
if ( is_file( APPPATH.'/models/'.$modelFile ) ) {
	require_once( APPPATH.'/models/'.$modelFile );
}
// インスタンス化したいクラスを引数で渡す
$c = new ReflectionClass( $className );
// インスタンス化
$obj = $c->newInstance();
// メソッドを取得
if ( !$c->hasMethod( $methodName ) ) {
	header("HTTP/1.0 404 Not Found");
	exit;
}
$method = $c->getMethod( $methodName );
// メソッドを起動
$method->invoke( $obj );

数カ所、設定ファイルで定義している定数を用いていますので適当に脳内変換してください。
作っていて気づいたのですが、index.phpでファイルを読み込んだり処理をかいたものはControllerやModelでも使えるので便利ですね。

Controller

class IndexController {
	public function index () {
		// モデルをインスタンス化
		$index = new Index();
	}
}

Model

class Index {
	public function __construct () {
		// コンストラクタ
	}
	public function hoge () {
		// hoge
	}
}

define.php

動的にvendors内のクラスファイルを読み込むため、__autoloadを定義しておきます。

/*
*
〜 いろいろ定数の定義とか 〜
*
*/
function __autoload ( $name ) {
	$classFile = VENDORSPATH.'/'.$name . '.class.php';
	if ( is_file( $classFile ) ) {
		require_once( $classFile );
	}
}

まとめ

今まで読むばかりでほとんど試したことがなかったMVCモデルを自作すること少し理解が深まりました。が、上記のものは体感的に速度が遅いです。リダイレクト処理のせいなのか、index.phpのせいなのか…。
どうせなら後でCakePHPに入りやすいようにとCakeと同じような構成になるようにと考えながら作ったのですが、時間がかるわ遅いわで、普通にCake使ったほうが良かったと思いました…。エラー処理もしっかりしてるし…。

追記 2011.6.29

作って行くと、上記のままではviewの呼び出しを各コントローラーに書かないといけないので、各コントローラーはAppControllerを継承、その中でviewの呼び出し処理を書き、index.phpでそれを呼び出しなんてことをしたり、他にも似たような処理を共通化させようとすると、結局どんどんCakeに近づいて行く…。そしてどんどん重くなって行くorz

まぁPHPでMVCフレームワークといえばCakeでしょうし、それがある種の完成系なわけだから当たり前か。やはりここは一度しっかりとCakeも触ってみないといけませんね。何か良い書籍なんかあるんでしょうか?チュートリアルも前バージョン?のものっぽいし…。

参考サイト

  • ブックマーク
  • Feedly

この記事を書いた人

キタジマタカシ

長崎在住、フリーランスのWordPress テーマ / プラグインデベロッパー。 多数のプロダクトをオープンソースで開発・公開しています。現在は WordPress 有料テーマ Snow Monkey を開発・販売しています。