downコマンドがなくupだけのDBマイグレーションツール Dbup

ビー・バップ・ハイスクールって知ってますか? Dカップって大きいですよね?

というわけで、超シンプルなDBマイグレーションツール Dbup を作ってみたので紹介。

インストールから実行までのデモンストレーション

  • dbup.pharをダウンロード
  • 必要なディレクトリや設定ファイルを作成
  • statusで確認しつつ、upで実行

dbup demo from brtriver on Vimeo.

特徴

  • 準備はdbup.pharをダウンロードするだけ
  • up コマンドしかありません。down コマンドは存在しません
  • マイグレーションの記述は親しみあるSQLそのままです。ORMやDSLを新しく覚える必要はありません
  • PHP標準のPDOクラスを利用しています
  • マイグレーションのステータス管理のためにデータベースに専用のテーブルを作る必要がありません
  • 設定ファイルはiniフォーマットです。PHPに依存しない一般的なフォーマットです。
  • PHP5.4以上。5.3.xでは動きません。

なぜDBマイグレーションツールを作ったか

DBマイグレーションはデプロイ自動化や継続的インテグレーションにおいて一般的に使われるようになってきました。PHPのDBマイグレーションツールとしてはDoctrine2 Migraionがすぐ思い浮かびます。フレームワークに特化したORMの一部として用意されているもの( CakePHP Migrations PluginとかCodeIgniterのマイグレーション )もあったりします。

これらは、ORMの機能の一部だったりするので、ORMの使い方を理解している必要があったり、DSLを覚えたりする必要があったりと手軽さがありません。

DBマイグレーションはDBに対してSQLを発行するのがお仕事なので、アプリの言語とは同じである必要はありません。なので、MyBatis Schema Migration*1Flywayのようなツールを使うほうが導入が楽だったり再利用性が高かったりします。

ただ、これでもまだコストが高いと思っています。というのは、DBマイグレーションというとスキーマのバージョンをUpしたりDownしたり、または特定の時点のバージョンに戻したりというのができて幸せ!ということができるようになっているのですが、事実上Downや特定のバージョンにマイグレーションツールを使って安全に戻せることはとても至難だと思います。

たとえば、カラムを追加しデータがその新しいカラムに追加された時点でDownを実行してデータを消していいかどうかは場合によると思いますし、実際はアプリケーションもあわせてロールバックしなくてはならない場合もあります。

ソースコードと違い、DBマイグレーションは常に歴史を進めていく(Upしつづける)のも運用方法の1つだと考えます。
もし、Upして何か問題が発生した場合も、新しくSQLを用意しそのSQLを発行することで修正を行う方法です。

そして、このようにUpだけできれば良いシンプルなマイグレーションツールが無く、あれば良いよなぁってことで作ってみました。

ドキュメントに書いてないこと

  • 当たり前ですが、フレームワークとか全く関係なく、使いたいときに使えます。
  • PHP環境であれば、他言語のプロジェクトでも使えます。たまたまたPHPで書かれてるだけです。PDOのドライバが対応していれば大丈夫(なはず)
  • githubからcloneしてきて適当にカスタマイズし、php dbup compile すれば俺マイグレーション作れます
  • 実装は Symfony Component の Consoleですので、Symfonyのコマンドの一部として作り込むなんてのも簡単
  • 「とりあえず作ってみた」感が強いので、実プロジェクトで使うときは要注意です。(dev環境やpre環境など本番とは別環境で十分確認できるようになっている等)
  • バグ見つけたらPull Requestくだしあ

作ってすっきり。

[PHP] Hamcrestを利用した超シンプルdocコメントでのテスト方法

PHPでテストを書くというとPHPUnitデファクトスタンダードで、次がSimpleTestでしょうか。
以前はインストールも大変でしたが、今となってはcomposer使えば楽ですし、実績もあります。
でも、本当にこの2択でPHPらしい開発ができていますか?

たとえば、テストケースのクラスを用意することが前提になります。
ちょっとPHPのコードを書いてテストしたいときもです。

たとえば、以下のようなロジックを書きたいとします。

<?php

$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];

// この$usersから男性('male')のものだけを抽出したい

$males = [
    '太郎' => 'male',
    '一郎' => 'male',    
];

?>

普通にPHPUnitでTDDでとなると、それなりに面倒です。
まず、ソースファイル(filter.php)とテストクラスファイル(filterTest.php)を作らなくてはなりません。
とりあえずさくっとコードを書きながらテストもしたいときは準備が面倒です。
最初にテストを書くとすると次のような感じでしょうか。

  • filterTest.php
<?php
require_once __DIR__ . '/filter.php';

class FilterTest extends PHPUnit_Framework_TestCase
{
    public function filterMales()
    {
        $users =[
	    '太郎' => 'male',
	    '花子' => 'female',
	    '一郎' => 'male',
        ];

        $expected = [
            '太郎' => 'male',
	    '一郎' => 'male',
        ];

        $this->assertEquals($expected, filterMale($users));
    }
}

