Skip to content
Akihito Koriyama edited this page Jun 3, 2014 · 62 revisions

アプリケーションのインストール

$ mkdir ~/bear-workshop
$ cd ~/bear-workshop/
$ composer create-project bear/skeleton Koriym.Work
Installing bear/skeleton (0.10.2)
  - Installing bear/skeleton (0.10.2)
    Loading from cache

フレームワークのインストール

BEAR.Sundayフレームワークを依存としてインストールします。

$ cd Koriym.Work
$ composer install

Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing aura/installer-default (1.0.0)
    Loading from cache
 ...
Writing lock file
Generating autoload files

アプリケーションリソースファイルの作成

src/Resource/App/Add.php

<?php

namespace Koriym\Work\Resource\App;

use BEAR\Resource\ResourceObject;

class Add extends ResourceObject
{

    public function onGet($a, $b)
    {
        $this['result'] = $a + $b;

        return $this;
    }
}

作成したリソースにアクセスしてみる

コンソールからアクセス

コンソールからアクセスしてみます。まずはエラー、必要な引き数を渡していません。

$ php bootstrap/contexts/api.php get 'app://self/add'
400 Bad Request
…
[BODY]

40Xのエラーはリクエストに問題があるエラーです。 次は引き数をつけて正しいリクエスト

$ php bootstrap/contexts/api.php get 'app://self/add?a=1&b=2'
200 OK
content-type: ["application\/hal+json; charset=UTF-8"]
cache-control: ["no-cache"]
date: ["Fri, 30 May 2014 15:16:49 GMT"]
[BODY]
result 3,

[VIEW]
{
    "result": 3,
    "_links": {
        "self": {
            "href": "http://localhost/app/add/?a=1&b=2"
        }
    }
}

計算結果が[BODY]に入りその表現が[VIEW]として表されてます。

Web APIサービス化してRESTクライアントからアクセス

これをWeb APIサービスにしてみましょう。

$ php -S 0.0.0.0:8081 bootstrap/contexts/api.php

RESTクライアント(Chromeアプリの Advanced REST client など)で http://0.0.0.0:8081/add?a=1&b=2にGETリクエストを送り確かめてみましょう。

Webサイトの確認

次はWebサイトです。最初から用意されているIndexページのスクリプトを使用します。

まずはコンソールでアクセスします。

$ php bootstrap/contexts/dev.php get /
200 OK
cache-control: ["no-cache"]
date: ["Fri, 30 May 2014 15:36:12 GMT"]
[BODY]
greeting Hello BEAR.Sunday,

[VIEW]
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>BEAR.Sunday</title>
    <link href="//netdna.bootstrapcdn.com/bootswatch/3.0.0/united/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <h2>Hello BEAR.Sunday</h2>
    <p>template engine: twig</p>
</div>
</body>
</html>

Webで確認するためには同じスクリプトでビルトインウェブサーバーを起動します。

$ php -S 0.0.0.0:8082 -t var/www/ bootstrap/contexts/dev.php

Webブラウザで http://0.0.0.0:8082/ にアクセスします。

ページリソースファイルの作成

計算ページをつくるために、Addリソースにアクセスするページリソースを作成します。

src/Resource/Page/Calc.php

<?php

namespace Koriym\Work\Resource\Page;

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;

class Calc extends ResourceObject
{
    use ResourceInject;

    public function onGet($a, $b)
    {
        $add = $this->resource
            ->get
            ->uri('app://self/add')
            ->withQuery(['a' => $a, 'b' => $b])
            ->request();
        $this['a'] = $a;
        $this['b'] = $b;
        $this['add'] = $add;

        return $this;
    }
}

src/Resource/Page/Calc.twig

<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
<div class="container">
    <h2>{{ a }} + {{ b }} = {{ add.result }}</h2>
</div>
</body>
</html>

アクセスしてみましょう。

http://0.0.0.0:8082/calc?a=1&b=2

DIでログを提供

この結果をログする機能を追加してみましょう。ログには monolog を使いますが利用するときに直接作成しないで、作成されたログオブジェクトを受け取るようにします。

このように必要なものを自らが取得するのではなく、外部からの代入に期待する仕組みを DI といいます。

ロガーインターフェイスはvendor/psr/logPSR3のロガーインターフェイスを使用します。

