OrePHPはシンプルで速いクールなフレームワーク

[追記1] 2012-06-10: ベンチマークを追加
[追記2] ブクマのコメントに回答
[追記3] ベンチマークをちょっと充実させた。Pinocoはえ
[追記4] コントローラーの仕組みを変更 & debugモード追加
[追記5] PHP5.4.4で再ベンチ

"ぼくがかんがえたさいきょうのふれーむわーく"ではないですが、OrePHPというPHP Webアプリケーション フレームワークを1つ書いてみた。

GitHub - brtriver/orephp: simple and fast PHP web application routing framework

こんせぷと

フレームワークが提供するのはルーティングだけ。シンプルに。速く。

ぼくがほしいのは、るーてぃんぐ

素のPHPでWebアプリケーションを書きたくない理由の1つがルーティングを用意するのが面倒というのがあります。Symfonyだとyamlphpxmlなどで定義できて結構便利です。つまり、自分が欲しいのはルーティングだけなんだというのは結構多いような気がします。というわけで、Symfony Component Routingを利用できるようにしただけのフレームワークです。なので、コアコードは70行ちょっと。

るーてぃんぐ・ふれーむわーく

仕組みは簡単。ルーティングの定義をconfig/routing.yaml に書きます。
書き方はSymfonyとほぼ同じ。どのコントローラーを呼ぶかを定義すればOK。

hello:
    pattern: /hello/{name}
    defaults: { controller: 'hello' }

これだと/hello/hogeでアクセスされると app/controller/hello.phpがincludeされるというだけ。

あとは、hello.phpでやりたいことを書いて、returnでブラウザに返したい文字列を書くだけ。
ここで、hello.phpで無名関数をreturnするようにします。そして無名関数内のreturnでブラウザに返したい文字列を書くだけ。

<?php
return function($request, $params, $container) {
    $now = date('Y-m-d H:i:s');
    return "Hello world " . $params['name'];
};

見ての通りクラスじゃないです。普通のPHPファイルです。よくMVCフレームワークだとアクションという概念がありますが、これには無いです。コントローラーだけです。

無名関数に渡される引数は、リクエストオブジェクト(Symfony2と同じ)、ルーティングで取得したパラメータ、DIコンテナ(Pimple)です。

ただ、returnで文字列返すのは面倒だ。やっぱりTwigぐらい使いたいってのはありますよね。その場合は次のように書けばOK

<?php
return function($request, $params, $container) {
    $now = date('Y-m-d H:i:s');
    return $container['tpl']->render('hello.html', array('now' => $now, 'name' => $params['name']));
};

$container['tpl']というのがTwigのインスタンスになってるので、この場合はだとapp/views/hello.htmlがtwigのテンプレートとして処理されます。簡単ですね。また、Twigのインスタンスは遅延読込になっているので呼ばない限り読込すらしません。このあたりはPimpleというDIコンテナを使ってよしなにやってます。つまり、Pimpleのインスタンスが$containerですね。

ふつうにはやい

一番重い処理はrouting.yamlの解析だと思いますが、一度解析するとcache/ProjectUrlMatcher.php にキャッシュし、2度目以降は解析しません。普通にPHPのクラスファイルを読み込むだけになるので速いです。ということで、routing.yamlを書き換えたらキャッシュされているファイルを手動で削除しなくちゃいけません。
そうしないと、変更後の内容が反映されないので。
また、デバッグモードを追加しました。
index.php

$app->c['debug'] = true;

と追記してデバッグモードを有効にすれば毎アクセスごとにrouting.yamlをパースします。開発時には有効にしておくと便利です。

(追記)簡単なHello World的なベンチマーク結果は以下のとおり。PHP5.4.3(php-fpm)+apc+nginxのvmにsiegeで計測。あくまでオレ環境なので参考程度で。
(追記2)FuelPHPを再計測
(追記3)PHP5.4.4で再ベンチ

siege -b -c 10 -t 3S http://xxxx/xxx で検証
name template engine trans/sec result(relative)
FuelPHP 1.2 Twig 130.04 63%
FuelPHP 1.2 - 169.08 82%
Silex Twig 170.63 83%
Silex - 205.61 100%
OrePHP Twig 342.89 167%
Pinoco(dev-master) PHPTAL 352.11 171%
CodeIgniter 2.1 - 373.02 181%
Pinoco(dev-master) - 429.84 209%
OrePHP - 484.76 236%

