シンプルな PHP7マイクロフレームワーク Karen

この記事は PHP7 で PSR-7 と Middleware を使うマイクロフレームワークを書いてみた の続編です。

コードは https://github.com/brtriver/karen

前回までの記事の流れをざっくりと書くと

  • Slim3 が PSR-7 と Middleware を採用していたので、PHP7の無名クラスを使ってみた
  • もっと薄いものほが欲しくなり PHP7で PSR-7 と Middleware を使ったマイクロフレームワークを作ってみた(Karen v0.1)

ただ、薄く作りすぎてエンドポイントのコードに色々書かなくてはならなくなって、それは見通しが悪くなったのでもう少し整理しがっつり書いてみた(v0.2)

Karen を使ったコード

解説はあとでやりますが

<?php
$app = new YourFramework(); // Karenアプリケーションを拡張したアプリケーション
$app->run();
$app->sendResponse();

のように書いたり、PHP7なので、無名クラスを利用して

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = new class extends Karen\Framework\Karen {
        public function action($map)
        {
            // hello name controller sample.
            $map->get('hello', '/hello/{name}', function($args, $controller) {
                $name = $args['name']?? 'karen';
                return $controller->render('[Karen] Hello, ' . $name);
            })->tokens(['name' => '.*']);

            return $map;
        }
    };

$app->run();
$app->sendResponse();

のように書くことができます。

Karen の構成

Applicationのインターフェースを用意しました。インターフェースでは

  • 何かしらのサービスコンテナ(DI)を構築する
  • 何かしらのMiddlewareを定義する
  • 何かしらのルーティングからリクエストにマッチするルーティングと処理を決定する
  • 定義したMiddlewareと決定したルーティングの処理(コントローラー)を行う

というルールだけを決めています。
そして、これらは`$app->run()` を呼ぶことで順番にメソッドが呼ばれ、構築されたアプリケ−ションから最後に `$app->sendResponse()` を叩くことで何かしらのレスポンスが返されます。

いわゆるテンプレートメソッドパターンになっていて、こんな感じです。

<?php
abstract class Application
{
   ....
    public function run()
    {
        $this->container();
        $this->middleware();
        $this->route();
        $this->response();
    }
}

そして、Karen はこのApplicationインターフェースを実装したApplicationクラスをベースに、

  • サービスコンテナに Pimple
  • Middleware のライブラリに Relay
  • ルーティングにAura.Router (Karen2のサンプルではFastRoute)

を使うように実装しています。

実際はルーティングにマッチした場合の処理は書かれていないので、このKarenクラスを拡張する必要があります。
これが、最初に書いた独自アプリケーションクラスを使ったコードや、無名クラスを使ったコードになります。

Karen のコントローラー

Karen ではアプリケーションから responseメソッドをコールしたときにルーティングに定義されたcallableなものをMiddlewareをとおして実行されます。この責務をコントローラーにまかせています。そして、アプリケーションで利用するコントローラーを差し替える事を可能にしています。

通常は、containerメソッドでpimple($c)に以下のようにコントローラーを突っ込むだけですが、

<?php
$c['controller'] = new Controller();

Twigテンプレートを使いつつ、そのためのメソッド(renderWithT) を使えるように拡張したものを定義するためには

<?php
$c['controller'] = function($c) {
    $controller = new class extends Controller{
            use Templatable;
        };
    $controller->setTemplate($c['template']);

    return $controller;
};

のようにControllerクラスをTraitを利用したクラスに無名関数で拡張するだけでOKです。PHP7便利ですね。

もちろん独自のコントローラーを定義することもできますし、機能を追加したいのであれば上のようにTraitを用意すれば代替事足りるかもしれません。

コントローラーはルーティングに一致したときに呼ばれるcallableなものを把握していますが、このクロージャーは引数として $args と $controller を受け取ります。
$argsはパスで定義され取得されたパラメータが入っていて、名前をkeyとしてアクセスできます。
また、$controllerはコントローラークラス自身です。RequestとResponseには $controller->request, $controller->responseでアクセスできます。
もちろん、このReuqestとResponseは Middleware が適用された後のオブジェクトが入ってきます。

あとは Middleware の仕様に従って $response を返すようにします。

<?php
        $map->get('hello', '/hello/{name}', function($args, $controller) {
            $name = $args['name']?? 'karen';
                return $controller->render('[Karen] Hello, ' . $name);
            })->tokens(['name' => '.*']);

Traitで追加したメソッドなども$controllerを通して呼び出すことができます。

<?php
            // with twig
            $map->get('render_with_twig', '/template/{name}', function($args, $controller) {
                return $controller->renderWithT('demo.html', ['name' => $args['name']]);
            });