やりたいコードの書き方

ソースとテストコードをいったりきたりせずに、コメントのように埋め込める方法があるといいですよね
以下のような感じで、ソースとテストを織り交ぜながらコードを書いていくパターンです。

<?php
$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];

// この$usersから男性(male)のものだけを抽出したい

$males = [
    '太郎' => 'male',
    '一郎' => 'male',    
];

function filterMale($users)
{
 .....
}

/**
 * @assertThat(filterMale($users), is($males)); <= このコメントがここでテストとして実行される。
 */

// 女性だけ抽出したい

$females = [
    '花子' => 'female',
];

function filterFemale($users)
{
 .....
}

/**
 * @assertThat(filterFemale($users), is($females)); <= このコメントがここでテストとして実行される。
 */

どうですか?直感的でシンプルでいい感じじゃないですか?
もちろんこのままプロダクトコードとして使うのはアレですが、PHPでのちょっとしたコードを試行錯誤しながら書いているときにこのように書けると良いですよね。


ルールは以下のような感じ

  • docコメントで@assertThatで定義したテストケースが実行される
    • assertThatが何というのはのちほどちらっと説明
  • 別テスト用のファイルが不要
  • コメントなので、実装には影響なし
  • docコメントブロックから普通のコメントブロックにしてしまえばテストも実行されない

HamcrestのassertThatだけを使う

PHPUnitのように、テストクラスを書く必要があるもので上記実装するのは超大変なので、HamcrestというMatcherライブラリのPHP版を使ってみます。

グローバル関数でassertThatが用意されているので、これを使います。assertThatはassertEqualsよりも直感的に英文に近い形で書くことができ、エラー時の表示もわかりやすいのが特徴です。
see also

チュートリアルにもあるようにMatcherライブラリなので、PHPUnitと組み合わせて使うこともできます。
hamcrestを使うと "$this->assertThat" と毎回 "$this->" を書かないといけないところを"assertThat"とグローバル関数で実行することができます。*1

Hamcrester

今回書いたのは、Hamcrestをコードのdocコメントブロックで書くことができるHamcresterです。
そんなにたいしたことはしてないのでgistにコードをコピペっておきました。

まず、vendor以下にHamcrestのライブラリを展開しておきます。

/Project
├── src
│      * sample.php ... ソースコード
└── vendor
    └── Hamcrest-1.1.0
        └── Hamcrest
               * Hamcrest.php

次にHamcresterをgistからとってきます。

$ wget https://gist.github.com/brtriver/5179397/raw/5bbe577769e079519901d3ba02d4f47ba892f2f9/hamcrester.php

最初のアイデアとして@assertThatと書いてましたが、それすらも長いので@tアノテーションで書けるようにします。
そして、できたのが以下のようなサンプル

<?php
$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];
// この$usersから男性(male)のものだけを抽出するfilterMale関数を作りたい

/**
 * @t(filterMale($users), is(arrayValue()));
 * @t(filterMale($users), not(hasValue('female')));
 * @t(filterMale($users), is(arrayWithSize(2)));
 */

function filterMale($users)
{
    return array_filter($users, function($user){
            return ($user === 'male')? true: false;
        });
}

// 女性だけ抽出する filterFemale関数を作りたい

/**
 * @t(filterFemale($users), is(arrayValue()));
 * @t(filterFemale($users), not(hasValue('male')));
 * @t(filterFemale($users), is(arrayWithSize(1)));
 */

function filterFemale($users)
{
    return array_filter($users, function($user){
            return ($user === 'female')? true: false;
        });
}

あとは、これを実行するだけ。

  • 失敗時
% php hamcrester.php src/sample.php
exception 'Hamcrest_AssertionError' with message 'Expected: is ["female"]
     but: was ["male", "female", "male"]' in ....
  • 成功時
% php hamcrester.php src/sample.php
OK (All Tests Green)

おー。できたできた。

実際は色付きで出力してるので以下なかんじ。

動きとしてはテスト実行時は、docコメントを外して @t を assertThatとして動かしているだけです。
PHPらしく、ちょっとしたコードをトライアンドエラーで書き上げていくときにこういうテストを使った実装方法があってもいいですよね。

Guardを使ってファイルの更新を監視してテストを実行する

次にやりたくなるのが、毎回テストを実行するのが面倒なので、更新時に自動的に実行してほしいという要望ですよね。
そこで、Guardというgemを使ったりするといいと思います。
サンプルはgistにおいてあるので、参考にしてみてください
https://gist.github.com/brtriver/5179397


(追記)

"それなんてDocTest..."

ブクマで指摘いただいてますが、そうです。やりたいのはDocTestです。
ただ欲しいのはassetThatを実行してくれるだけだったりします。


