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

2016-01-06

[] シンプルな 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ってそんなに無いし、差し替えたくなる場面って思い浮かばない。
  • ある程度準備されたフレームワークのほうがオレオレフレームワークより楽。
  • メソッドの戻りの型を定義できるので、ちゃんと落ちてくれるのは楽(ただし実行時)

2016-01-03

[] PHP7 で PSR-7 と Middleware を使うマイクロフレームワークを書いてみた

コードはこちら。https://github.com/brtriver/karen

[追記]

この記事に書いてあるコードからさらに改良加えてApplicationレイヤーを作りました(v0.2)

詳しくはこっちの記事を参照をば

http://d.hatena.ne.jp/brtRiver/20160106/karen_framework

この記事の時点のコード(v0.1)を見たい場合は

https://github.com/brtriver/karen/tree/v0.1.3

からどうぞ。

なにこれ?

PSR-7が用意されてからコンポーネントを色々好きなのを選択できる時代が来つつあります。

たとえばzend-expressiveとか。

ただフレームワークががんばってこれらを抽象化しようとしてるのですが、もっとシンプルでもいいなぁと。

というわけで、コンポーネントをむき出しにして、ざぁーっと書いたらどうなるかをヤッてみたかったので書いただけという。

結果こんな感じに

https://github.com/brtriver/karen/blob/master/web/index.php

試してみたい人は

php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"
php composer.phar create-project brtriver/karen  ./<パス>
cd <パス>
make server
// http://localhost:8888/hello/karen_girls

で。


Karenでやったこと

PSR-7 とMiddleware は自由に差し替えられるように

逆にいうと、サービスコンテナ(DI)、ルーティングはフレームワークとして蜜になっててもいいかなと。

フレームワークのコントローラーは薄く

ルーティングのクロージャーで少し書きやすくするためのControllerクラスとTemplatableトレイトだけを用意。

「こういう処理をやる」ということをラベル(コメント)ブロック付けた

スコープが閉じられるわけじゃないです。あくまでもラベル。

ただインラインコメントを残すよりもわかりやすかなと。

<?php
container: {
    $c = new Container();
    ....
}

middleware: {
     $relayBuilder = new RelayBuilder();
      ...
}
さすがにサービスコンテナ無いと辛そう

コンテナは Container Interop というのも用意されてたりしますが、まだ公式というほどでもないのと、好きなの使えばいいじゃんということで使い慣れた Pimpleを選択。

別のコンテナ使ったとしても、Karen程度のフレームワークであればそんなに書き換えるのは大変じゃないと思う。

ルーティングは Aura.Router

ルーティングはクロージャーが定義できればいいので、使ったことがある Aura.Routerで

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

PHP7だと issetじゃなくて "??" 使えるの便利ですね。

KarenではControllerクラスを用意して、便利関数やRequest, Responseオブジェクトにクロージャーからアクセス出来るようにしてあります。

コントローラークラスの拡張を無名クラスでもできるように

必要があればControllerをextendsした専用のコントローラーを用意すればいいのですが、PHP7で無名クラス使えるので無名クラスでControllerを拡張できるようにしてみました。

たとえば通常は

<?php
...
$c['controller'] = function($c) {
	return new Controller($c['request'], $['response']);
};

と、コントローラーの用意しますが、

Templatable トレイトを利用できるようにしたControllerを用意したい場合は

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

    return $controller;
};

Middleware は Realy で

詳しくはRealyを使ってみた。