monologcomposerで取得します。

$ composer require monolog/monolog "~1.0"

Ray.Diではインターフェイスとその実装をバインディングする方法をいくつか提供していますがここではProviderバインディングを使います。詳細は BEAR.SundayのマニュアルRay.DiのREADME をご覧ください。

最初にインスタンスを用意するProviderというファクトリーを作成し、インターフェイス実装に必要なgetメソッドでインスタンスを返します。

<?php

namespace Koriym\Work\Module\Provider;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Ray\Di\ProviderInterface;

class MonologLoggerProvider implements ProviderInterface
{
    public function get()
    {
        $log = new Logger('monolog');
        $log->pushHandler(
            new StreamHandler(__DIR__ . '/../../../var/log/debug.log',
            Logger::DEBUG)
        );

        return $log;
    }
}

次にインターフェイス束縛の定義をAppModuleconfigureメソッド内でbindメソッドを使い行います。

class AppModule extends AbstractModule
{
    // ...
    protected function configure()
    {
        $this->install(new StandardPackageModule('Koriym\Work', $this->context, dirname(dirname(__DIR__))));

        $this->bind('Psr\Log\LoggerInterface')
            ->toProvider('Koriym\Work\Module\Provider\MonologLoggerProvider');

        // ...
    }
}

この定義はキャッシュされる事に注意してください。webサーバーで確認する時には変更が反映されるようにキャッシュをクリアします。

$ php bin/clear.php

アプリケーションスクリプトの中で毎回クリアするためにはコメントアウトを外します。

bootstrap/contexts/dev.php

//
// The cache is cleared on each request via the following script. We understand that you may want to debug
// your application with caching turned on. When doing so just comment out the following.
//
require $appDir . '/bin/clear.php';

Addクラスのコンストラクタでmonologオブジェクトを受け取りプロパティに格納します。ログ出力ではそのロガーを使います。

<?php

namespace Koriym\Work\Resource\App;

use BEAR\Resource\ResourceObject;
use Psr\Log\LoggerInterface;
use Ray\Di\Di\Inject;

class Add extends ResourceObject
{
    private $logger;

    /**
     * @Inject
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onGet($a, $b)
    {
        $this['result'] = $a + $b;

        $this->logger
            ->debug(sprintf('%d + %d = %d', $a, $b, $this['result']));

        return $this;
    }
}

フレームワークはメソッドについた@Injectを発見すると "ここには依存が必要だ" という事を理解し、モジュールで束縛したインスタンスを代入します。そのメソッドがコンストラクタの時はコンストラクタインジェクション、その他のセッターメソッドのときはセッターインジェクションと呼ばれます。

実行してみて、var/log/debug.log に結果が出力されていることを確認しましょう。

AOPで実行時間を計測

メソッドの実行時間を出力するインターセプターを作成しましょう。

インターセプターは指定されたメソッドを横取り(intercept)します。インターセプターから元のメソッドを呼びだす時に前後に処理を記述することができます。

メソッドの実行時間を計測するインターセプターはこのようになります。

<?php

namespace Koriym\Work\Interceptor;

use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

class BenchMarker implements MethodInterceptor
{
    public function invoke(MethodInvocation $invocation)
    {
        $start = microtime(true);
        $result = $invocation->proceed(); // 元のメソッドの実行
        $time = microtime(true) - $start;

        var_dump($time);

        return $result;
    }
}

MethodInvocationインターフェイスの$invocationに"メソッド実行オブジェクト"が渡され$invocation->proceed()とすると対象のメソッドを実行します。

例えばこのインターセプターをAddクラスのonGetメソッドにバインドすると$invocation->proceed()で足し算の結果が返ります。

対象メソッドの指定にはmatcherを使います。matcherで指定した条件でメソッドが検索されマッチしたメソッドに、1つまたは複数のインターセプターがバインドされます。まずはクラスやメソッドを名前で検索するstartsWith()を使用してみましょう。前方一致文字列でマッチします。

src/Module/AppModule.php

<?php

namespace Koriym\Work\Module;

use BEAR\Package\Module\Package\StandardPackageModule;
use Koriym\Work\Interceptor\BenchMarker;
use Ray\Di\AbstractModule;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;

class AppModule extends AbstractModule
{
    // ...
    protected function configure()
    {
        // ...

        // マッチするメソッドにインターセプターをバインド
        $this->bindInterceptor(
            $this->matcher->startsWith('Koriym\Work\Resource\App\Add'), // クラスの指定
            $this->matcher->any(), // メソッドの指定
            [new BenchMarker()] // インターセプター
        );
    }
}

これで、Addクラスのすべてのメソッドに対してBenchMarkerがバインドされました。実行して画面に実行時間を表示させましょう。

アノテーションによるインターセプターのバインド

インターセプターによるAOPが実装できましたが、別のメソッドでも実行時間の計測をしたくなった時に、今の実装ではバインドの定義を都度追加する必要があります。

そこで、@BenchMarkとアノテーションをつけたメソッドにインターセプターをバインドするように変更しましょう。

まずはアノテーションクラスを実装します。このアノテーション自体は何も処理をしないので、中身は空でよいです。

<?php

namespace Koriym\Work\Annotation;

use Ray\Aop\Annotation;

/**
 * @Annotation
 */
class BenchMark implements Annotation
{
}

次に、バインドの定義を変更します。先ほどはstartsWithでマッチングしましたが、今度はannotatedWithを使います。

$this->bindInterceptor(
    $this->matcher->any(),
    $this->matcher->annotatedWith('Koriym\Work\Annotation\BenchMark'),
    [new BenchMarker()]
);

最後に、Add::onGetメソッドにアノテーションを付けてあげれば完成です。

<?php

namespace Koriym\Work\Resource\App;

use BEAR\Resource\ResourceObject;
use Psr\Log\LoggerInterface;
use Ray\Di\Di\Inject;
use Koriym\Work\Annotation\BenchMark;

class Add extends ResourceObject
{
    private $logger;