Phakeを使ったり、Hamcrest使ったりと、なんとなく自分の中でPHPUnitへの依存がどんどん減っていってる...

*1:PHPUnitのassertThatよりも書きやすいし、これだけでも使ってみると良いと思いますし、実際自分もとあるプロジェクトの全アサーションをHamcrestのassertThatに書き換えました。ただし、見てのとおり短い名前のグローバル関数を利用するのでそれなりの考慮が必要です

*2:追記: テストデータが含まれてるパターンだったので、テストデータがないパターンに修正

3分でできる俺PHPテスト環境

※この記事はVOYAGE GROUP エンジニアブログ : Advent Calendar 2012の15日目の記事として書いてます。

3種の神器 PHPUnit, Stagehand_TestRunner, Phake

初めての方はじめまして。いつもの方こんにちは。
VOYAGE GROUP の adingo という会社で 広告配信に関わる Webアプリケーション開発エンジニアをしている @brtriver です。

今日は実際の現場でも使っているPHPの開発環境構築Tipsということで みんな大好き PHPUnit 、 自分は大好き Phake 、毎回テストを手動で実行するのが許されるのは小学生までだよねー Stagehand_TestRunner の 3つを3分でプロジェクトごとにインストールする方法を書いてみます。

何も考えず composer.jsonをコピペで用意

プロジェクトのルートディレクトリを prj としましょう。
そこに 以下の内容の composer.json ファイルを用意します。

{
  "require": {
    "phake/phake": "v1.0.3",
    "piece/stagehand-testrunner": "v3.5.0"
  },
  "require-dev": {
      "phpunit/phpunit": "3.7.*"
  }
}

そうです。composerを使ってライブラリをプロジェクト内にインストールします。

composer.phar を用意する

あとは、以下のコマンドを黙って叩いて最新のcomposer.pharをダウンロードしてくる

$ curl -s https://getcomposer.org/installer | php

もし、curlがなければ以下でもOk

$ php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"

れっつ インストール

さぁ composerを使ってインストールしましょう

$ php composer.phar install --dev

composerのインストールはパッケージの確認などに時間が結構かかります。
お茶でもすすってお待ちください。

これで、vendorディレクトリが作成されコードが配置されます。

PHPUnit を実行してみる

testsディレクトリを作成してテストコードを書いてみましょう。

  • tests/SampleTest.php
<?php
Class SampleTest extends PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function テストが実行できること()
    {
        $this->assertTrue(true);
    }
}

これを phpunitで動かしてみます

$ ./vendor/bin/phpunit --tap tests
TAP version 13
ok 1 - SampleTest::テストが実行できること
1..1

簡単ですね。ちなみに説明は端折ってますがphakeも使えます。

Stagehand_TestRunner を使ってみる

最後にStagehand_TestRunner の用意です。
何も考えずに以下のおまじないコマンドを叩けば準備Ok

$ ./vendor/bin/testrunner compile -p vendor/autoload.php

あとは、testrunnerを起動しておきます。

$ ./vendor/bin/testrunner phpunit -p vendor/autoload.php -a tests
PHPUnit 3.7.10 by Sebastian Bergmann.

.

Sample

 [x] テストが実行できること

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

この状態でSampleTest.phpのテストを assetTrue(false) に書き換えてみましょう。
すると次のようにファイルの更新をキャッチし自動的にテストが実行されます。

PHPUnit 3.7.10 by Sebastian Bergmann.

F

Sample
 [ ] テストが実行できること

Time: 0 seconds, Memory: 5.50Mb

There was 1 failure:

1) SampleTest::テストが実行できること
Failed asserting that false is true.

/tmp/prj/tests/SampleTest.php:9
/tmp/prj/vendor/piece/stagehand-testrunner/src/Stagehand/TestRunner/Runner/PHPUnitRunner.php:81
/tmp/prj/vendor/piece/stagehand-testrunner/src/Stagehand/TestRunner/Process/TestRun.php:103
/tmp/prj/vendor/piece/stagehand-testrunner/src/Stagehand/TestRunner/CLI/TestRunner.php:69
/tmp/prj/vendor/piece/stagehand-testrunner/src/Stagehand/TestRunner/CLI/TestRunnerApplication/Command/PluginCommand.php:145
/tmp/prj/vendor/symfony/console/Symfony/Component/Console/Command/Command.php:238
/tmp/prj/vendor/symfony/console/Symfony/Component/Console/Application.php:192
/tmp/prj/vendor/symfony/console/Symfony/Component/Console/Application.php:105

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

なんと素敵なことでしょう!

まとめ

たった3分で、PHPで継続的にテストを書いて開発する俺環境ができてしまいました。
簡単というところがミソで、実プロジェクトでも 今回の環境+α を make install で自動構築できるようにしています。

