Skip to content
kenjis edited this page Jul 1, 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

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

$ 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/logPSR-3 のロガーインターフェイスを使用します。

monologcomposerで取得します。

$ composer require monolog/monolog "~1.0"

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

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

src/Module/Provider/MonologLoggerProvider.php

<?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メソッドを使い行います。

src/Module/AppModule.php

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オブジェクトを受け取りプロパティに格納します。ログ出力ではそのロガーを使います。

src/Resource/App/Add.php

<?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)します。インターセプターから元のメソッドを呼びだす時に前後に処理を記述することができます。

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

src/Interceptor/BenchMarker.php

<?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(), // メソッドの指定
            [$this->requestInjection('Koriym\Work\Interceptor\BenchMarker')] // インターセプター
        );
    }
}

AbstractModule::requestInjection()メソッドはここでは、new Koriym\Work\Interceptor\BenchMarker()と同じ動作をします。

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

アノテーションでバインド

名前によるマッチングでメソッドを指定しましたが、次は@BenchMarkとアノテーションをつけたメソッドにインターセプターをバインドするように変更しましょう。

まずはアノテーションクラスを実装します。BEAR.Sundayでは Doctrine Annotation を使います。

アノテーションファイルを作成します。

src/Annotation/BenchMark.php

<?php

namespace Koriym\Work\Annotation;

use Ray\Aop\Annotation;

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

バインドの定義をannotatedWithを使ったものに変更します。

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

計測するメソッドにアノテートします。

src/Resource/App/Add.php

<?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;
    }
}

アノテーションはuse文を使ってフルパスで指定する必要があります。 実行してみて同じように結果が出力されることを確認しましょう。

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

インターセプターにDI

実行時間の出力先を画面ではなくログにするように変更しましょう。 ロガーインターフェイスのバインディングがされているので、新たに設定をすることなく@Injectをアノテートするだけでログオブジェクトを受け取ることができます。

src/Interceptor/BenchMarker.php

<?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;
    }
}

今までは$this->requestInjection('Koriym\Work\Interceptor\BenchMarker')newと同じ動作でしたがここでは依存解決してインスタンスを生成しました。

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

リソース表現

Addリソースはを提供するだけでしたが表現も提供するアプリケーションリソースを作成してみましょう。

src/Resource/App/User.php

<?php

namespace Koriym\Work\Resource\App;

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Annotation\Cache;

class User extends ResourceObject
{
    private $data = [
        0 => ['name' => 'BEAR',   'age' => 10],
        1 => ['name' => 'Sunday', 'age' => 3]
    ];

    /**
     * @Cache(10)
     */
    public function onGet($id = 0)
    {
        $this->body = $this->data[$id];

        return $this;
    }
}

次にリソースを表現にするためのテンプレートを作成します。

src/Resource/App/User.twig

<div>
    <h2>User</h2>
    Name: {{ name }}<br>
    Age: {{ age }}<br>
</div>

ユーザーリソースの値はテンプレートと合成されページにセットされます。@Cacheとアノテートしているので指定した10秒間テンプレートの合成も含めてキャッシュされます。

リソースの埋め込み

次に@Embedアノテーションを使ってユーザーリソースを埋め込んで(セットして)みましょう。

src/Resource/Page/User.php

namespace Koriym\Work\Resource\Page;

use BEAR\Resource\ResourceObject;
use BEAR\Resource\Annotation\Embed;

class User extends ResourceObject
{
    /**
     * @Embed(rel="user", src="app://self/user{?id}")
     */
    public function onGet($id)
    {
        return $this;
    }
}

relで指定された名前でsrcで指定したリソースが埋め込まれます。URIにはonGetで渡された$idをそのままappリソースに渡しています。

URIに動的要素を加える時は RFC-6570URI Template を用います。

この@Embedアノテーションによるリソースのセットは以下のコードと同じ事を行っています。

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

        return $this;
    }

HTMLの<img><script>、それに<iframe>タグをイメージしてみてください。srcで指定される他のリソースを自身のリソースに埋め込んでいます。@Embedタグも同じように埋め込んでいます。

src/Resource/Page/User.twig

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Wlecome to {{ user.name }} page</title>
    <link href="//netdna.bootstrapcdn.com/bootswatch/3.0.0/united/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div>{{ user }}</div>
</div>
</body>
</html>

ページのテンプレートでは{{ user }}でユーザーリソースのプレースフォルダを指定します。プレースフォルダにはアプリケーションリソースがテンプレートと合成されたマイクロコンテンツ(部分的なHTML)が展開されます。

<title>タグでは{{ user.name }}としてユーザーリソースのnameという要素を利用しています。このようにリソースは他のリソースに埋め込まれ文字列として評価される({{ user }})と表現(マイクロコンテンツ)になり、配列として評価される({{ user.name }})とその値が取り出されます。

マイクロコンテンツ

作成したリソースは他のシステムから容易に利用することができます。

my_system.php

<?php
$app = require __DIR__ . '/bootstrap/instance.php';
$user = $app->resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 0])
    ->eager->request();
?>

<!DOCTYPE html>
<head>
    <title>"<?php echo $user['name']; ?>" in my system</title>
</head>
<body>
<?php echo $user; ?>
</body>
</html>

instance.phpはアプリケーションインスタンスでrequireで取得します。初期化は必要ありません。次にリソースクライアントを使ってapp://self/userリソースを取得しています。

Symfony等他のフレームワークやWordPress等他のCMSからBEAR.Sundayで作成したリソースを利用することができます。

コンソールで試してみましょう

$ php my_system.php 
<!DOCTYPE html>
<head>
    <title>"BEAR" in my system</title>
</head>
<body>
<div class="app-user">
    <h2>User</h2>
    Name: BEAR<br>
    Age: 10<br>
</div>
</body>
</html>