シンプルな 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); } }