明日の Advent Calendar は いつも笑顔が素敵な @bash0C7 です。お楽しみに!

Symfony を楽しむためのポイント - Symfony Advent Calender 2012 1日目 -

この記事は Symfony Advent Calender 2012 の1日目の記事です。
全体のカレンダーは Symfony Advent Calender 2012で見ることができます。

Symfonyの基礎

まず最初に、Symfonyをあまり知らない方のために簡単に特徴をまとめます。
Symfony はフランス Sensio Labs社 が開発するオープンソースPHPウェブアプリケーションフレームワークソフトウェアです。 2007年にバージョン1.0が公開されて以降、大規模なサイトを含む多数のウェブサイト、E-コマースサイトで利用されてきました。2012年12月1日現在の最新版は 2.1.4 です。
日本国内では日本Symfonyユーザー会で翻訳や勉強会を行なっています。

Symfony の開発は github で開発が行われており、PHPに分類された世界中のリポジトリの中では hiphop-php の次に2番目に注目を集めているリポジトリです。
とはいえ日本語の情報をあまり見ないので、その部分もふくめて Symfony を楽しむポイントを書いて行きたいと思います。

Symfonyはモデルには無関心

SymfonyはWebアプリケーションフレームワークです。関心があるのは、コントローラとビュー *だけ* です。
あなたが開発しようとしているドメイン部分(モデル)については全くもって関心はありません。
ドメイン部分については自分の好きなように開発することができます。
他のよくあるPHPフルスタックフレームワークはモデル部分を作成しようとすると、class User extends FrameworkModel のようにフレームワークに依存したコードになってしまったりしますが、Symfonyではそもそもモデルは関与しないので、フレームワークに依存せずに、ドメイン部分を育てていくことが自然にできます。
この部分はテストでも地味に影響してきます。たとえば、モデルにテストコードを書くことを考えてみましょう。さきほどのような extends が必須なモデルクラスのテストコードを書くことはフレームワークの基底モデルクラス(FrameworkModel)に依存しているので、本来気にしなくてもよいフレームワークのクラスをテストとの依存を何らかの方法で解決しなければなりません。
ドメイン部分をアジャリティに開発したい場合は Symfony はとても優れているとおもいます。

逆にいうと、ドメイン部分はあなたが正しく設計していかないと、簡単に廃れてしまいます。そのための知識が要求されるということになりますが、この知識はフレームワーク限定の知識ではありませんし、これからのエンジニアとして必要とされる知識なので、Symfony を使ってフレームワークに限定されないエンジニアとして必要な知識も勉強できるとも言えます。

Symfony Componentの存在

SymfonySymfony Component を組み合わせたフルスタックフレームワークです。Symfony Componentとは単なるライブラリ群です。
Webアプリケーションで必ず必要となるRequestやResponseをオブジェクトで扱えるようにしたHTTP Foundation
コンソールアプリをPHPで作るときに便利なConsoleなどがあります。
自分が見ているOSSの中ではテストコードも比較的しっかり書かれつつコミュニティベースで開発されているライブラリだと思います。
実際に、Symfonyそのものではなく、Symfony Componentの一部を使って別のフレームワークを開発しているプロジェクトもあります。
その代表的なのがSilexという、RubySinatra風にアプリケーションをPHPで構築できるフレームワークです。
私自身もCodeIgniterの既存プロジェクト内でSymfony Componentを組み合わせて使っていたりもします。
これらのコンポーネントのドキュメントから読み始めてみるのも面白いとおもいます。

とりあえず十分すぎるドキュメント

本家の公式ドキュメントはPDFでダウンロードできるのですが900ページ以上あります。なので、ググる前にPDF内検索することで大体やりたいことを見つけることができます。
how to 的なブログが少ないのもその影響があるのかもしれません。
また、日本語訳は全然追いつけていませんが、それでも多くのドキュメントやチュートリアルがオンラインで見ることができます。
海外ではカンファレンスも多く開催されており、そのときのセッションの多くが動画で見れるようになっています。
自分自身も全部を読み切れていない充実ぶりです。
また、翻訳作業については随時募集していますので、読むだけではなくコントリビューターとしても楽しむこともできます。興味がある方は日本Symfonyユーザー会のMLに声をかえてください。

難しいと感じるかわかりやすいと感じるか

SymfonyでWebアプリケーションを開発するときに、DDD(ドメイン駆動設計)に関する知識が必要になってきます。
また、普通にDIも使うことになります。
でも、このあたりの知識が不足しているものはアプリケーションが *ただ動く* だけで、ドメイン領域を反映できていないものになり、結果として普通のテストすらも書けなくなってしまいます。

PHPは歯ブラシですが、偉大な先人たちの経験を活かした普通のWebアプリケーションをPHPでも書くことができるのです。
デザインパターンを勉強した結果、チームが「ここはシングルトンパターンで...」というだけで共通認識を持てるのと同じですよね。「Userはエンティティだから...」というだけで共通認識が持てて開発できるのですから。

