ぷぎがぽぎ このページをアンテナに追加 RSSフィード

2012-07-29

[][]フラットな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に近いので、ある程度の複雑なアプリケーション開発になってくると難しいんじゃないかとも思いました。

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

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

2012-07-28

[][] フラットな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へというのも書いたのですが、フレームワークの話が出てくるまでの前半は共通です。

しかも、書いていて気づいたのですが、元記事のコードはそのままでは動きません。(えっ!!)

そのあたりをカバーするためにもとりあえず書いてみました。

参照: 日本語訳 Symfony2 vs フラットなPHP | Symfony2日本語ドキュメント

なぜ マイクロフレームワーク は単にファイルを開いてフラットな 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 を利用します。

参照: no title

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も入れておきます。(これでテンプレートでpathurlというメソッドを使うことができるようになります)

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

2012-07-07

[][] これからの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ですよ。

2012-07-02

[][] Symfony勉強会 #6 が無事終了!!

f:id:brtRiver:20120703000119j:image:left

無事開催できました。

2012/6/30に VOYAGE GROUP にて Symfony勉強会#6 が開催されました。

レポートはユーザー会にもアップされる予定です。アップされました。

http://www.symfony.gr.jp/blog/20120630-symfony2-workshop6-report

それに、参加者のみなさんのブログ記事もアップされはじめているので、詳細はそちらをみていただくとして。。。


今回は自分が発起人として動いていたということもあり疲労感はかなりなものでした。

勉強会後のSymfony ミッドナイトでは最後まで起きていることすらできませんでした。。

なんといっても、勉強会は参加者も含めて皆の協力があってこそなのでこの場をお借りしてスタッフの皆さん、参加者の皆さんに本当にお礼申し上げます。

Symfony勉強会は回を増すごとに濃くなっているという事実

ユーザー会主催としてのSymfony勉強会は今回で6回目だったのですが、おそらく今回は過去最高の濃い内容でした。

f:id:brtRiver:20120703000121j:image:left

なんといっても、開催時間が長かった。。10:30から懇親会(と言うなの特別セッションやLT)で11:30という13時間コースでした。

ただセッションを聞くだけでなく、ワークショップも3時間ありましたし、参加された皆さんも気を抜く時間はほとんどなかったんじゃないでしょうか。。

学ぶということ

また、ただ勉強会をやるというのではなく、今回はテーマを決めました。

"フレームワークに縛られない技術とそれを実践している一歩先ゆくエンジニアたちの声"

そして、実際に参加者の人たちにはこのテーマを体感できたんじゃないかと思います。

あの勉強会では自分自身も多くのことを学びました。正直、勉強会の内容を100%なんかとてもじゃないですが理解できてません。

考えてみれば当たり前ですよね。たった1日話を聞いただけで身につくわけがないので。なので、これをきっかけに見えた色々な自分の足らない部分を把握し前に一歩踏み出し、学び続けることが大事じゃないかなと思っています。

ポシャった幻のセッション

今月フランスで Symfony Live Paris 2012 が開催されていました。去年のSymfony Live後はファビアンさんのKeynoteの動画がアップされたりしたので今年もあるだろうと推測し、その動画に某スタッフTシャツをデザインしてくれた広島人にappleのアレっぽく吹き替えしてもらおうと考えてました。

これは残念ながら動画が公開されずボツに....

BEAR.Sunday勉強会!?

f:id:brtRiver:20120703000120j:image:left

当日強引に@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ベースに。

また、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 の世界的な勢いがすごいですね。

2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2013 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2014 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2015 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2016 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |