フラットなPHPからSilexへ
追記
- DB接続時にcharset=utf8を指定
- bindValueで暗黙の型変換されないように変更
- Pimpleをサービスロケータとして使う場合の注意点を追加
- テンプレートとしてフラットなPHPからTwigで書いた場合を追加
前提
スクリプト、ファイル、DBの文字コードはすべてUTF-8で統一です。
また、最初に以下のMySQLのテーブルがあることを前提として記事を書いています。
- Database: MySQL
- user: myuser
- password: mypassword
CREATE TABLE `blog_db`.`post` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `body` text NOT NULL, `date` date NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB CHARACTER SET utf8
フラットなPHPからSymfony2へ ... にインスパイアされて
この記事は Symfony versus Flat PHP (Symfony Docs) をベースに
Symfony2ではなくマイクロフレームワーク(Silex)を使ったパターンに書き換えたらどうなるかについて書いています。
また、姉妹記事としてフラットなPHPからSlimへというのも書いたのですが、フレームワークの話が出てくるまでの前半は共通です。
しかも、書いていて気づいたのですが、元記事のコードはそのままでは動きません。(えっ!!)
そのあたりをカバーするためにもとりあえず書いてみました。
参照: 日本語訳 http://docs.symfony.gr.jp/symfony2/book/from_flat_php_to_symfony2.html
なぜ マイクロフレームワーク は単にファイルを開いてフラットな PHP を書くよりも良いのでしょうか?
マイクロフレームワークをご存知でしょうか? 一番有名なのは Ruby の sinatra だと思います。通常のMVCという考え方ではなく、どのリクエストメソッドでどのURIにアクセスされたかによって、レスポンスを用意するというシンプルな構成が特徴です。
PHPではSilex, Slimなど sinatraからインスパイアされて開発されているマイクロフレームワークがあります。
フラットなPHPを使うよりも早く、マイクロフレームワークを利用することでよりよいソフトウェアを開発できるということを、1ステップずつ説明していきたいと思います。
この記事では、最初にフラットな PHP でシンプルなアプリケーションを記述します。
フラットなPHPによる単純なブログ
フラットなPHPでざくっとブログの記事を表示するコードを書くと次のようになります
<?php $pdo = new PDO( 'mysql:host=localhost;dbname=blog_db;charset=utf8', 'myuser', 'mypassword', array(PDO::ATTR_EMULATE_PREPARES => false) ); $stmt = $pdo->query('SELECT id, title FROM post'); ?> <html> <head> <title>投稿の一覧</title> </head> <body> <h1>投稿の一覧</h1> <ul> <?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)): ?> <li> <a href="show.php?id=<?php echo htmlspecialchars($row['id'], ENT_QUOTES, 'utf-8') ?>"> <?php echo htmlspecialchars($row['title'], ENT_QUOTES, 'utf-8') ?> </a> </li> <?php endwhile; ?> </ul> </body> </html>
(元記事では、PDOすら使っていませんでしたが、さすがにこの時代にそれも無いかということでPDOを使っています。)
(元記事では エスケープせずにechoしてましたが、さすがにこの時代に(ry )
HTMLと混在させることができたり、HTMLの中での繰り返し処理はのようにブロックの閉じタグが分かりやすくなっていたりするところはPHPらしいところだと思います。
このようにサクッと書けるのはいいことなのですが、アプリケーションが大きくなってくるとメンテナンスが大変になってくることが想像できます。
次のような解決すべき問題があります。
- エラーチェックがない: データベースへの接続が失敗した場合はどうなるのでしょう?
- 体系化されていない: アプリケーションが複雑になってくると、この1ファイルはどんどんメンテナンスできなくなってきます。フォームの送信を行うコードや、メール送信するコードを追加したいときはどこに書いたらよいのでしょう?
- コードの再利用性が低い: 全てが1ファイルにまとまっているので、アプリケーションで新しく作成したページでこのコードの一部を再利用することができません。
note: ここで述べられていない他の問題として、データベースが MySQL に固定されてしまうということがあります。 よくある解決策として、何かしらのデータベースの抽象化を行うライブラリ(Doctrine, Propel, フレームワークが提供しているライブラリ)を使うことになります。 Silexであれば、DoctrineのDBALというライブラリを簡単に利用できるようになっています。 Slimはデータベースアクセスのためのライブラリは用意してくれていないのですが、 PDOを薄くカプセル化したライブラリを自前で用意したり、 「特定のデータベースに固定されてもいい」という判断も有りだと思います。
さぁ、これらの問題を解決していきましょう
表示部分(view)の分離
このコードは、HTML部分とアプリケーションの「ロジック」を分離することで、すぐに改善できますね。
<?php $pdo = new PDO( 'mysql:host=localhost;dbname=blog_db:charset=utf8', 'myuser', 'mypassword', array(PDO::ATTR_EMULATE_PREPARES => false) ); $stmt = $pdo->query('SELECT id, title FROM post'); // HTML部分のコードを読み込む require 'templates/list.php';
HTML部分は別のファイル (templates/list.php) に保存するようにしました。これは本来、テンプレート風の PHP 文法を使う HTML ファイルです。
<html> <head> <title>投稿の一覧</title> </head> <body> <h1>投稿の一覧</h1> <ul> <?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)): ?> <li> <a href="show.php?id=<?php echo htmlspecialchars($row['id'], ENT_QUOTES, 'utf-8') ?>"> <?php echo htmlspecialchars($row['title'], ENT_QUOTES, 'utf-8') ?> </a> </li> <?php endwhile; ?> </ul> </body> </html>
慣例によって、全てのアプリケーションのロジックを含むファイル「index.php」は「コントローラ」と呼ばれます。コントローラという用語は、使用する言語やフレームワークに関係なく、よく聞くことでしょう。コントローラは、あなたのコードにおける、ユーザからの入力を処理し、レスポンスを返す部分のことを指しています。
この場合、コントローラはデータベースからのデータを準備し、それからそのデータを提供するテンプレートをインクルードします。テンプレートとコントローラを分離させることによって、何か他のフォーマット (例えば JSON フォーマットの list.json.php) でブログのエントリをレンダリングする必要があった場合に、テンプレートファイルだけを簡単に変更することができます。
アプリケーション (ドメイン) ロジックの分離
今のところアプリケーションは1つのページしか含んでいませんが、2番目のページが同じデータベース接続、あるいは同じ投稿の配列を使用する必要がある場合はどうでしょうか?アプリケーションのコアの動作とデータアクセスの機能を mode.php という新しいファイルに分離するように、コードをリファクタリングしてみましょう。
<?php // model.php function get_database_connection() { $pdo = new PDO( 'mysql:host=localhost;dbname=blog_db;charset=utf8', 'myuser', 'mypassword', array(PDO::ATTR_EMULATE_PREPARES => false) ); return $pdo; } function close_database_connection(&$pdo) { $pdo = null; } function get_all_posts() { $pdo = get_database_connection(); $stmt = $pdo->query('SELECT id, title FROM post'); $posts = array(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $posts[] = $row; } close_database_connection($pdo); return $posts; }
Tip model.php というファイル名が使われているのは、アプリケーションのロジックとデータアクセスが 伝統的に「モデル」というレイヤーだからです。 うまく体系付けられたアプリケーションでは、「ビジネスロジック」を表すコードの大部分は、 モデル内に存在するべきです (コントローラに存在するのとは対照的に) 。 そしてこの例とは違って、モデルの一部分のみが実際にデータベースへのアクセスに関わることになります。
コントローラー(index.php)はさらにシンプルになります。
<?php require 'model.php'; $posts = get_all_posts(); require 'templates/list.php';
テンプレートもこの$postsを使うように修正しシンプルになります。
<html> <head> <title>投稿の一覧</title> </head> <body> <h1>投稿の一覧</h1> <ul> <?php foreach ($posts as $post): ?> <li> <a href="/show.php?id=<?php echo htmlspecialchars($post['id'], ENT_QUOTES, 'utf-8') ?>"> <?php echo htmlspecialchars($post['title'], ENT_QUOTES, 'utf-8') ?> </a> </li> <?php endforeach; ?> </ul> </body> </html>
さきほどまで$rowという変数を利用していましたが、$postになりました。名前が変わっただけでもコードは意図を表現できて読みやすくなるのがわかりますね。
この時点で、コントローラの唯一のタスクは、アプリケーションのモデルレイヤー(モデル)からデータを取り出し、そのデータをレンダリングするためにテンプレートを呼び出すことです。これは、モデル-ビュー-コントローラ(MVC)パターンのとても単純な例です。
レイアウトの分離
この時点でアプリケーションは、いくつかの有利な点を持つ3つの明確な部品(MVC)にリファクタリングされ、別のページでほとんど全てを再利用できる機会を得ます。
コードの中で再利用できない唯一の部分は、ページレイアウトです。レイアウトでは各ページで共通で利用される部分です。layout.php ファイルを新しく作成して、この問題に対応しましょう。
<!-- templates/layout.php --> <html> <head> <title><?php echo $title ?></title> </head> <body> <?php echo $content ?> </body> </html>
次に、list.php をレイアウトを拡張するように修正します。
<?php $title = '投稿のリスト' ?> <?php ob_start() ?> <h1>投稿のリスト</h1> <ul> <?php foreach ($posts as $post): ?> <li> <a href="show.php?id=<?php echo htmlspecialchars($post['id'], ENT_QUOTES, 'utf-8') ?>"> <?php echo htmlspecialchars($post['title'], ENT_QUOTES, 'utf-8') ?> </a> </li> <?php endforeach; ?> </ul> <?php $content = ob_get_clean() ?> <?php include 'layout.php' ?>
ここで、レイアウトの再利用を可能にする方法を説明しましょう。残念なことに、これを可能にするために、いくつかの格好悪い PHP の関数 (ob_start() と ob_get_clean())をテンプレート内で使わなければならないことにお気づきだと思います。
(元記事ではob_end_cleanになってたのですが、それじゃ動かないっすよね...)
正直テンプレートを用意するのに毎回これを書くのはヒドイですよね。通常はフレームワークが提供しているテンプレートのライブラリや,Twig, Smarty, PHPTALなどのサードパーティーの優れたテンプレートエンジンを利用することになります。
ブログの「show (単独表示) 」ページを追加
ブログの「list (一覧表示)」ページは、より体系付けられて再利用可能なコードになるようリファクタリングされました。これを証明するために、id をクエリーパラメータとしてそれぞれのブログの投稿を表示する「show (記事の詳細表示)」ページを追加しましょう。
まず初めに、与えられた ID を元にそれぞれのブログの結果を取得する関数を model.php ファイルに追加する必要があります。
<?php // model.php function get_post_by_id($id) { $pdo = get_database_connection(); $sth = $pdo->prepare('SELECT id, date, title, body FROM post where id = :id'); $sth->bindValue(':id', $id, PDO::PARAM_INT); $sth->execute(); $post = $sth->fetch(PDO::FETCH_ASSOC); close_database_connection($pdo); return $post; }
次に、この新しいページのためのコントローラである show.php という新しいファイルを作ってください。
<?php require_once 'model.php'; $post = get_post_by_id($_GET['id']); require 'templates/show.php';
最後に、それぞれの投稿を表示するための templates/show.php という新しいテンプレートファイルを作ってください。
<?php $title = $post['title'] ?> <?php ob_start() ?> <h1><?php echo htmlspecialchars($post['title'], ENT_QUOTES, 'utf-8') ?></h1> <div class="date"><?php echo htmlspecialchars($post['date'], ENT_QUOTES, 'utf-8') ?></div> <div class="body"> <?php echo htmlspecialchars($post['body'], ENT_QUOTES, 'utf-8') ?> </div> <?php $content = ob_get_clean() ?> <?php include 'layout.php' ?>
2番目のページを作るのは、とても簡単で、重複したコードもありません。まだこのページには、フレームワークが解決できるさらにやっかいな問題があります。例えば、「id」クエリーパラメータが存在しなかったり不正な場合、ページがクラッシュする原因になります。このような問題では 404 ページを表示する方がよいですが、まだこれは簡単には実現できません。
それ以外の大きな問題として、それぞれのコントローラのファイルが model.php ファイルを含まなくてはならないということです。それぞれのコントローラファイルが、突然追加のファイルを読み込む必要に迫られたり、その他のグローバルなタスク(例えばセキュリティの向上など)を実行する必要が出た場合、どうなるでしょう。現状では、それを実現するためのコードは全てのコントローラのファイルに追加する必要があります。もし何かをあるファイルに含むのを忘れてしまった時、それがセキュリティに関係ないといいのですが…。
「フロントコントローラ」の出番
フロントコントローラを使うことでファイルの読込忘れが起こらないようにすることができます。これは、全てのリクエストが処理される際に通過する一つの PHP ファイルです。フロントコントローラによって、アプリケーションの URI は少し変更されますが、より柔軟になり始めます。
フロントコントローラなしの場合
index.php をフロントコントローラとして使用した場合
Tip URI の index.php という一部分は、Apache のリライトルール(あるいはそれと同等の仕組み)を使っている場合は、省略することができます。 この場合、ブログの単独表示ページの URI は、単純に /show になります。
フロントコントローラを使用する時は、一つの PHP ファイル(今回は index.php)が全てのリクエストをレンダリングします。ブログの単一表示ページでは、/index.php/show という URI で実際には、完全な URI に基づいてルーティングのリクエストに内部的に応える index.php ファイルが実行されます。ここで見たように、フロントコントローラはとてもパワフルなツールなのです。
フロントコントローラの作成
我々のアプリケーションに関して、大きな一歩を踏み出そうとしています。全てのリクエストを扱う一つのファイルによって、セキュリティの扱いや、設定の読み込み、ルーティングといったことを集中的に扱えるようになります。我々のアプリケーションでは index.php が、リクエストされた URI に基づいて、ブログの一覧表示ページあるいは単一表示ページをレンダリングするのに十分なぐらい洗練されている必要があります。
<?php // index.php // グローバルライブラリの読み込みと初期化 require_once 'model.php'; require_once 'controllers.php'; // ドキュメントルート以外に設置した場合のベースとなるアプリケーションのパス $base = '/path/application_root'; // リクエストを内部的にルーティング $uri = $_SERVER['REQUEST_URI']; if ($uri === ($base .'/index.php')) { list_action(); } elseif ( preg_match("#^{$base}/index.php/show#", $uri) && isset($_GET['id'])) { show_action($_GET['id']); } else { header('Status: 404 Not Found'); echo '<html><body><h1>ページが見つかりません</h1></body></html>'; }
コードの体系化のために、2つのコントローラ(以前の index.php と show.php)は、PHP の関数になり、それぞれは別のファイル controllers.php に移動されました。
<?php // controllers.php function list_action() { $posts = get_all_posts(); require 'templates/list.php'; } function show_action($id) { $post = get_post_by_id($id); require 'templates/show.php'; }
フロントコントローラとして、index.php は全く新しい役割を引き受けることになりました。それは、コアライブラリを読み込み、2つのコントローラ(list_action() と show_action() 関数)のうちの1つを呼び出せるようにアプリケーションをルーティングすることです。実際にこのフロントコントローラは、リクエストを取り扱いルーティングする MVCフレームワークのメカニズムによく似た見た目と動作をし始めています。
Tip フロントコントローラのもう一つの利点が、柔軟性のある URL です。 コードのたった1箇所だけを変更すれば、ブログ単一表示ページの URL を /show から /read に変更できることに注目してください。 以前は、ファイル全体の名前を変更する必要がありましたね。SilexやSlimなどのマイクロフレームワークではさらに柔軟に設定できます。
ここまで、アプリケーションを単一の PHP ファイルから、体系化されてコードの再利用ができる構造へと発展させてきました。これで幸せになれたらいいのですが、現実的に満足からは程遠いものでしょう。例えば、「ルーティング」システムは気まぐれで、一覧表示ページ(/index.php)が / (Apacheのリライトルールが追加されている場合)からでもアクセス可能であるべきだということを認識できません。また、ブログを開発する代わりに、コードの「アーキテクチャ」(例えばルーティングや呼び出すコントローラ、テンプレートなど)にたくさんの時間を費やしています。より多くの時間を、フォームの送信の扱い、入力のバリデーション、ロギングやセキュリティといったことに費やす必要があるでしょう。なぜこれら全てのありふれた問題への解決策を再発明しなければならないのでしょうか?
ライブラリを使って再開発を防ぐ
- Slimに興味がある方は => フラットなPHPからSlimへ
- Silex (Symfony Component と Pimple)に興味がある方はこのまま読み進めてください。
次にこの再開発をしなくて済むように Symfony Component の出番です。
ちょっと Symfony Component の Request と Response に手を出してみる
実際に Silex でWebアプリケーションを開発すると、Symfony Componentのライブラリを使うことになります。まず最初にこれらのライブラリのクラスをどのように見つけるのかを PHP が知っているようにする必要があります。これは、 Composerというパッケージ管理システムを使えば名前空間を利用したオートローダーが簡単に利用できます。これはSilexに限らず、SymfonyやBehatなどのライブラリなどでも同じです。
Composerを使って ResponseとRequesetを使うようにしてみましょう。
まずはComposerをコマンドラインからインストールします。
$ curl -s https://getcomposer.org/installer | php // もし curlがインストールされていない場合は以下でもOK $ php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"
これで、composer.pharというファイルがダウンロードされます。次に、Symfony ComponentのHttpFoundationというRequestやResponseを扱うために用意されたコンポーネントをダウンロードするために、以下のように composer.jsonファイルを用意します。
{ "require": { "symfony/http-foundation": "2.1.x-dev" } }
あとは、コマンドラインでinstallを叩くだけです。
$ php composer.phar install Installing dependencies - Installing symfony/http-foundation (dev-master) Cloning 4ac6d1ef88798fbbdc7600b1859e62403e1f8c97 Writing lock file Generating autoload files
これで、vendorディレクトリが作成され、そこにコンポーネントとautoload.phpが用意されます。
もし、追加で必要なコンポーネントがあれば composer.jsonに追加してインストールが行えます。
requireやuseなどの宣言を bootstrap.phpファイルとしてまとめて、フロントコントローラから読み込むようにしましょう。
<?php // bootstrap.php require_once 'vendor/autoload.php'; require_once 'controllers.php'; require_once 'model.php';
フロントコントローラでHttpFoundationコンポーネントを使うように書き換えてみます。
これまでアプリケーションをどのパスに設置するかを考慮していましたが、HttpFoundationコンポーネントがその部分を吸収してくれています。
<?php // index.php // グローバルライブラリの読み込みと初期化 require 'bootstrap.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; // リクエストを内部的にルーティング $request = Request::createFromGlobals(); $uri = $request->getPathInfo(); if ($uri === '/') { $response = list_action(); } elseif ($uri === '/show' && $request->query->has('id')) { $response = show_action($request->query->get('id')); } else { $html = '<html><body><h1>Page Not Found</h1></body></html>'; $response = new Response($html, 404); } // ヘッダーを返し、レスポンスを送る $response->send();
コントローラは、Response オブジェクトを返す責任を持つようになりました。これを簡単にするために、新しく render_template() 関数を追加しています。ちなみに、この関数は Symfony2 のテンプレートエンジンとちょっと似た動きをします。
この関数には読み込みたいテンプレートのパスと、テンプレートで使用する変数を配列で渡します。
// controllers.php <?php use Symfony\Component\HttpFoundation\Response; function list_action() { $posts = get_all_posts(); $html = render_template('templates/list.php', array('posts' => $posts)); return new Response($html); } function show_action($id) { $post = get_post_by_id($id); $html = render_template('templates/show.php', array('post' => $post)); return new Response($html); } // テンプレートをレンダリングするためのヘルパー関数 function render_template($path, $params) { extract($params, EXTR_SKIP); ob_start(); require $path; $html = ob_get_clean(); return $html; }
Symfony Component の HttpFoundation を使うことによって、アプリケーションはより柔軟で信頼できるものになりました。Request は HTTP リクエストに関する情報にアクセスするための信頼できる仕組みを提供します。具体的にいうと、getPathInfo() メソッドは整理された URI(常に /show で、/index.php/show ではない)を返します。そのため、もしユーザが /index.php/show にアクセスしたとしても、アプリケーションは show_action() によってリクエストをルーティングするインテリジェントさを持っています。
Response オブジェクトは、HTTP ヘッダーとコンテンツをオブジェクト指向のインタフェースを介して追加できるようにすることで、HTTP レスポンスを構成する際に柔軟性を提供しています。そして、アプリケーションのレスポンスがシンプルなために、この柔軟性はアプリケーションが成長するのに大きな利点があるのです。
データベースの設定を外に出す
MySQLが別のデータベースに変更になることはそれほど無いかもしれませんが、別のサーバーで動かすためにデータベース名、ユーザー名、パスワードが変更になるということはよくあることです。さらに、model.phpのテストコードを書こうとすると、テスト用のDB接続に切り替えることができません。これを柔軟に対応する方法を考えましょう。
本格的に複雑なアプリケーションを構築するためには 本格的な DI(Dependency Injection) ライブラリを利用するのですが、ここでは手軽に依存関係を入れておく入れ物(コンテナ)だけを用意してくれる Pimple を利用します。
参照: Pimple - A simple PHP Dependency Injection Container
Pimpleは40行程度しかない小さなライブラリでPHP5.3以降で利用できる無名関数を活用したDIコンテナだけのライブラリです。
Pimpleのインストールは composer.jsonにpimpleを追加し、composer.phar update します。
{ "require": { "symfony/http-foundation": "2.1.x-dev", "pimple/pimple": "1.0.x-dev" } }
$ php composer.phar update
次にconfig.phpを用意し、データベースに関する設定をコンテナ(pimpleオブジェクト)に配列のように追加します。
// pimple <?php $container = new Pimple(); // database $container['db.config'] = array( 'host' => 'localhost', 'database' => 'blog_db', 'user' => 'myuser', 'password' => 'mypassword' );
このconfig.phpをbootstrap.phpで読み込みます。
<?php // bootstrap.php require_once 'vendor/autoload.php'; require_once 'config.php'; <= 追加 require_once 'controllers.php'; require_once 'model.php';
次にフロントコントローラで読み込んだコンテナを渡します。
<?php // index.php // グローバルライブラリの読み込みと初期化 require 'bootstrap.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; // リクエストを内部的にルーティング $request = Request::createFromGlobals(); $uri = $request->getPathInfo(); if ($uri === '/') { $response = list_action($container['db.config']); // <= databaseの設定を渡す } elseif ($uri === '/show' && $request->query->has('id')) { $response = show_action($request->query->get('id'), $container['db.config']); // <= databaseの設定を渡す } else { $html = '<html><body><h1>Page Not Found</h1></body></html>'; $response = new Response($html, 404); } // ヘッダーを返し、レスポンスを送る $response->send();
つぎに、controllers.phpにコンテナを引き渡すための修正を行います。
<?php // controllers.php function list_action($db_config) { $posts = get_all_posts($db_config); $html = render_template('templates/list.php', array('posts' => $posts)); return new Response($html); } function show_action($id, $db_config) { $post = get_post_by_id($id, $db_config); $html = render_template('templates/show.php', array('post' => $post)); return new Response($html); }
最後にコンテナから取得したデータベースの設定情報をmodel.phpで利用できるように修正します。
<?php // model.php function get_database_connection($db_config) { $pdo = new PDO( sprintf('mysql:host=%s;dbname=%s;charset=utf8', $db_config['host'], $db_config['database']), $db_config['user'], $db_config['password'], array(PDO::ATTR_EMULATE_PREPARES => false) ); return $pdo; } function close_database_connection(&$pdo) { $pdo = null; } function get_all_posts($db_config) { $pdo = get_database_connection($db_config); $stmt = $pdo->query('SELECT id, title FROM post'); $posts = array(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $posts[] = $row; } close_database_connection($pdo); return $posts; } function get_post_by_id($id, $db_config) { $pdo = get_database_connection($container); $sth = $pdo->prepare('SELECT id, date, title, body FROM post where id = :id'); $sth->bindValue(':id', $id, PDO::PARAM_INT); $sth->execute(); $post = $sth->fetch(PDO::FETCH_ASSOC); close_database_connection($pdo); return $post; }
これで、model.phpからデータベース設定のハードコーディングを追い出すことができました。しかし、PDOオブジェクトを毎回生成し毎回接続、終了を繰り返している部分が気になります。
そこで、無名関数を利用してサービスコンテナにmodelの関数を登録することを考えてみます。
まず、PDOオブジェクトの取得はPimpleのshareメソッドを利用して登録します。shareを使うことで何度呼ばれても同じPDOオブジェクトが返されます。
<?php // model.php $container['db.pdo'] = $container->share(function($c) { $db_config = $c['db.config']; $pdo = new PDO( sprintf('mysql:host=%s;dbname=%s;charset=utf8', $db_config['host'], $db_config['database']), $db_config['user'], $db_config['password'], array(PDO::ATTR_EMULATE_PREPARES => false) ); return $pdo; });
shareメソッドに渡す無名関数は引数としてコンテナ自身が渡されるので、内部でコンテナで定義したデータを利用することができます。以降は無名関数内で参照するコンテナ自身はPimpleオブジェクトである$containerと混乱しないように$cという名前(containerのc)で使うようにしています。
次に、get_all_posts関数を無名関数として登録してみましょう。Pimpleに無名関数を登録すると引数に自身のオブジェクトが渡されるので、これを利用してPDOオブジェクトの取得を行なっています。最初のget_all_posts関数より読みやすくなりましたね
<?php // model.php $container['model.all_posts'] = function($c) { $stmt = $c['db.pdo']->query('SELECT id, title FROM post'); $posts = array(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $posts[] = $row; } return $posts; };
これを呼ぶlist_action関数をあわせて修正します。
<?php // controllers.php function list_action($container) { $posts = $container['model.all_posts']; // <= コンテナから無名関数を実行 $html = render_template('templates/list.php', array('posts' => $posts)); return new Response($html); }
最後にget_post_by_id関数も登録します。普通に無名関数を登録しても、引数はコンテナ自身しか渡らないため、protectメソッドを使って$idを引数として渡すことができる無名関数を登録します。また、この場合コンテナ自身を無名関数の内部で利用できるようにするためにuseを使ってコンテナ自身を渡します。
<?php // model.php $container['model.post_by_id'] = $container->protect(function($id) use ($container) { $sth = $container['db.pdo']->prepare('SELECT id, date, title, body FROM post where id = :id'); $sth->bindValue(':id', $id, PDO::PARAM_INT); $sth->execute(); $post = $sth->fetch(PDO::FETCH_ASSOC); return $post; });
これを呼ぶshow_action関数も修正します。
<?php //controllers.php function show_action($id, $container) { $get_post_by_id = $container['model.post_by_id']; // <= コンテナから無名関数を取得 $post = $get_post_by_id($id); // <= 無名関数を引数$idを渡して実行 $html = render_template('templates/show.php', array('post' => $post)); return new Response($html); }
Pimpleで用意したコンテナを利用するように書き換えたことで、テスト時にPDOをテスト用のPDO_TESTに変更したいとしたい場合でもコンテナの内容を変更すれば簡単に差し替えることができるようになりました。
Note. ここではPimpleを利用してmodelを実装していますが、このようにコンテナとしてPimpleにあらゆるものをただ入れていくとすべての処理がPimpleに依存してしまいます。 このようにサービスロケータとしてPimpleを使うシンプルさだけに目を奪われず、本当に必要な知識を適切な場所に依存させるということも考えましょう
Silexで書き換える
ここまでの、Webアプリケーション開発を通してどのようにコードを分離してきたかを整理してみましょう。
アプリケーションへのアクセスとは"どのURIに、どのリクエストメソッドで、どのパラメータをもってアクセスされるか"ということです。
つまり、ルーティングによってどの処理を行うかが決定されるということだけなのです。
この部分に注目したのが、マイクロフレームワークです。マイクロフレームワークではルーティングごとに処理を定義するだけです。
では、これまで書いてきたコードをSilexで書き換えてみます。
まず、Silexをインストールします。composer.jsonを以下に書き換えてupdateするだけです。
{ "require": { "silex/silex": "1.0.*" }, "minimum-stability": "dev" }
$ php composer.phar update
注目すべきは、vendor以下にsilexとその依存するコードがインストールされますが、これまで書いてきたコードはSilexと同じライブラリを使っているので、全くコードを修正しなくても今の状態でサンプルコードは動くということです。
では、本格的にSilexに書き換えていきます。
最初の一歩として1ファイルでとりあえず書いてみます。
とはいえ、難しくありません。完成形のコードを見てみましょう
<?php //index.php require_once __DIR__.'/vendor/autoload.php'; use Silex\Application; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $app = new Silex\Application(); //config.php'; $app['db.config'] = array( 'host' => 'localhost', 'database' => 'blog_db', 'user' => 'myuser', 'password' => 'mypassword' ); //model.php'; $app['db.pdo'] = $app->share(function($c) { $db_config = $c['db.config']; $pdo = new PDO( sprintf('mysql:host=%s;dbname=%s;charset=utf8', $db_config['host'], $db_config['database']), $db_config['user'], $db_config['password'], array(PDO::ATTR_EMULATE_PREPARES => false) ); return $pdo; }); $app['model.all_posts'] = function($c) { $stmt = $c['db.pdo']->query('SELECT id, title FROM post'); $posts = array(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $posts[] = $row; } return $posts; }; $app['model.post_by_id'] = $app->protect(function($id) use ($app) { $sth = $app['db.pdo']->prepare('SELECT id, date, title, body FROM post where id = :id'); $sth->bindValue(':id', $id, PDO::PARAM_INT); $sth->execute(); $post = $sth->fetch(PDO::FETCH_ASSOC); return $post; }); // controllers.php $app->get('/', function(Application $app, Request $request) { $posts = $app['model.all_posts']; $html = render_template('templates/list.php', array('posts' => $posts)); return $html; }); $app->get('/show', function(Application $app, Request $request) { $get_post_by_id = $app['model.post_by_id']; $post = $get_post_by_id($request->query->get('id')); if (!$post) { $app->abort(404); } $html = render_template('templates/show.php', array('post' => $post)); return $html; }); $app->error(function (\Exception $e, $code) { $html = '<html><body><h1>ページが見つかりません</h1></body></html>'; return new Response($html, $code); }); $app->run(); // テンプレートをレンダリングするためのヘルパー関数 function render_template($path, $params) { extract($params, EXTR_SKIP); ob_start(); require $path; $html = ob_get_clean(); return $html; }
1ファイルで書いていても、それなりに読みやすいとおもいます。
Silex化する前とのコードの違いはコンテナ($container)はSilexではSilexそのものがコンテナになっているため$appに書き換えている点とルーティングごとに処理を無名関数で登録している点です。
無名関数の内容は最初とほとんど変わっていません。戻り値がResponseオブジェクトを指定しなくても、ブラウザに返却したい文字列を返しているというぐらいです。
また、記事詳細を表示するときに該当するIDで記事が存在しなかった場合の処理もSilexが提供するabortメソッドで404として簡単に実装できていることもわかりますね。
あとは、適切にファイルに分けてrequireすれば良いのですが、テンプレートをレンダリングする処理が微妙な感じです。これだけグローバル関数として存在しています。テンプレートエンジンに置き換えることも簡単ですが、まずはにコンテナに無名関数として閉じ込めてしまいましょう。
<?php // template $app['template.render'] = $app->protect(function($path, $params) { extract($params, EXTR_SKIP); ob_start(); require $path; $html = ob_get_clean(); return $html; }); // controllers.php $app->get('/', function(Application $app, Request $request) { $posts = $app['model.all_posts']; $render = $app['template.render']; $html = $render('templates/list.php', array('posts' => $posts)); return $html; }); $app->get('/show', function(Application $app, Request $request) { $get_post_by_id = $app['model.post_by_id']; $post = $get_post_by_id($request->query->get('id')); if (!$post) { $app->abort(404); } $render = $app['template.render']; $html = $render('templates/show.php', array('post' => $post)); return $html; }); $app->error(function (\Exception $e, $code) { $html = '<html><body><h1>ページが見つかりません</h1></body></html>'; return new Response($html, $code); });
あと、もう少しです。ファイルを分割してみてください。
そうするとフロントコントローラは以下のようになるはずです
<?php //index.php require_once __DIR__.'/vendor/autoload.php'; $app = new Silex\Application(); require __DIR__.'/config.php'; require __DIR__.'/model.php'; require __DIR__.'/controllers.php'; $app->run();
シンプルになりましたね!
テンプレートエンジン Twig を使う
あと気になるところはどこでしょうか。それは、テンプレート部分の記述がフラットなPHPで実現しているため汚いというところです。Silex では Twig というとてもエレガントなテンプレートエンジンを簡単に導入できるようになっています。そこで Twig でテンプレートを書き換えてみましょう
参照:
- Home - Twig - The flexible, fast, and secure PHP template engine
- Documentation - Twig - The flexible, fast, and secure PHP template engine
Twig のインストール
PimpleやSilexと同じでcomposerで簡単にインストールしましょう。
以下のようにcomposer.jsonに追記します。
また、Twigに幾つかメソッドを追加するために、Symfony Componentのtwig-bridgeも入れておきます。(これでテンプレートでpath、urlというメソッドを使うことができるようになります)
{ "require": { "silex/silex": "1.0.*", "twig/twig": "1.*", <= 追加 "symfony/twig-bridge": "2.1.*" <= 追加 }, "minimum-stability": "dev" }
あとはいつもの様にupdateを行います。
$ php composer.phar update
次にSilexにあるプロバイダーという拡張機能で、Twigを利用する準備を行います。具体的には以下のようなregisterメソッドで TwigServiceProviderとUrlGeneratorServiceProviderを登録します。
UrlGeneratorServiceProviderはTwigとは直接関係ありませんが後々必ず使うことになるので入れておきましょう。
<?php .... // twig $app->register(new Silex\Provider\TwigServiceProvider(), array( 'twig.path' => __DIR__.'/templates', )); $app->register(new Silex\Provider\UrlGeneratorServiceProvider());
ルーティングのレンダラーをTwigに変更する
次に一覧表示の処理をTwigを使うように変えてみましょう。
<?php // controllers.php $app->get('/', function(Application $app, Request $request) { $get_all_posts = $app['model.all_posts']; $posts = $get_all_posts; $render = $app['template.render']; return $app['twig']->render('list.html.twig', array('posts' => $posts)); //<= $app['twig']に変更 });
テンプレートエンジンを変えただけなので、Viewの部分をTwigに変えただけです。
同じように、詳細表示のコントローラーも変更します。
<?php $app->get('/show/{id}', function($id, Application $app, Request $request) { //<= idをパスから取得 $get_post_by_id = $app['model.post_by_id']; $post = $get_post_by_id($id); if (!$post) { $app->abort(404); } return $app['twig']->render('show.html.twig', array('post' => $post)); // <= $app['twig']に変更 }) ->bind('blog_show'); // <= このルーティングに'blog_show'という名前をつける
詳細表示側もTwigを使うように変更しました。また詳細表示へのリンクをテンプレートで行うときに、詳細表示のURLをハードコーディングしていたものを'blog_show'という名前をつけることで参照できるようにします。これがbindメソッド部分です。そしてこの機能がさきほど追加したUrlGeneratorServiceProviderの機能です。
また、前回までは記事idはGETパラメータで取得するようにしていましたが、Silexなどのマイクロフレームワークではパスから自由にパラメータとして取得することが簡単に記述できるようになっています。Silexの場合は{id}のように括弧でパラメータ名を指定することで無名関数で$idとして取得することができます。
これで、記事詳細のパスは /show/xxx と定義したことになり、xxxの部分を$idとして内部で扱うことができます。
テンプレートの作成 layout.html.twig
次にテンプレートを修正します。これまで利用してきたlayout.phpに対応するlayout.html.twigを作成します。
Twigではテンプレートの継承が行えます。つまり、このlayoutを継承したテンプレートを用意し、継承先で書き換えたい継承元の一部だけをコーディングすれば良いということになります。では、具体的に見てみましょう。
<!-- layout.html.twig //--> <!doctype html> <html> <head> <title>{% block title %}Default title{% endblock %}</title> </head> <body> {% block body %}{% endblock %} </body> </html>
Twigでは継承できる部分を {% block 名前 %}デフォルトの値{% endblock %} と定義します。
このレイアウトでは title と body ブロックが定義されています。これを継承したテンプレートで title と body を定義すればOKということです。
list.html.twig
次に一覧表示のテンプレートを用意しましょう。
<!-- list.html.twig //--> {% extends "layout.html.twig" %} {% block title %}投稿のリスト{% endblock %} {% block body %} <h1>投稿のリスト</h1> <ul> {% for post in posts %} <li> <a href="{{path('blog_show', {'id': post.id}) }}"> {{ post.title }} </a> </li> {% endfor %} </ul> {% endblock %}
まず最初にどのレイアウトを継承するかという記述があります。これは extends で指定します。もし、異なるレイアウトを使いたい場合はここで新しいレイアウトファイルを指定するだけでレイアウトを変えることができます。
あとは上書きしたいblockを記述していくだけです。Twigの構文では{{ xxx }} で変数をエスケープしたものを出力できるのでとても読みやすいテンプレートになったことがわかります。また、オブジェクトのプロパティへや配列のアクセスも {{post.title}} のように記述できるのが特徴です。
また、さきほどのコントローラーでblog_showという名前をつけたパスをIDのパラメータを指定しつつテンプレートに埋め込むために、pathというメソッドで指定しています。これで、パスをハードコーディングする必要もありません。コントローラー側でURIが変わったとしてもルーティングにつけた名前が同じである限り自動的に解決してくれます。この機能がcomposerで追加したTwigBridgeコンポーエントが提供している機能です。
よくみると、ループ処理も for in というTwigの構文で書かれていたり、pathメソッドでのIDの指定の仕方がjson方式だったりとフラットなPHPとは異なる部分が多いですが、フラットなPHPよりも書きやすく読みやすいというのがわかっていただけるかと思います。
ここではshow.html.twigで用意したコードは書いていませんが、簡単ですので実際にテンプレートを用意して表示を試してみてください。
最後に
フラットなPHPからSilexに変化していく様子を見てきました。なぜフレームワークが便利なのかというのが見えてきたのではないでしょうか?とはいえ、フレームワークは銀の弾丸ではありません。このようにルーティングの処理は任せて、本来コーディングしたい部分に集中できるように助けてくれます。
今回はSilexで説明してきましたが、Silexには他にも良い感じの機能を提供してくれています。たとえばサービスプロバイダという拡張機能が用意されているため、テンプレートエンジンをTwigに変えたり、PDOではなくDoctrineのDBALを使うというのも簡単にできます。
参照:
また、Silexは小規模で複雑でないアプリケーションを開発するときには悩むことはそれほどありませんが、ある程度の規模や人数での開発になってくるばあいはそのために考慮したり開発を行う部分が増えてきます。そのため、Symfonyなどのフルスタックフレームワークで開発することをお勧めします。
これまで理解した知識を活かしつつ、さらにしっかりとした枠組み(フレームワーク)で開発が行うことができます。
次はSlim版も書く予定。たぶんこれよりももっと薄い内容になるはず。。疲れた書いた。