覚えることは多いかもしれませんが、フレームワークを使う以上フレームワークの知識が必要なのは避けれませんし、それはどのフレームワークでも必要な初期コストです。
どうせ勉強するなら今後も自身のキャリアで活かすことができる領域について勉強していきたいですよね。Symfonyはそういったエンジニアの方が楽しめるフレームワークだと思います。


とはいえ、Symfony自体の設計でまだまだイケていない部分もありますが、そういった部分と向き合いながらSymfonyにフィードバックをしてSymfonyを進化させることをコントリビューターとして楽しむというのが究極の楽しみ方かもしれません。

配列のキーの存在チェック(isset)をしながらhtmlspecialcharsするのをちょっと読みやすくする

1枚のPHPでGETパラメータの値をそのまま出力したりする場合、何も考えずに書くとこんな感じになります。

<?php echo (isset($_GET['name']))? htmlspecialchars($_GET['name'], ENT_QUOTES, 'utf-8'): "" ?>

さすがに、毎回これを埋め込むのは嫌ですよね。
なぜなら、同じkey名を2回間違わずに書かなくてはなりませんし、なによりも長い。
なので、次の考えるのがヘルパー関数をつぎように用意することです。

<?php
function h_array_key($key, $array) {
    return isset($array[$key])? htmlspecialchars($array[$key], ENT_QUOTES, 'utf-8'): "";
}
?>

<?php echo h_array_key('name', $_GET) ?>

これでちょっとはシンプルになりました。でも、name以外の項目が増えてくると

<?php echo h_array_key('name', $_GET) ?>
<?php echo h_array_key('email', $_GET) ?>
<?php echo h_array_key('tel', $_POST) ?>

といった感じになってきます。つまり、どのパラメータからなのかを毎回指定しなくてはなりません。
h_array_keyという自分の命名センスの無さも涙ですね。


そして、もう少し綺麗にできないかと思って書いたのが次のコード

<?php
$h_array_key = function($array, $encoding = 'utf-8') {
    return function($key) use ($array, $encoding) {
        return isset($array[$key])? htmlspecialchars($array[$key], ENT_QUOTES, $encoding): "";
    };
};

// $_GETパラメータ用の関数を生成
$_get = $h_array_key($_GET);

// $_POSTパラメータ用の関数を生成
$_post = $h_array_key($_POST);
?>

<?php echo $_get('name') ?>
<?php echo $_get('email') ?>
<?php echo $_post('tel') ?>

