■PHPフレームワーク(Webシステム用)の概要
久しぶりに?PHPを触る機会があったので、フレームワークを作ってみました。
コンセプトは、
- スモールスタートできる。
- あんまり準備が必要ない。
- 直感的にわかる。
です。
なぜフレームワークを作ったかと言うとLaravelとかCakePHPとか使いづらい、と言うか作りづらいと思うからです。
車輪の再発明だと言われるかもしれませんが、世の中にたくさんのフレームワークが存在しているわけですし、今更1つくらい増えても問題はないでしょと思います。
ちなみに名前は、安易ですが「WSI: Web System Infrastructure(Webシステム基盤)」としています。
★Controllerを作るのは面倒
LaravelもCakePHPも、その他のフレームワークもほとんどの場合、Controllerを作成します。
Controllerで処理をした後Viewに制御を渡して、画面を表示します。
プロトタイプなど作成すると、ほとんどの場合、HTMLと紙芝居用のJavaScript、見た目のデザイン用のCSSしか作りません。
でも、そのHTMLとかJavaScriptとか、CSSとかをいざアプリケーションとして開発するために利用すると、結構、多大な変更が必要になります。
変更が必要な原因は、HTML/JavaScript/CSSがアプリケーション様に部品化されていないのがほとんどですが、それは動作イメージを捉えるために、システマチックな”部品化”などと言う考えを排除しているためです。
それでもデザインツールなどで作成すれば、部品化(と言うか、パターン化)されるわけですけど。
それよりも、例えばURLの書き換え(プロトタイプでは相対URLなどで書いていることが多い)をするのが意外に面倒です。
また、最近はRESTfullと言うのが流行りらしく、URLの構造がAPI的な構造だったりします。
でも、プロトタイプの時は、HTML/JavaScript/CSSを「.html」「.js」「*.css」としています。
それを「/xxx/yyy/zzzz」みたいなURLに修正する必要があります。
あと、LaravelとかはControllerをartisanコマンドとかで作成しなければならないです。
「手動で作れるよ」とか言われそうですが、結局Controllerはルールがあるので、開発者が多いほどartisanコマンドで、生成するべきです。
Controllerくらいは大したことないですけど…。
そして、実際のHTMLファイルなどはView専用のディレクトリに配置して、Controllerから制御を渡すコードを書く必要があります。
例えば、プロトタイプで作成した「index.html」があったとします。
それを、そのままブラウザで見られるようにすると良いと思います。
※これだけでは処理(Controllerなどに実装する処理)が書けないので、それは後程説明します。
[root]
- index.html
このような場合に、ブラウザで「http://xxxx.yyy.zzz/index.html
」を表示したら、上記のindex.htmlが表示されるようにしています。
だから、基本的にルーティングもいらない。
★Viewに対する処理は簡単に追加したい
上記で書いたContollerの話にも関連しますが、Viewに対する処理は簡単に追加できるようにしたいです。
具体的には、上記のファイル構成の場合に以下のようにします。
[root]
- index.html
- index.html.php
このように、ファイル名+.phpとすることで、そのファイルを表示する際に実行する処理を追加できます。
index.html.phpの中身(必要最低限)は下記のとおりです。
use WSI\Request;
use WSI\Status;
return function(Request $request) {
return Status::ok();
};
例えば、以下のようにするとindex.html(View)に変数を渡せます。
// index.html.phpファイルのコード
use WSI\Request;
use WSI\Status;
return function(Request $request) {
return Status::ok()->set_param('message', 'Hello world !!');
};
<!-- index.htmlファイルのコード -->
<!DOCTYPE html>
<html>
<body>
<?=$status->get_param('message') ?>
</body>
</html>
このように簡単に処理コードを追加できます。
ボタンをクリックした場合の処理はどうやって追加するのか?
それは後で説明します。
見てわかる通り、基本的にHTMLファイル(それ以外のファイルもですが…)は、phpソースとしてincludeしています。
テンプレートエンジンは使いません(php自体がテンプレートエンジンのようなものなので…)。
これにより、新しいテンプレートの構文を覚える必要はないですし、Viewのコーディングの自由度が上がります。
★データベースはSQLビルダーとか使いたくない
LaravelもCakePHPも、その他のPHPフレームワークも、SQLビルダー(もしくはそれに類するもの)を使えるようになっています。
確かにSQLビルダーを使うことでSQL(で実現したいこと)を抽象化できますし、他のDBに移行するのも簡単になります。
しかし、SQLビルダーのルールは覚えなければならないですし、SQLビルダーによってどのようなSQLを実行しているのか分かりづらくなります。
ほとんどの場合、DB開発用のツールを使ってSQLを考えていると思います。
そうなった場合、いちいちSQL文をSQLビルダー構文に変換しなければなりません。
また、その逆もしかりで、何か障害が発生しSQLの解析をするにしても、ログから取得したSQLかSQLビルダーから変換したSQLを元に調査していると思います。
調査した結果でSQLを変更した場合、またSQLビルダー構文に変換しなければなりません。
データベースの移行なんてシチュエーションも、ほとんどありません(その場合は、絶対にSQLを再実装するはずです)。
面倒ですし、無駄が多いですよね?
だったら、SQLのまま使った方が良くないですか?
SQLビルダーはSQLに比べて学習コストが低かったりする(売り文句)様ですが、結局どんなSQL(DBに対する命令)を実行しているのか
イメージしながらSQLビルダーでコーディングしますよね?
本当に面倒です!
DBのアクセスはSQLで実装しましょう。
このフレームワークでは、SQL(と言うかDB)に対してよく使う機能だけをメソッドとして提供します。
とあるテーブルに問い合わせるSQLは、以下のようなコードになります。
-- usersテーブルをユーザーIDで問い合わせるSELECT文です。
-- rootディレクトリ配下に、users\select\all.sqlとして配置されているものとします。
select
*
from
users
where
id = :user_id
and del_flg = 0
// index.htmlにユーザー情報を表示するために実装した、index.html.phpのコード
use WSI\Request;
use WSI\Status;
use WSI\Database;
return function(Request $request) {
$db = Database::connect(); //データベースへの接続(接続先の定義は別にありますが、ここでは割愛します)
$sql = Resource::from('/users/select/all.sql'); //SQL文を取得する。
$user = $db->row($sql, ['user_id'='xxxxxx']); //rowメソッドで結果セットの先頭1行目を取得する。
//バインド変数は変数名で参照されます(「?」は使わない)。同じ変数名なら同じ値がバインドされます。
return Status::ok()->set_param('user', $user);
};
<!-- index.htmlファイルのコード -->
<?php
$user = $status->get_param('user'); //取得したusersテーブルのレコードをあえて変数に格納
?>
<!DOCTYPE html>
<html>
<body>
<!--レコードはPDO::FETCH_BOTHで取得したものとなっている-->
ID: <?=$user['id'] ?><br>
名前: <?=$user['name'] ?><br>
:
:
</body>
</html>
煩わしいSQLビルダーは使いません。
素直にSQLを使いましょう。
なお、実はSQLファイルもphpファイルとしてincludeしています。
つまり、SQL文を動的に変更することも可能です。
ただし、SQLファイルをキャッシュしていないので、ループの中でResource::fromを実行しない様にしてください。
★機能別に分けて開発したい
PHPは、COMPOSERによるパッケージ化ができるのですが、開発しているシステム内を簡単にパッケージ化するのには向いていません。
一つのシステムでは、機能A/機能B/機能C…のように分かれていることが多いですが、LaravelやCakePHPはそういった機能ごとに
分割(分担)して開発するのは、あまり得意でない様に思います。
(自分が知らないだけなのかもしれませんが…)
PHPでも(Javaで言うところのjarファイルのように)pharファイル(PHp ARchiveでしたっけ?)が作れますが、実際のシステム開発であまり使われていないような気がします。
機能ごとに開発してpharファイル化したものを本番環境に配置することでリリースができたら、管理も楽になると思います。
もちろん、1システムが1セットで開発することのメリットはあります。
最近では開発に構成管理ツール(Subversionやgitなど)を導入することは多いですので、チェックアウト(gitだとクローン)すればフルセットで開発に必要な資材が手元のPCに簡単に用意できるわけです。
それ以外にも、他の機能のコードも含んでいるので調査したり解析することもできます。
しかし、やはり弊害の方が多い気がします。
- 見えるソースが多いため、思考にノイズが入りやすい。
- IDEなどによっては、ソース量が多いため多くのメモリが必要になる。
- 無意識に関係ない機能のソースを修正してしまって、気が付かない場合がある(コミットする際に気が付くとは思いますが…)。
- 依存関係が見えづらい。
など。
機能別にパッケージ化されてソースが隠蔽されることで、設計(特にインターフェースになる部分)が重要になるわけですが、本質的には設計が重要なのは当たり前です。
具体的にどうなるかと言うと、
- 公開ディレクトリ ←下記の共通機能、機能A、機能B、機能Cをpharファイルにして配置
- 共通機能 ←パッケージ
- 機能A ←パッケージ(共通機能に依存)
- 機能B ←パッケージ(共通機能と機能Aに依存)
- 機能C ←パッケージ(共通機能と機能Aに依存)
と言った具合に、機能ごとにパッケージを分けられるようにすれば、部分的な機能別のリリースも可能になります。
なお、当フレームワークでは、最初は1セットとして開発してたものを、機能別に分割(ディレクトリを分けるだけですけど)し、さらに機能別に分割したものをpharファイル化すれば、そのpharファイルを参照して動作する様になっています。
例えば、以下のようなディレクトリ構成でシステムに資材を配置していたとします。
[root]
+ index.html
+ [view]
+ login.html
+ topmenu.html
+ userlist.html
+ page1.html
+ page2.html
+ [js]
+ jquery.js
+ common.js
+ login.js
+ topmenu.js
+ userlist.js
+ page1.js
+ page2.js
+ [css]
+ bootstrap.js
+ common.css
+ method_a_common.css
これを以下のような構成に分割します。
↓root(公開ディレクトリ)
[root]
+ index.html
↓common(共通機能)
[common]
+ index.html
[view]
+ login.html
+ topmenu.html
+ userlist.html
+ [js]
+ jquery.js
+ common.js
+ [css]
+ bootstrap.js
+ common.css
↓機能A(ディレクトリ)
[method_a]
+ [view]
+ page1.html
+ page2.html
+ [js]
+ page1.js
+ page2.js
+ [css]
+ common.css ←元は、method_a_common.css
このように分割すると、それぞれの資源は以下のようにアクセスできます。
- 公開ディレクトリにある資材… /index.hml など
- 共通機能の資材… /common/view/login.html など
- 機能Aの資材… /method_a/view/page1.html など
この状態で、例えば「機能A」をパッケージ化して「method_a.phar」とした場合でも、やはり「/method_a/view/page1.html」と言うように、資材にアクセスするURLは変えません。
もちろん、フレームワークがその様になるように処理しているわけです。
資材の配置について、当フレームワークでは、以下のようなURLでアクセスする様にできます。
- 設定前)/method_a/view/page1.html
- 設定後)/method_a/page1.html
これは、「.htmlファイルは、viewディレクトリ配下に配置している」と設定しているためです。
同様に.jsはjsディレクトリ、.cssはcssディレクトリに配置すると設定できます。
このように、アクセスする資材の拡張子によって、ディレクトリの構造を決めることが可能です。
もう一つ、公開ディレクトリの特殊な処理についてです。
例えば、rootディレクトリ(公開ディレクトリ)内に、view/common/topmenu.htmlとなるように資材を配置したとします。
この場合、本来のcommonディレクトリ(共通機能)に格納されている資材よりも優先的に参照するようになっています。
つまり「修正してみてどう変わるか」を簡単に検証できます。
topmenu.htmlをコピーしてroot配下に修正したものを配置すると、修正内容を試すことが可能です。
また、method_a全体に波及するような修正を施し、method_a1として配置します。
そのmethod_a配下の資材を参照するURLを書き換えた資材(例えば、aタグのhrefを変更したtopmenu.html)を、root配下に配置します。
そうすると、元のmethod_aディレクトリ及びtopmenu.htmlを変更せずに、修正を試すことが可能になります。
想定通りに動作することを確認した後は、method_aをmethod_a1に挿げ替え、topmenu.htmlをcommonディレクトリ配下に配置すれば、不具合の発生を抑えることが可能になります。
★やっぱりAjaxですよね
リクエストに対してViewの全てをレスポンスする場合、実装は難しくなります。
ここで言う「難しい」の意味は、単にコード量が多いという意味ではありません。
例えば、処理結果で「はい/いいえ」の確認ダイアログを表示して、「はい(または、いいえ)」を選択して処理を継続するような場合に、従来の画面リフレッシュ方式のWeb実装(こういうのをMVCと言えばよいのかな?)では、ちょっとしたテクニックが必要だったりします。
それは、アクションに対する一連の処理(トランザクション)とHTTPによるリクエスト/レスポンスのサイクルは、必ずしも一致しないと言うことに起因します。
また、HTTP通信の役割は画面の再描画であるため、確認ダイアログを表示するために一旦はブラウザにレスポンスしないとなりません。
ブラウザにレスポンスと言う事は、この時点でHTTP通信(リクエスト~レスポンス)の1サイクルは完了しています。
しかし、トランザクションとしては継続中です。
その為、「はい・いいえ」を選択した後は2サイクル目のHTTP通信に対して、1サイクル目のHTTP通信の状態を引き継ぐ必要があります。
このようなシチュエーションは、割とよく発生します。
Ajaxの場合は、HTTP通信の役割がデータのやり取りでしかない(画面の再描画ではない)ため、アクションのトランザクション中の「データ・アクセス」と「画面の描画」が切り離せると言う事です。
(データベースのトランザクションの意味ではないです)
これにより、実装はシンプルになり意味的に理解しやすくなるため、結果として実装が難しくなくなります。
LaravelやCakePHPは、基本的にMVCフレームワークであるため、こういった問題が発生しやすくなります。
(LaravelだとRESTFull APIを実装できるので、設計次第ではAjaxも選択できるわけですが…)
当フレームワークは、一般に言うMVCのフレームワークではなく、Ajax方式のフレームワークになっています。
その為、クライアントで動作する基本JavaScriptとして、以下のような機能を提供しています。
- Ajax
- DOM操作
また、上記のほかに、JavaScriptを実装するためのいくつかの機能も提供しています。
- ネームスペース化
- クラス化
- 画面項目のコントロール
- 画面管理クラス化
- IFrameをまたいだメッセージ通信
結構長くなったので続きは別の機会に
とりあえず、他にもフレームワークとしての機能は実装していますが、さすがに長くなってきたので一旦区切りとします。
また、まだまだ未完成でもありますので、今後も改善・機能追加は発生します。
以下に、公開しているGitHubのURLを掲載しますので、興味のある方はご参照ください。
今後は、実際のアプリケーションを作成するための実装方法を掲載するようにします。
それでは、また。