フラットな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版も書く予定。たぶんこれよりももっと薄い内容になるはず。。疲れた書いた。
これからのSilexのインストール方法
七夕ですね。BEAR.Sundayが楽しいですね。詳細についてはまだ発表されていませんが7/19(木)についに BEAR.Sunday Meetup #0 が開催されるようですよ。
今日はSilexのインストール方法について色々変更があったのでまとめておきます。
silex.pharは配布しないぜよ
詳しくはダウンロードページにまとめられていますが、これまで silex.phar の1ファイルをDLするだけ!が特徴だったのですが、これは今後配布されなくなります。
1ファイルだけで管理できるというのが大きかったのですが、それよりもデメリットのほうが大きいという判断だと思います。
たとえば、実行速度。pharファイルはアーカイブなので展開処理分オーバーヘッドです。以前試したところでは、Hello Worldを表示するレベルでもpharを使わなければ140%ぐらいのリクエスト処理数になりました。
あと、コア開発者側としては、pharを動かせずにissueやMLにいつも質問が飛び交っていてサポートするのが大変というのもあったかもしれません。
事実、pharに入っているSymfony Componentのバージョンが上がるだけでSilexのコアが変更されていなくてもsilex.pharも更新しなければならず面倒というのもあったと思います。
この問題はこれから説明するcomposerによるインストールでいっきに解決します。
とりあえず Silex のコードだけを入れる方法
composerはパッケージの依存関係を管理する方法です。最小構成のSilexをインストールするには以下のcomposer.jsonというファイルを用意します。
composer.json
{ "require": { "silex/silex": "1.0.*" } }
そして、composer.pharをダウンロードしてきて、installを叩くだけ。
$ curl -s http://getcomposer.org/installer | php $ php composer.phar install
かんたんですよね。もし、twigも使うという場合は
{ "require": { "silex/silex": "1.0.*", "twig/twig": ">=1.8.0,<2.0-dev" } }
とtwig/twigの1行を追加し
$ php composer.phar update
を叩くだけ。これで最新のstable版が追加でインストールされます。
いままでのことを考えるとかなり便利だと思います。
本格的にSilexを使うためのたったひとつの方法
この記事を書いたのはこの方法について知って欲しかったからなのです。
Silexである程度大きなアプリを作るとなると、コントローラーを別ファイルにしたり、ロガー欲しいよねとか、いろいろあるわけですが、このあたりの準備となるディレクトリ構成などは自分で考えなくてはなりませんでした。もしくは、Silex-Kitchen-Editionという全部入りのコードをgithubからダウンロードしてくるというのが一般的でした。
大丈夫です。これからは良い感じのスケルトンが用意されました。
何も考えずに以下のコマンドを打てば作業は終わりです。
$ php composer.phar create-project fabpot/silex-skeleton ./silex $ mkdir ./silex/silex.log ./silex/cache $ chmod 0777 ./silex/silex.log ./silex/cache
これでコマンドを打ったディレクトリにsilexというディレクトリが作成され、そこにsilexのスケルトンファイルが用意されます。実際にはdoctrine/dbal以外はすべてインストールされます。
これだけでも便利ですよね。
あとはweb/index.phpがプロダクション環境用、web/index_dev.phpが開発用のコントローラになっているのはSymfony2っぽい感じですね。
また、コンソールから叩くための準備もされていて
$ php console
を叩くとmy-commandがサンプルとして用意されているのが見えます。
このスケルトンを利用すれば、最初の開発するまでの手順はかなり短縮化できると思います。
あとは composerがもう少し安定してくれれば...
おまけ
doctrine/dbalも使いたい場合は composer.json に
{ "name": "fabpot/silex-skeleton", "require": { "php": ">=5.3.3", "silex/silex": "1.0.*", "twig/twig": ">=1.8.0,<2.0-dev", "monolog/monolog": ">=1.0.0,<1.2-dev", "symfony/browser-kit": "2.1.*", "symfony/class-loader": "2.1.*", "symfony/config": "2.1.*", "symfony/console": "2.1.*", "symfony/css-selector": "2.1.*", "symfony/finder": "2.1.*", "symfony/form": "2.1.*", "symfony/monolog-bridge": "2.1.*", "symfony/process": "2.1.*", "symfony/security": "2.1.*", "symfony/translation": "2.1.*", "symfony/twig-bridge": "2.1.*", "symfony/validator": "2.1.*", "doctrine/dbal": "2.3.*" }, "minimum-stability": "dev", "autoload": { "psr-0": { "": "src/" } } }
と、最後にdoctrine/dbalを追加すればOKですよ。
Symfony勉強会 #6 が無事終了!!
無事開催できました。
2012/6/30に VOYAGE GROUP にて Symfony勉強会#6 が開催されました。
レポートはユーザー会にもアップされる予定です。アップされました。
http://www.symfony.gr.jp/blog/20120630-symfony2-workshop6-report
それに、参加者のみなさんのブログ記事もアップされはじめているので、詳細はそちらをみていただくとして。。。
今回は自分が発起人として動いていたということもあり疲労感はかなりなものでした。
勉強会後のSymfony ミッドナイトでは最後まで起きていることすらできませんでした。。
なんといっても、勉強会は参加者も含めて皆の協力があってこそなのでこの場をお借りしてスタッフの皆さん、参加者の皆さんに本当にお礼申し上げます。
Symfony勉強会は回を増すごとに濃くなっているという事実
ユーザー会主催としてのSymfony勉強会は今回で6回目だったのですが、おそらく今回は過去最高の濃い内容でした。
なんといっても、開催時間が長かった。。10:30から懇親会(と言うなの特別セッションやLT)で11:30という13時間コースでした。
ただセッションを聞くだけでなく、ワークショップも3時間ありましたし、参加された皆さんも気を抜く時間はほとんどなかったんじゃないでしょうか。。
学ぶということ
また、ただ勉強会をやるというのではなく、今回はテーマを決めました。
"フレームワークに縛られない技術とそれを実践している一歩先ゆくエンジニアたちの声"
そして、実際に参加者の人たちにはこのテーマを体感できたんじゃないかと思います。
あの勉強会では自分自身も多くのことを学びました。正直、勉強会の内容を100%なんかとてもじゃないですが理解できてません。
考えてみれば当たり前ですよね。たった1日話を聞いただけで身につくわけがないので。なので、これをきっかけに見えた色々な自分の足らない部分を把握し前に一歩踏み出し、学び続けることが大事じゃないかなと思っています。
ポシャった幻のセッション
今月フランスで Symfony Live Paris 2012 が開催されていました。去年のSymfony Live後はファビアンさんのKeynoteの動画がアップされたりしたので今年もあるだろうと推測し、その動画に某スタッフTシャツをデザインしてくれた広島人にappleのアレっぽく吹き替えしてもらおうと考えてました。
これは残念ながら動画が公開されずボツに....
BEAR.Sunday勉強会!?
当日強引に@nekogetさんにLTを振るという結果、BEAR.Sundayの勉強会の話が進みそうです。よかったよかった。
懇親会セッションに参加された方は@koriymのあのホワイトボードの白熱教室の凄さを覚えてますよね。(油性ペンだったという話はまた違う凄さですが)
スライドは11枚。
http://www.slideshare.net/akihito.koriyama/bearsunday-offline-talk-1
このスライドで1時間以上使って進んだ枚数が5枚!!
あの続きが気になってしょうがないですし、参加するしかありません。
もういまからwktkです。
最後に
勉強会終了時に「参加してよかった人」との質問に、皆が元気よく手を挙げてくれたのに感動しました。
次回の開催は未定ですが、次回は皆が「LTしたい!!」ぐらいの勉強会にしたいですね!
おまけ
今日ファビアンさんが色々つぶやいていました。
phpBB, Drupal が Symfony Componentを使うことは周知の事実として...
eZPublishとPHP-NukeのフォークのZikulaも Symfony2ベースに。
#eZPublish 5 is going to use the #Symfony full-stack framework. That is an awesome news. symfony.com/blog/symfony2-…
— Fabien Potencierさん (@fabpot) 7月 2, 2012
PHP-Nuke was forked to Post-Nuke. Post-Nuke was renamed to Zikula. Next version of #Zikula is based on #Symfony2 full-stack. QED
— Fabien Potencierさん (@fabpot) 7月 2, 2012
また、eZが Symfony2 を選んだ理由として
After benchmarking the available open-source PHP frameworks, the indisputable winner was the Symfony framework & community.
色々なPHPのフレームワークのベンチマークを行った結果、議論の余地もなく勝者は Symfony とそのコミュニティーだった
via: http://symfony.com/blog/symfony2-meets-ez-publish-5
Symfony の世界的な勢いがすごいですね。
OrePHPはこれでさらに高速になった(OrePhalcon)
前回、OrePHPはシンプルで速いクールなフレームワーク - ぷぎがぽぎという内容の記事を書きましたが、これぐらいの速度で満足してはいけないということで更に高速化を考えました。
Cのエクステンションで書かれた超高速PHPフレームワーク Phalcon
Phalconというフレームワークをご存知でしょうか?フレームワークをCのエクステンションで書いちゃった超高速PHPフレームワークです。
最速フレームワークと宣言してあるだけのことがあるベンチマーク結果が以下のグラフです。
前回のベンチマークをも超越しそうな結果です。というわけで、早速HelloWorldでOrePHPと比べてみます。
まずはPhalconをインストール。
エクステンションなのでgitからcloneしてきたものをいつもどおりphpnizeしてmake。
あとはphp.iniに
extension=phalcon.so
を追加。
環境はPHP5.4 + nginx なので
nginxの設定に以下を追加
location / { if (!-e $request_filename) { rewrite ^/(.+)$ /index.php?_url=$1 last; break; } }
_urlというパラメータで渡すのを忘れずに。
準備はできたのでさっそくベンチ。
name | trans/sec | relative(%) |
---|---|---|
OrePHP | 484.76 | 100% |
Phalcon | 738.94 | 152% |
はい。簡単に負けましたw
最速と自負するだけのことはあります。
じゃあ、Phalconをベースにすればいいじゃまいか
OrePHPではSymfony ComponentsのRoutingコンポーネントを使っていましたが、たぶんこれがCで書かれていないから負けるんだということで、Phalconのルーティングを使うようにしてOrePhalconを作ってみました。
<?php ini_set('display_errors', 1); error_reporting(-1); $router = new Phalcon_Router_Regex(); $router->setBaseUri('/phalcon'); $router->add("hello/(\w+)", array( "controller" => "hello", "action" => "say", "name" => 1, )); $router->handle(); $controller = $router->getControllerName(); $action = $router->getActionName(); $params = $router->getParams(); try{ $controllerFilePath = __DIR__ . '/../app/controllers/' . $controller . ".php"; if (!file_exists($controllerFilePath)) { throw new Exception("controller file is not found"); } require $controllerFilePath; } catch (Exception $e) { echo $e->getMessage(); exit; } $class = ucfirst($controller); echo $class::$action($params);
今回は最速を目指すのでフレームワークとしてコアファイルはこのファイル1つです。
フレームワークのコアクラスはエクステンションで書かれているのでrequireすら不要です。
あとは、実際に呼び出されるコントローラーファイルをHello.phpとして用意します。
<?php class Hello { public static function say($params) { return "Hello " . $params['name']; } }
OrePhalconは最速になれたのか?
name | trans/sec | relative(%) |
---|---|---|
OrePHP | 484.76 | 100% |
Phalcon | 738.94 | 152% |
OrePhalcon | 923.60 | 191% |
はい。最速でした。
Phalconほんと速いですね。
composerでPHPのプロジェクトを簡単に始める
composerとはなんぞや?
composerはPHPのパッケージの依存関係やバージョン管理を行う仕組みで多くのライブラリがPackagistを通して利用可能です。
詳しいcomposerの記事は以下を参照していただくとして...
composerのautoloadを利用する
composerはパッケージ管理なので、別に公開することが前提ではありません。プライベートなプロジェクトでも普通に使うことができます。
composerを使えば、PSR-0に準拠した名前空間のautoloadも用意されていて便利です。準備に必要なのは以下の作業だけ
composerをインストール
composer.pharをダウンロードしてきます。グローバルではなく個々のプロジェクト毎にインストールできます
$ curl -s http://getcomposer.org/installer | php
さくっとプロジェクトのひな形作成
composer.jsonを作成する必要がありますが、通常はコマンドラインからひな形が作成できますが、欲しいのはpackagistに登録するためではなく自分用の名前空間の登録だけです。
というわけで以下をcomposer.jsonとしてコピペ
{ "repositories": [ { "packagist": false } ], "autoload": { "psr-0": {"Oreore": "src"} } }
Oreoreの部分はあなたのベンダー名をつけてくださいね。
あとは
$ php composer.phar install
するだけで、自前のライブラリは./src/Oreore/Hoge.phpのように書けばautoloadに対応できます。
composerは"packagist": falseを書かないと標準でリポジトリとしてPackagistを見に行こうとするようです。ただ自前のディレクトリ構造を用意したいだけなのにこのPackagistへのアクセスのためにインストールに時間がかかってしまいます。1分以上掛かることもあります。というわけでこの対策のためにfalseを書いています。これを書いておけば1秒も掛かりません。
OrePHPはシンプルで速いクールなフレームワーク
[追記1] 2012-06-10: ベンチマークを追加
[追記2] ブクマのコメントに回答
[追記3] ベンチマークをちょっと充実させた。Pinocoはえー
[追記4] コントローラーの仕組みを変更 & debugモード追加
[追記5] PHP5.4.4で再ベンチ
"ぼくがかんがえたさいきょうのふれーむわーく"ではないですが、OrePHPというPHP Webアプリケーション フレームワークを1つ書いてみた。
GitHub - brtriver/orephp: simple and fast PHP web application routing framework
こんせぷと
フレームワークが提供するのはルーティングだけ。シンプルに。速く。
ぼくがほしいのは、るーてぃんぐ
素のPHPでWebアプリケーションを書きたくない理由の1つがルーティングを用意するのが面倒というのがあります。Symfonyだとyaml、php、xmlなどで定義できて結構便利です。つまり、自分が欲しいのはルーティングだけなんだというのは結構多いような気がします。というわけで、Symfony Component Routingを利用できるようにしただけのフレームワークです。なので、コアコードは70行ちょっと。
るーてぃんぐ・ふれーむわーく
仕組みは簡単。ルーティングの定義をconfig/routing.yaml に書きます。
書き方はSymfonyとほぼ同じ。どのコントローラーを呼ぶかを定義すればOK。
hello: pattern: /hello/{name} defaults: { controller: 'hello' }
これだと/hello/hogeでアクセスされると app/controller/hello.phpがincludeされるというだけ。
あとは、hello.phpでやりたいことを書いて、returnでブラウザに返したい文字列を書くだけ。
ここで、hello.phpで無名関数をreturnするようにします。そして無名関数内のreturnでブラウザに返したい文字列を書くだけ。
<?php return function($request, $params, $container) { $now = date('Y-m-d H:i:s'); return "Hello world " . $params['name']; };
見ての通りクラスじゃないです。普通のPHPファイルです。よくMVCフレームワークだとアクションという概念がありますが、これには無いです。コントローラーだけです。
無名関数に渡される引数は、リクエストオブジェクト(Symfony2と同じ)、ルーティングで取得したパラメータ、DIコンテナ(Pimple)です。
ただ、returnで文字列返すのは面倒だ。やっぱりTwigぐらい使いたいってのはありますよね。その場合は次のように書けばOK
<?php return function($request, $params, $container) { $now = date('Y-m-d H:i:s'); return $container['tpl']->render('hello.html', array('now' => $now, 'name' => $params['name'])); };
$container['tpl']というのがTwigのインスタンスになってるので、この場合はだとapp/views/hello.htmlがtwigのテンプレートとして処理されます。簡単ですね。また、Twigのインスタンスは遅延読込になっているので呼ばない限り読込すらしません。このあたりはPimpleというDIコンテナを使ってよしなにやってます。つまり、Pimpleのインスタンスが$containerですね。
ふつうにはやい
一番重い処理はrouting.yamlの解析だと思いますが、一度解析するとcache/ProjectUrlMatcher.php にキャッシュし、2度目以降は解析しません。普通にPHPのクラスファイルを読み込むだけになるので速いです。ということで、routing.yamlを書き換えたらキャッシュされているファイルを手動で削除しなくちゃいけません。
そうしないと、変更後の内容が反映されないので。
また、デバッグモードを追加しました。
index.phpで
$app->c['debug'] = true;
と追記してデバッグモードを有効にすれば毎アクセスごとにrouting.yamlをパースします。開発時には有効にしておくと便利です。
(追記)簡単なHello World的なベンチマーク結果は以下のとおり。PHP5.4.3(php-fpm)+apc+nginxのvmにsiegeで計測。あくまでオレ環境なので参考程度で。
(追記2)FuelPHPを再計測
(追記3)PHP5.4.4で再ベンチ
siege -b -c 10 -t 3S http://xxxx/xxx で検証
name | template engine | trans/sec | result(relative) |
---|---|---|---|
FuelPHP 1.2 | Twig | 130.04 | 63% |
FuelPHP 1.2 | - | 169.08 | 82% |
Silex | Twig | 170.63 | 83% |
Silex | - | 205.61 | 100% |
OrePHP | Twig | 342.89 | 167% |
Pinoco(dev-master) | PHPTAL | 352.11 | 171% |
CodeIgniter 2.1 | - | 373.02 | 181% |
Pinoco(dev-master) | - | 429.84 | 209% |
OrePHP | - | 484.76 | 236% |
SilexのTwig無バージョンを基準としてみると、2倍ぐらいのレスポンスは叩きだせますね。わーい。
でーたべーすとかは?
なにそれ?おいしいの?
せっていふぁいるとかは?
routing.yamlだけあります。ほかは$app->cがDIコンテナなのでよしなにやってね。web/index.phpでTwigをほり込んでるあたりが分かりやすいかと。ちなみにindex.php全体でもたったこれだけ。
<?php require __DIR__ . '/../vendor/autoload.php'; $app = new Ore\Framework; $app->c['tpl'] = $app->c->share(function($c){ $loader = new Twig_Loader_Filesystem($c['base_dir'] . '/app/views'); return new Twig_Environment($loader,array( 'cache' => $c['cache_dir'], )); }); // $app->c['debug'] = true; $app->display();
つかいかた
githubからgit cloneなりダウンロードしてきたら
$ php composer.phar install
で外部ライブラリをインストールするだけ。
cacheディレクトリは実行者が書き込めるように0777にしておきましょう。
あとは、ブラウザからhttp://example.com/hello/orephpにアクセスすればOK
コメントありがとうございます
以下のコメントをいただきました。ありがとうございますm(__)m
なぜPHPを直書きさせてないのか?について
- yaml じゃなくて素の PHP の方が速くなりそうなんだけど、どうなんだろう。 by id:heavenshellさん
- キャッシュを手動削除する手間が必要なら、素直にPHP直書きさせたらいいのに by id:terurouさん
キャッシュを利用しているのは速くなるからなのですが、yamlよりPHPの方がと考えが浮かぶのはわかります。しかし、実際にどのようにキャッシュされているかを見ると納得してもらえるのではないかと...。
routing.ymlで以下のように定義したファイルは
hello: pattern: /hello/{name} defaults: { controller: 'hello' } default: pattern: /{controller} defaults: { controller: controller }
以下のような cache/ProjectUrlMatcher.php に変換されキャッシュされます。
<?php use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; /** * ProjectUrlMatcher * * This class has been auto-generated * by the Symfony Routing Component. */ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher { /** * Constructor. */ public function __construct(RequestContext $context) { $this->context = $context; } public function match($pathinfo) { $allow = array(); $pathinfo = rawurldecode($pathinfo); // hello if (0 === strpos($pathinfo, '/hello') && preg_match('#^/hello/(?<name>[^/]+)$#s', $pathinfo, $matches)) { return array_merge($this->mergeDefaults($matches, array ( 'controller' => 'hello',)), array('_route' => 'hello')); } // default if (preg_match('#^/(?<controller>[^/]+)?$#s', $pathinfo, $matches)) { return array_merge($this->mergeDefaults($matches, array ( 'controller' => 'controller',)), array('_route' => 'default')); } throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); } }
なんとなくyamlファイルがただパースされてPHPの配列になると想像する方もいるかもしれませんが、Symfony Component Routingはこのように、DSLで複雑なPHPコードを抽象化し記述できるようにしてくれています。なので、今回はyamlを選択していますが、xmlでもphpの配列でも定義することができ、最終的には同じPHPコードに変換されます。さすがにこのクラスコードをルーティングを追加、編集するたびに書けというのは辛いですよね。
参考: ドメイン特化言語とモデル駆動エンジニアリング - Johan den Haan - Digital Romanticism
とはいえ、開発用にキャッシュしないオプションがあったほうがいいですよね。
index.phpでデバッグモードを有効にしている場合はキャッシュを利用せずに毎回パースするようにしました。
$cとか突然でてくるのキモいよね
- $cとか突如出現するの怖いし、$thisにできないですかね。(再代入防げるし)by id:escape_artistさん
はい。キモイですよね。うん。キモイとおもいます。
まだまだ改良の余地はありますよね。せめてクラスメソッドで閉じ込めるようにしたいと思います。
改良し、無名関数に閉じ込めました。
オレオレフレームワーク作るの楽しいですね。
プロとしてWebアプリケーションを開発するということ
クロコスのエンジニアでもあり、日本Symfonyユーザー会のメンバーでもある小川さん([twitter:@fivestr])が執筆した最新作「効率的なWebアプリケーションの作り方 -PHPによるモダン開発入門-」を献本いただいたので紹介したいと思います。
最新の技術を追っているわけではなく、実績ある技術や手法の基礎編
サブタイトルに「モダン」という言葉があってPHPでの最先端な開発かと感じる方がいるかもしれませんが、内容はOOP、DI、そしてドメイン駆動開発などの基礎的なところを実例を通して紹介しています。つまり、これから5年後または10年後と利用することができる実績ある技術と手法について紹介していることが特徴だと感じました。*1
Webアプリケーションをプロとして開発するために必要な知識を理解するための一歩
PHPの書籍というと、PHPの構文などPHPの使い方にフォーカスした本、またはWordpress、EC-Cube、CakePHPなど特定のアプリケーションやフレームワークに特化した書籍は本屋でよく見かけると思います。しかし、Webアプリケーションを設計、開発するために必要な知識はそういった説明をPHPでしっかりとしてくれている日本語で読むことができる書籍はほとんど知りません。実際、そういった知識を得るためにはTDD、DDD、デザパタなどの世界へ足を踏み入れる必要がありますが、これらが敷居が高いと感じる方が多いのではないでしょうか?そうでない方は次のような本は既に読んでると思いますが...
洋書だとこんなのあるんですけどね...
「説明が浅すぎるかな?」と感じる部分はありますが、今まで一歩目を踏み出すことができなかった方も、この本でその世界に触れることができ理解を深めるための第一歩が踏み出せると思うととても良いバランス配分だと思います。ただ、決して内容は簡単ではありません。かなりの時間と努力が必要になると思います。
Symfony本であってSymfony本ではない
もう一つ大切な側面として、日本語で読むことができるSymfony2系の解説本という側面もあります。本書ではレンタカーアプリケーションの開発例を通して具体的な開発方法を説明するというかなりガッツリな内容になっています。ただし、本を開かない限りSymfonyだということを意識させない紙面構成になっています。なぜなら、フレームワークに特化した使い方の説明がメインではなく、フレームワークに依存しない知識や技術がメインだからです。もっというと、PHP以外の言語で開発するとなってもきっと役に立つことが多いんじゃないでしょうか?
「何か難しそう。興味はあるけど本を読むだけではよくわからん。」
という方もいらっしゃるでしょう。で、ステマですが、6/30(土)に Symfony勉強会#6 を渋谷で開催します。
http://www.zusaar.com/event/306001
この勉強会ではテーマを「フレームワークに縛られない技術とそれを実践している一歩先ゆくエンジニアたちの声」としてSymfony2の基礎から開発に必要な技術を皆で学ぼうという機会で多くのベテランエンジニアの方に参加して頂いたり、スピーカーとして話していただいたりします。もちろん、この中でメインセッションとして著者である小川さんにも40分の講演をしていただく予定です。
募集は6/4(月) 12:00 ~ から先着順で行いますので、小川さんのサインが欲しい方、握手したい方、写真を一緒に撮りたい方、ファンの方是非ご参加を検討いただければと思います。
*1:本書内でもコラムでしっかりとこの点について強調されていました。