クロージャを使っているのでPHP5.3以上ですが、いまどきPHP5.2を使ってるなんてありえ(ry
最初の呼び出しで、どの配列に対しての操作なのかで$_getと$_postという2つの関数を生成し、作成した関数にkey名を渡すようにしています。
呼び出す側もシンプルになりましたし、読みやすい。


スコープが巨大な場合はこのままだと扱いづらいので、スコープが小さいときでないとダメだよねとか万能ではないのですが、特定のメソッド内だけで何度も利用するちょっとした処理は無名関数を上手につかうと可読性がよくなることも多いとおもいます。

フラットなPHPからSlimへ

フラットなPHPからSilexへの姉妹版記事です。

追記

  • configにモデルを突っ込むコードからcontainerプロパティを作り、配列としてクロージャを登録する方式に変更
  • $app全体を持ち回す必要がないところは必要な情報のみ渡すように修正

追記 2014/08/13

前提

前回の記事Symfony Componentを使い始める前までは同じです。
まずは、前回の記事で、素のPHPでブログアプリのコードを書いてみるところまで実践してみてください。

Slimを使ってみる

Slimのインストール

前回は Symfony Component (HttpFoundation) や Pimple を使いつつ Silexへ移行していきましたが、SlimはSilexのような外部ライブラリを使わず、Slimが用意したライブラリを使って書くことになります。

というわけで、Slimをインストールをして続きを書き換えていきます。
SlimのインストールはComposerでできます。

まずはComposerをコマンドラインからインストールします。

$ curl -s https://getcomposer.org/installer | php
// もし curlがインストールされていない場合は以下でもOK
$ php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"

これで、composer.pharというファイルがダウンロードされます。次に、Slimをインストールするためのcomposer.jsonファイルを用意し以下のように書いておきます。

{
    "require": {
        "slim/slim": "1.6.4"
    }
}

あとは、コマンドラインでinstallを叩くだけです。

$ php composer.phar install

Installing dependencies
  - Installing slim/slim (1.6.4)

Writing lock file
Generating autoload files

これで、vendorディレクトリが作成され、そこにSlimのファイルとautoload.phpが用意されます。
もし、追加で必要なコンポーネントがあれば composer.jsonに追加してインストールが行えます。

Slimのコントローラに書き換える

ここまでの、Webアプリケーション開発を通してどのようにコードを分離してきたかを整理してみましょう。

アプリケーションへのアクセスとは"どのURIに、どのリクエストメソッドで、どのパラメータをもってアクセスされるか"ということです。
つまり、ルーティングによってどの処理を行うかが決定されるということだけなのです。
この部分に注目したのが、マイクロフレームワークです。マイクロフレームワークではルーティングごとに処理を定義するだけです。
では、これまで書いてきたコードをSlimで書き換えてみます。

Slimのドキュメントは英語しかありませんが、コードと共に紹介されているので、それほど難しくはありません。
参照: Slimのドキュメント

<?php
require_once 'vendor/autoload.php';

$app = new Slim();

require_once 'controllers.php';
require_once 'model.php';

// リクエストを内部的にルーティング
$app->get('/', function () {
    list_action();
});

$app->get('/show', function () use ($app) {
    $id = $app->request()->get('id');
    show_action($id);
});

$app->notFound(function () use ($app) {
    echo '<html><body><h1>ページが見つかりません</h1></body></html>';
});

$app->run();

これまでif文で書いていたURIの条件が$pp->getメソッドで記述できるようになっています。比べると分かりやすいことがわかります。
$_GETへのアクセスは Slim では $app->request()->get('name') が用意されています。SilexではRequest, ResponseはSymfony ComponentのHttpFoundationコンポーネントを利用していた部分です。
そして、ルーティングに一致しないアクセスの場合はフレームワーク側で自動的にnotFoundメソッドが呼び出されます。このあたりの本来のロジックとは関係が薄い典型的な処理がゼロから書かなくても用意されているのがフレームワークを使うメリットです。

また、getメソッドの場合は$app->request()でパラメータの値を取得することもできますが、URIのパスにIDを含めておきURIから取得することもできます。

// (例) /show/2 => $id = 2 として処理する
$app->get('/show/:id', function ($id) use ($app) {
    show_action($id);
});

データベースの設定を外に出す

Silexの説明ではサービスコンテナを使うパターンでしたが、Slimの場合は$app->configメソッドを通して設定などのデータを共有することができます。セットするときは $app->config(array('key', 'value')) で、ゲットするときは $app->config('key') になります。

まず、フロントコントローラ(index.php)にデータベースの設定を記述しコントローラでcontrollers.phpやmodel.phpに引数で渡すようにします。

<?php
require 'bootstrap.php';

$app = new Slim();

$app->config('db.config', array(
               'host' => 'localhost',
               'database' => 'blog_db',
               'user' => 'myuser',
               'password' => 'mypassword',
               ));

require_once 'controllers.php';
require_once 'model.php';

// リクエストを内部的にルーティング
$app->get('/', function () use($app) {
    list_action($app->config('db.config'));
});

$app->get('/show/:id', function ($id) use ($app) {
    show_action($id, $app->config('db.config'));
});

$app->notFound(function () use ($app) {
    echo '<html><body><h1>ページが見つかりません</h1></body></html>';
});

$app->run();

これにあわせて、controllers.php, model.phpを書き換えます。

<?php
// controllers.php
function list_action($db_config)
{
    $posts = get_all_posts($db_config);
    require 'templates/list.php';
}

function show_action($id, $db_config)
{
    $post = get_post_by_id($id, $db_config);
    require 'templates/show.php';
}
<?php

// model.php
function get_database_connection($config)
{
    $pdo = new PDO(
      sprintf('mysql:host=%s;dbname=%s;charset=utf8', $config['host'], $config['database']),
      $config['user'],
      $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($db_config);

    $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;
}

次に、フロントコントローラーに記述しているロジックをcontrollers.phpに移してしまいましょう。show_actionやlist_actionというグローバル関数を使わずにSlimのルーティングコントローラーで記述していきます。

// index.php
<?php
// グローバルライブラリの読み込みと初期化
require 'bootstrap.php';

$app = new Slim();

$app->config('db.config', array(
               'host' => 'localhost',
               'database' => 'blog_db',
               'user' => 'myuser',
               'password' => 'mypassword',
               ));

require_once 'controllers.php';
require_once 'model.php';

$app->run();
<?php
// controllers.php
$app->get('/', function () use($app) {
    $posts = get_all_posts($app->config('db.config'));
    $app->render('list.php', array('posts' => $posts));
});

$app->get('/show/:id', function ($id) use ($app) {
    $post = get_post_by_id($id, $app->config('db.config'));
    if (!$post) {
        // 該当する記事がないので、このルーティングにマッチしなかったとして
        // 次のマッチするルーティングに処理を委譲するpassメソッドをコールする
        // => つまり、どのルーティングにもマッチしないのでnotFoundが実行される
        $app->pass();
    }
    $app->render('show.php', array('post' => $post));
});

$app->notFound(function () use ($app) {
    echo '<html><body><h1>ページが見つかりません</h1></body></html>';
});

フロントコントローラがすっきりしました。またコントローラからテンプレートの描画処理もSlimが用意していうるテンプレート機能を用いるように書き換えたため、読みやすくなりましたね。 (Slimでは標準でrenderに渡したファイル名はtemplatesディレクトリ以下から探します)

また、Slimのpassメソッドを利用して、詳細表示時に指定したIDが存在しなかった場合はnotFoundメソッドが処理されるようにしています。

コンテナを用意する

これまでモデルは関数の集まりでした。今後も増えていく予定だとしてクラスとしてまとめてみます。
そのため、Postモデルクラスを作成してPostオブジェクトをconfig経由で利用コンテナ経由で利用するようにしてみましょう。
SilexではPimpleというDIコンテナを使いました。同様にcomposer.jsonにpimpleを追加しインストールして利用してもよいですが、外部ライブラリに依存していないSlimらしさを活かすためにSlimクラスにcontainerプロパティを作るシンプルなパターンで書いてみましょう。
*1

まずは、Sliemのオブジェクト自身にcontainerプロパティを作成して空配列で初期化しておきます。

<?php
...
$app = new Slim();
$app->container = array(); // <= コンテナとして使う配列プロパティ
...

このコンテナにたとえばUserオブジェクトを作成するための処理を次のように用意します

<?php

$app->container['model.user'] = function() use($app) {
    return new User($app->config('logger'));
};

Pimpleと同じで、関数を定義しているだけなので、この時点ではクロージャ実行されません。
遅延評価で必要なときにUserオブジェクトを作成することができるのです。
また、$app を use を使って渡しているので呼び出すときは意識する必要がありません。

このサンプルのUserオブジェクトを作成したいときは以下のように使います。

<?php
$app->get('/', function () use($app) {
    $model_user = $app->container['model.user'](); // <= コンテナからクロージャを取得し実行
    $users = $model_user->get_all();
    ....
});

この例では引数が無いですが、もし引数を渡したい場合はクロージャーに引数を定義すれば良いだけです。

モデルを関数からクラスに

Postクラス

とりあえず、これまでmodel.phpで記述した関数をPostクラスとして書きなおしてみます。

<?php
// model.php
class Post
{
  public $db_config;
  public $pdo = null;

  public function __construct($db_config)
  {
    $this->db_config = $db_config;
  }

  public function open_database_connection()
  {
    if ($this->pdo === null ) {
      $this->pdo = new PDO(
        sprintf('mysql:host=%s;dbname=%s;charset=utf8', $this->db_config['host'], $this->db_config['database']),
        $this->db_config['user'],
        $this->db_config['password'],
        array(PDO::ATTR_EMULATE_PREPARES => false)
      );
    }
  }

  function close_database_connection()
  {
      $this->pdo = null;
  }

  public function get_all_posts()
  {
    $this->open_database_connection();
    $stmt = $this->pdo->query('SELECT id, title FROM post');
    $posts = array();
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $posts[] = $row;
    }
    return $posts;
  }
  function get_post_by_id($id)
  {
    $this->open_database_connection();
    $sth = $this->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;
  }
}

// containerにmodel.postというkeyで登録
$app->container['model.post'] = function() use($app) {
    return new Post($app->config('db.config'));
};

Postクラスは、データベース(PDO)を扱いつつ、postデータを処理しています。
データベースコネクションを行うために$app->configメソッドを使うので$appをコンストラクタで渡すようにしました。
また、データベースのコネクションはクラスの中でしか利用しないのでクラス変数として扱うようにし、それに伴いメソッド名をget_database_connectionからopen_database_connectionに変更しました。

これをSlimのコントローラで利用したいので、コンテナに'model.post'という名前で登録しています。
こうすることで、コントローラは次のように$app->contaier['model.post'] を通してクロージャを実行することでPostオブジェクトを作成することができます。

<?php
// controllers.php
$app->get('/', function () use($app) {
    $post_model = $app->container['model.post'](); // <= Postモデルオブジェクトを生成
    $posts = $post_model->get_all_posts();
    $app->render('list.php', array('posts' => $posts));
});

$app->get('/show/:id', function ($id) use ($app) {
    $post_model = $app->container['model.post']();  // <= Postモデルオブジェクトを生成
    $post = $post_model->get_post_by_id($id);
    $app->render('show.php', array('post' => $post));
});

ここで、Postクラスで気になることがあるのでちょっとリファクタリングしてみます。
たとえば、Userテーブルが新しく追加され、Userモデルが追加されたとします。その場合に今のままだとUserモデルにもstart_database_connectionメソッドが用意しなければならないことになります。
つまり、PostクラスはDatabaseを使いたいだけでPostクラスそのものがDatabseの情報を把握する必要はないということです。
PHP5.4からは trait が利用できるようになったので、Postクラスと Databaseトレイトに分けてみましょう。
*2

<?php
// model.php

trait Database
{
  public $pdo = null;

  public function open_database_connection($config)
  {
    if ($this->pdo === null ) {
      $this->pdo = new PDO(
        sprintf('mysql:host=%s;dbname=%s', $config['host'], $config['database']),
        $config['user'],
        $config['password'],
        array(PDO::ATTR_EMULATE_PREPARES => false)
      );
    }
  }
}


class Post
{
  use Database;
  public $db_config;

  public function __construct($db_config)
  {
    $this->db_config = $db_config;
  }

  public function get_all_posts()
  {
    $this->open_database_connection($this->db_config);
    $stmt = $this->pdo->query('SELECT id, title FROM post');
    $posts = array();
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $posts[] = $row;
    }
    return $posts;
  }
  function get_post_by_id($id)
  {
    $this->open_database_connection($this->db_config);
    $sth = $this->pdo->prepare('SELECT id, date, title, body FROM post where id = :id');
    $sth->execute(array(':id' => $id));
    $post = $sth->fetch(PDO::FETCH_ASSOC);

    return $post;
  }
}

