Phalconでマイクロフレームワーク

久しぶりにPHPのエクステンションで書かれた超高速フレームワークPhalconを見たら

  • 公式ページが見やすくなってた
  • バージョンが1.0.0になってた
  • annotationリーダーのライブラリができてた(そしてPhlconで使える)
  • Microというクラスがあり、マイクロフレームワーク風に書けるようになってた

と、相変わらず斜め上まっしぐらな感じがします。

マイクロフレームワークはルーティングだけ欲しいのなら選択肢としてありなきがします。
Phalconのことだからオーバーヘッドも少ないでしょうし。

というわけでGWで遊んでみたい人のためのやってみよう記事。

Phalconをインストール

たぶん一番難しいのはここだけ。

自分はmacのローカルに入れたので、以下のコマンドをぽちぽちたたいた。

$ git clone git://github.com/phalcon/cphalcon.git
$ cd cphalcon/build
$ sudo ./install

実際は複数のPHPのバージョンを入れている関係でinstallファイルに書かれているphpizeのパスを変更したり、--with-php-configの指定を変更して実行

コントローラーファイルを書く

requireなんていりません。だってPhalconだから。

<?php
$app = new Phalcon\Mvc\Micro();

$app->get('/', function () {
        echo "<h1>Welcome!</h1>";
    });

$app->get('/say/welcome/{name}', function ($name) {
        echo "<h1>Welcome $name!</h1>";
    });

$app->handle();

すっきり。

組み込みサーバーを立ち上げる

PHP5.4だよね?アプリケーションを書いたディレクトリで以下を実行

$php -S localhost:9999 

アクセスしてみる

ブラウザから以下のようなURLでアクセス。htaccessとか書いてないので、パスは_urlパラメータとして渡すのがコツ。

http://localhost:9999/index.php?_url=/say/welcome/brtriver

これは遊べそう。

マイクロな使い方のドキュメントは以下を参照。サンプルコードが多いからたぶん難しくないと思う。

さくらインターネットとかどこかのレンサパさん!
Phalconを標準で組み込んだPHPを使えるようにすると大ヒットするかもしれません。

まとめ

本当は BEAR.SundayルーターとしてPhalconを使えるようにしようとして久しぶりに見たのがきっかけだったり。腰据えてするならBEAR.Sundayのほうが面白いと思う。

あと、Silexの1.0がリリースされました。

5.3/5.4/5.5 どのバージョンでも動作します。また、5.4以上だとtraitで実装された機能も使えます。
これも要チェックです。

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を使うことができないので、ここで説明しているコードは動きません。