また、Jsonのレスポンスを返したい場合はreturn がJsonResponseになっていればいいので

<?php
$map->get('json', '/json/{name}', function($args, $controller) {
    return new \Zend\Diactoros\Response\JsonResponse(['name' => $args['name']]);
});

のようにすれば、まぁできます(ただし、Middlewareで適用されてきた $controller->response を破棄しちゃいますが)

Karenを利用してオレオレフレームワークの作る

で、Karen は Applicationインターフェースを実装したテンプレートパターンに従った何かに過ぎないので、このパターンに従ってさえすれば好きなものを書けばいいと思います。

途中で違うライブラリ(コンポーネント)に差し替える.. なんてことあんまりやらないと思うので、最初に使いたいコンポーネントをある程度決めて書いてしまうとかでいいんじゃないでしょうか。

たとえば、サンプルとして Aura.Router ではなく FastRoute を使う Karen2 を作る場合は、routeメソッドが代わり、route結果を使うresponseメソッドもそれに伴って書き換えるんですが、それだけであとの処理は同じです。

  • Aura.Router 版
<?php
class Karen extends Application
{
    ....
    public function route()
    {
        $map = $this->c['router']->getMap();
        // define routes at an action method in an extended class
        $map = $this->action($map);
        $this->route = $this->c['router']->getMatcher()->match($this->request);
    }
    public function response()
    {
        if (!$this->route) {
            $response = $this->response->withStatus(404);
            $response->getBody()->write('not found');
            return;
        }
        // parse args
        $args = [];
        foreach ((array)$this->route->attributes as $key => $val) {
            $args[$key] = $val;
        }
        // add route action to the queue of Midlleware
        $this->addQueue('action', $this->c['controller']->actionQueue($this->route->handler, $args));
    }
}
  • FastRoute 版
<?php
class Karen extends Application
{
    ....
    public function route()
    {
        $this->c['handlers'] = function () {
            return $this->handlers();
        };
        $dispatcher = $this->c['dispatcher'];
        $this->route = $dispatcher->dispatch($this->request->getMethod(), $this->request->getUri()->getPath());
    }
    public function response()
    {
        switch ($this->route[0]) {
            case \FastRoute\Dispatcher::NOT_FOUND:
                echo "Not Found\n";
                break;
            case \FastRoute\Dispatcher::FOUND:
                $handler = $this->route[1];
                $args = $this->route[2];
                $this->addQueue('action', $this->c['controller']->actionQueue($handler, $args));
                break;
            default:
                throw new \LogicException('Should not reach this point');
        }
    }
}

ちなみに、ローカルでベンチ取ると、圧倒的に FastRoute 速いです。

Middleware ライブラリを導入してみる

Karen は Middleware を持っているので、psr7-middlewares を簡単に使えます

composer require oscarotero/psr7-middlewares

でインストールすれば、エンドポイントのコードでmiddlewareをqueueに追加するだけです

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = new class extends Karen\Framework\Karen {
        // middleware を追加する
        public function middleware()
        {
            $this->addQueue('responseTime', Psr7Middlewares\Middleware::responseTime());
        }

        public function action($map)
        {
            // hello name controller sample.
            $map->get('hello', '/hello/{name}', function($args, $controller) {
                $name = $args['name']?? 'karen';
                return $controller->render('[Karen] Hello, ' . $name);
            })->tokens(['name' => '.*']);

            return $map;
        }
    };

$app->run();
$app->sendResponse();

これでレスポンスヘッダに処理時間を追加することができました。

X-Response-Time:8.789ms

便利。

Karen を作ってみて

  • 無名クラスはさくっとやるのには有りな場面はある。たとえばテストで無名クラスを使って呼び出し順序が正しいかどかのコードも書ける。
<?php
class ApplicationTest extends \PHPUnit_Framework_TestCase
{
    public function testRunOrder()
    {
        $app = new class extends Application{
                public $passed = '';
                public function container()
                {
                    $this->passed .= 'container->';
                }
                public function middleware()
                {
                    $this->passed .= 'middleware->';
                }
                public function route()
                {
                    $this->passed .= 'route->';
                }
                public function response()
                {
                    $this->passed .= 'response';
                }
            };
        $app->run();
        $this->assertSame('container->middleware->route->response', $app->passed);
    }
}
  • Middleware に乗っかておけば、色んなライブラリがそのまま使えるメリットは大きい。たとえばpsr7-middlewares
  • PSR-7 に準拠したRequestやResponseってそんなに無いし、差し替えたくなる場面って思い浮かばない。
  • ある程度準備されたフレームワークのほうがオレオレフレームワークより楽。
  • メソッドの戻りの型を定義できるので、ちゃんと落ちてくれるのは楽(ただし実行時)