// containerにmodel.postというkeyで登録
$app->container['model.post'] = function() use($app) {
    return new Post($app->config('db.config'));
};

これで、Userクラスを追加するとしても use Database; をするだけでPDOのためのコードを再利用できるようになりました。

configにセットしていくスタイルはシンプルですが、今回の例だとconfigにセットする時点でクラスをnewしています。つまり、configに沢山のオブジェクトをセットするとそれだけconfigが膨れていきます。そのため、configを使わずにファクトリメソッドを用意してコントローラーでPostクラスを作成するというアプローチもあるとおもいます。 コンテナ経由でクロージャを利用する方法に変更済み。

Silexの場合は無名関数を利用した遅延評価になっているため、実際に呼び出されるまで実態が作成されません。大量に登録しても実際に利用されるオブジェクトだけ展開されるというメリットがあります。
ただ、そのような大量なオブジェクトを扱う必要があるようなアプリケーションになると、Silexそのもので開発するのも大変になると思います。

このあたりは"良い感じ"にフレームワークの制約を活用しつつ柔軟に書くことが大事だと思います。

最後に

フラットなPHPからSlimに変化していく様子を見てきました。なぜフレームワークが便利なのかというのが見えてきたのではないでしょうか?とはいえ、フレームワーク銀の弾丸ではありません。このようにルーティングの処理は任せて、本来コーディングしたい部分に集中できるように助けてくれます。