SilexのTwig無バージョンを基準としてみると、2倍ぐらいのレスポンスは叩きだせますね。わーい。

でーたべーすとかは?

なにそれ?おいしいの?

せっていふぁいるとかは?

routing.yamlだけあります。ほかは$app->cがDIコンテナなのでよしなにやってね。web/index.phpでTwigをほり込んでるあたりが分かりやすいかと。ちなみにindex.php全体でもたったこれだけ。

<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new Ore\Framework;
$app->c['tpl'] = $app->c->share(function($c){
    $loader = new Twig_Loader_Filesystem($c['base_dir'] . '/app/views');
    return new Twig_Environment($loader,array(
                                  'cache' => $c['cache_dir'],
                                  ));
  });
// $app->c['debug'] = true;
$app->display();

つかいかた

githubからgit cloneなりダウンロードしてきたら

$ php composer.phar install

で外部ライブラリをインストールするだけ。

cacheディレクトリは実行者が書き込めるように0777にしておきましょう。

あとは、ブラウザからhttp://example.com/hello/orephpにアクセスすればOK

コメントありがとうございます

以下のコメントをいただきました。ありがとうございますm(__)m

なぜPHPを直書きさせてないのか?について
  • yaml じゃなくて素の PHP の方が速くなりそうなんだけど、どうなんだろう。 by id:heavenshellさん
  • キャッシュを手動削除する手間が必要なら、素直にPHP直書きさせたらいいのに by id:terurouさん

キャッシュを利用しているのは速くなるからなのですが、yamlよりPHPの方がと考えが浮かぶのはわかります。しかし、実際にどのようにキャッシュされているかを見ると納得してもらえるのではないかと...。

routing.ymlで以下のように定義したファイルは

hello:
    pattern: /hello/{name}
    defaults: { controller: 'hello' }
default:
    pattern: /{controller}
    defaults: { controller: controller }

以下のような cache/ProjectUrlMatcher.php に変換されキャッシュされます。

<?php

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;

/**
 * ProjectUrlMatcher
 *
 * This class has been auto-generated
 * by the Symfony Routing Component.
 */
class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
    /**
     * Constructor.
     */
    public function __construct(RequestContext $context)
    {  
        $this->context = $context;
    }

    public function match($pathinfo)
    {  
        $allow = array();
        $pathinfo = rawurldecode($pathinfo);

        // hello
        if (0 === strpos($pathinfo, '/hello') && preg_match('#^/hello/(?<name>[^/]+)$#s', $pathinfo, $matches)) {
            return array_merge($this->mergeDefaults($matches, array (  'controller' => 'hello',)), array('_route' => 'hello'));
        }

        // default
        if (preg_match('#^/(?<controller>[^/]+)?$#s', $pathinfo, $matches)) {
            return array_merge($this->mergeDefaults($matches, array (  'controller' => 'controller',)), array('_route' => 'default'));
        }

        throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
    }
}

なんとなくyamlファイルがただパースされてPHPの配列になると想像する方もいるかもしれませんが、Symfony Component Routingはこのように、DSLで複雑なPHPコードを抽象化し記述できるようにしてくれています。なので、今回はyamlを選択していますが、xmlでもphpの配列でも定義することができ、最終的には同じPHPコードに変換されます。さすがにこのクラスコードをルーティングを追加、編集するたびに書けというのは辛いですよね。

参考: ドメイン特化言語とモデル駆動エンジニアリング - Johan den Haan - Digital Romanticism

とはいえ、開発用にキャッシュしないオプションがあったほうがいいですよね。
index.phpデバッグモードを有効にしている場合はキャッシュを利用せずに毎回パースするようにしました。

$cとか突然でてくるのキモいよね
  • $cとか突如出現するの怖いし、$thisにできないですかね。(再代入防げるし)by id:escape_artistさん

はい。キモイですよね。うん。キモイとおもいます。
まだまだ改良の余地はありますよね。せめてクラスメソッドで閉じ込めるようにしたいと思います。
改良し、無名関数に閉じ込めました。

オレオレフレームワーク作るの楽しいですね。