これって知ってる人は知ってる Mojavi のアレですよね。(懐かしい

書いてみてどうだったの?

  • まだもう少しコードを整理して書くつもり。
  • コード書くのが疲れたので、感想はいつかのLTのネタとして。

2016-01-01

[] 無名クラス(PHP7)をマイクロフレームワークで使ってみた

PHP7からは無名クラスが使えるようになりました。

どういった場合に無名関数を使う機会があるかなぁと考えていると、ふとマイクロフレームワークの無名関数部分の代わりに使うと良いんじゃないかと思いやってみましたというのがこの記事。

[追記]

無名関数 vs 無名クラス

また、PSR-7でリクエストとレスポンスのインターフェースが決められ、それに従ったフレームワークやミドルウェアが出てきています。そこで、PSR-7 のサポートをした Slim3 と PHP7 試してみます。

まずは、Slim3 のドキュメント通りに Hello World すると:

<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

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

$c = new \Slim\Container;
$c['greet'] = 'Hello ';

$app = new \Slim\App($c);

$app->get('/hello/{name}', function (Request $request, Response $response, $args) use ($app){
    $response->write($app->getContainer()['greet'] . $args['name']);

    return $response;
});
$app->run();

リクエストとレスポンスのオブジェクトが無名関数に渡されてくるので、レンダリングするときは Slimに用意されているレスポンスの `write` メソッドを呼びます。

シンプルで良いのですが、毎回 Request と Response を意識してコード書かなくても済むぐらいの抽象化が欲しくなります。そこで、MyControllerクラスを用意し、そこで RequestとResponseへの処理を行うようにしてみたのが以下のコード:

<?php
...
$app->get('/hello/{name}', new class($app) extends MyController {

        public function action($args)
        {
            return $this->render($this->container['greet'] . $args['name']);
        }
});

MyControllerを抽象クラスとして定義し、かならずactionメソッドが呼ばれるようにしてあります。

なので、この無名クラスでは actionメソッドを書くことで各ルーティングごとに処理が行われるという仕組みです。

また、レスポンスオブジェクトは直接さわらずrenderメソッドを用意して動作するようにしてみました。

MyControllerのコード

これでマイクロフレームワークで無名関数では大変だった抽象化も簡単に行えますし、ある意味オレオレフレームワークが簡単にできます。

マイクロフレームワークだけではルーティング数が増えてくると無名関数だけでは実装が難しくなってくる部分を、毎回クラスを定義し`__invoke`を利用する方式ではなく、ある程度無名クラスでゆるく書けるという方式はメリットがありそうです。

Twigテンプレートを使うためにTraitで

Slim3 は Pimpleベースのサービスコンテナがあるので、テンプレートエンジン(twig)を `view` に以下のようにセットすることで

<?php
$app = new \Slim\App();

// Get container
$container = $app->getContainer();

// Register component on container
$container['view'] = function ($container) {
    $view = new \Slim\Views\Twig('path/to/templates', [
        'cache' => 'path/to/cache'
    ]);
    $view->addExtension(new \Slim\Views\TwigExtension(
        $container['router'],
        $container['request']->getUri()
    ));

    return $view;
};

ルーティングで以下のようにviewを使ってレンダリングができます。

<?php
$app->get('/hello/{name}', function ($request, $response, $args) {
    return $this->view->render($response, 'profile.html', [
        'name' => $args['name']
    ]);
});

これも、トレイトを使って違ったアプローチで使えるようにしてみました。

トレイトを用意して、そこでTwigの設定関連を書いてしまいます。

サービスコンテナは利用せず直接 viewプロパティを用意しています。

  • TwigTemplatable.php
<?php
namespace Karen\Controller;

use \Psr\Http\Message\ResponseInterface as Response;

trait  TwigTemplatable
{
    private $templatePath;
    private $cachePath;
    private $view;

    public function useTwig()
    {
        $this->templatePath = __DIR__ . '/../../templates/';
        $this->cachePath = '/tmp/';

        // Register component on container
        $this->view = new \Slim\Views\Twig($this->templatePath, [
            'cache' => $this->cachePath
        ]);
        $this->view->addExtension(new \Slim\Views\TwigExtension(
            $this->container['router'],
            $this->container['request']->getUri()
        ));
    }

    public function renderTwig($path, $args)
    {
        $this->useTwig();
        $this->view->render($this->response, $path, $args);
    }
}

これをルーティングのMyControllerでuseし、renderTwigを通してレンダリングします。

<?php
$app->get('/hello/{name}', new class($app) extends MyController {
        use TwigTemplatable;

        public function action($args)
        {
            return $this->renderTwig('web.html', ['name' => $args['name']]);
        }
});

アプリケーションが大きくなるとコンテナ自身が膨れていく問題も無名クラス + トレイトを利用することである程度うまく整理できそうな感じがします。が、ここまでやらなくてもなぁとも思ったり。

試してみる

PHP7がインストールされていればgithubからコード持ってくれば色々遊べます。

$ git clone git@github.com:brtriver/slim3-anonymous-class.git
$ make setup
$ make install
$ php -S localhost:8888 -t ./web
// あとはブラウザから  http://localhost:8888/hello/brtriver にアクセスすればOK

もしくは、実際に Slim3 アプリケーションに組み込むなら、整理したこちらのパッケージをどうぞ

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 |