今回はSlimで説明してきましたが、Slimには他にも良い感じの機能を提供してくれています。今回の記事でいう$app->pass() のような便利な機能は他にもあります。まずはSlimのドキュメントでさらに理解を深めてください。

2日分書き終えて

Silexだと無名関数をうまく利用しているコードが面白いですし、SlimはDBアクセス部分の標準での機能が無いなどSilexのように多機能でない分、設計力が試されるような気がします。マイクロフレームワークはとても楽しくて便利なのですが、フラットなPHPに近いので、ある程度の複雑なアプリケーション開発になってくると難しいんじゃないかとも思いました。

*1:configにモデルをセットして使いまわすというのは決して綺麗な実装ではないと思ってます。それってconfigちゃいますし。

*2:PHP5.3まではtraitを使うことができないので、ここで説明しているコードは動きません。

フラットな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 を書くよりも良いのでしょうか?

マイクロフレームワークをご存知でしょうか? 一番有名なのは Rubysinatra だと思います。通常の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 => ブログ一覧表示ページ (index.php が実行されます)
  • /show.php => ブログ単独表示ページ (show.php が実行されます)

index.php をフロントコントローラとして使用した場合

  • /index.php => ブログ一覧表示ページ (index.php が実行されます)
  • /index.php/show => ブログ単独表示ページ (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のリライトルールが追加されている場合)からでもアクセス可能であるべきだということを認識できません。また、ブログを開発する代わりに、コードの「アーキテクチャ」(例えばルーティングや呼び出すコントローラ、テンプレートなど)にたくさんの時間を費やしています。より多くの時間を、フォームの送信の扱い、入力のバリデーション、ロギングやセキュリティといったことに費やす必要があるでしょう。なぜこれら全てのありふれた問題への解決策を再発明しなければならないのでしょうか?

ライブラリを使って再開発を防ぐ

  • 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を使うシンプルさだけに目を奪われず、本当に必要な知識を適切な場所に依存させるということも考えましょう

参照: PHP Mentors -> Pimpleでシンプルに正しくDIを理解する

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 でテンプレートを書き換えてみましょう

参照:

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版も書く予定。たぶんこれよりももっと薄い内容になるはず。。疲れた書いた。