    /**
     * @inject
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @BenchMark
     */
    public function onGet($a, $b)
    {
        $this['result'] = $a + $b;

        $this->logger->debug(sprintf('%d + %d = %d', $a, $b, $this['result']));

        return $this;
    }
}

実行してみて、先ほどと同じように結果が出力されることを確認しましょう。

AOP機能の詳細は BEAR.SundayのマニュアルRay.AopのREADME をご覧ください。

実行時間の出力先を、画面表示ではなくログ出力に変更してみる(インターセプターへのDI)

今の実装では、インターセプターで計測した実行時間はvar_dumpで画面に出力されてしまいますが、これをログに出力するように変更してみましょう。

もちろん、ロガーはDIによってインジェクトします。

まずは、BenchMarkerを、ロガーをインジェクトできるように拡張しましょう。

<?php

namespace Koriym\Work\Interceptor;

use Psr\Log\LoggerInterface;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
use Ray\Di\Di\Inject;

class BenchMarker implements MethodInterceptor
{
    private $logger;

    /**
     * @Inject
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function invoke(MethodInvocation $invocation)
    {
        $start = microtime(true);
        $result = $invocation->proceed(); // 元のメソッドの実行.
        $time = microtime(true) - $start;

        $this->logger->debug(sprintf('%f sec elapsed', $time));

        return $result;
    }
}

Psr\Log\LoggerInterfaceに対するmonologのバインドは既に定義してあるので、これで完成・・・ではありません。

今のままだと、インターセプターとしてバインドされるBenchMarkerのインスタンスは、bindInterceptorの引数内でnewして作ってしまっているため、DIでロガーをインジェクトすることが出来ません。

$this->bindInterceptor(
    $this->matcher->any(),
    $this->matcher->annotatedWith('Koriym\Work\Annotation\BenchMark'),
    [new BenchMarker()]
);

なので、この部分をnewではなくDIコンテナからインスタンスをもらってくるように書き換える必要があります。 AppModulerequestInjectionメソッドがその機能を提供しています。

$this->bindInterceptor(
    $this->matcher->any(),
    $this->matcher->annotatedWith('Koriym\Work\Annotation\BenchMark'),
    [$this->requestInjection('Koriym\Work\Interceptor\BenchMarker')]
);

これで、インターセプターにロガーがインジェクトされるようになりました。

実行してみて、var/log/debug.logに実行時間のログが出力されることを確認しましょう。

Clone this wiki locally