Skip to content

Latest commit

 

History

History
514 lines (401 loc) · 16.2 KB

README.ja.md

File metadata and controls

514 lines (401 loc) · 16.2 KB

BEAR.Resource

Hypermedia framework for object as a service

Scrutinizer Code Quality codecov Build Status Build Status

BEAR.Resource はオブジェクトがリソースの振る舞いを持つHypermediaフレームワークです。 クライアントーサーバー、統一インターフェイス、ステートレス、相互接続したリソース表現、レイヤードコンポーネント等の RESTのWebサービスの特徴をオブジェクトに持たせます。

既存のドメインモデルやアプリケーションの持つ情報を柔軟で長期運用を可能にするために、 アプリケーションをRESTセントリックなものにしAPI駆動開発を可能にします。

リソースオブジェクト

リソースとして振る舞うオブジェクトがリソースオブジェクトです。

  • 1つのURIのリソースが1クラスにマップされ、リソースクライアントを使ってリクエストします。
  • 統一されたリソースリクエストに対応したメソッドを持ち名前付き引き数でリクエストします。
  • メソッドはリクエストに応じてリソース状態を変更して自身$thisを返します。
<?php
namespace MyVendor\Sandbox\Blog;

class Author extends ResourceObject
{
    public $code = 200;

    public $headers = [
        'Content-Type' => 'application/json'
    ];

    public $body = [
        'id' =>1,
        'name' => 'koriym'
    ];

    /**
     * @Link(rel="blog", href="app://self/blog/post?author_id={id}")
     */
    public function onGet(int $id) : ResourceObject
    {
        return $this;
    }

    public function onPost(string $name) : ResourceObject
    {
        $this->code = 201; // created
        // ...
        return $this;
    }

    public function onPut(int $id, string $name) : ResourceObject
    {
        $this->code = 203; // no content
        //...
        return $this;
    }

    public function onDelete($id) : ResourceObject
    {
        $this->code = 203; // no content
        //...
        return $this;
    }
}

インスタンスの取得

ディペンデンシーインジェクターを使ってクライアントインスタンスを取得します。

use BEAR\Resource\ResourceInterface;

$resource = (new Injector(new ResourceModule('FakeVendor/Sandbox')))->getInstance(ResourceInterface::class);

リソースリクエスト

URIとクエリーを使ってリソースをリクエストします。

$user = $resource->get('app://self/user', ['id' => 1]);
  • このリクエストはPSR0に準拠した MyVendor\Sandbox\Resource\App\User クラスの onGet($id) メソッドに1を渡します。
  • 得られたリソースは code, headers それに bodyの3つのプロパティを持ちます。
var_dump($user->body);
Array
(
 [name] => Athos
 [age] => 15
 [blog_id] => 0
)

リソースリクエストには2種類の方法があります。即時実行(eager request)と遅延実行(lazy request)です。

即時実行

即時実行は通常のPHPのメソッドと同様にすぐに実行して結果を求めます。いくつかの書き方があります。

$user = $resource
  ->get
  ->uri('app://self/user')
  ->withQuery(['id' => 1])
  ->eager
  ->request();
$user = $resource->get->uri('app://self/user')(['id' => 1]);
$user = $resource->uri('app://self/user')(['id' => 1]); // 'get' request method can be omitted

遅延実行

遅延評価として Request to resource until the point at which it is needed. One common use case is that assign to template the resource request object not the instance.

// get `ResourceRequest` objcet 
$user = $resource->get->uri('app://self/user')->withQuery(['id' => 1]);
// assign to the template
echo "User resource body is {$user}"; // same in the template enigne template

It is callable object, you can invoke with other parameters;

// invoke
$user1 = $user(); // $id = 1
$user2 = $user(['id' => 2);

ハイパーメディア

リソースは関連するリソースの ハイパーリンクを持つ事ができます @Linkアノテーションをメソッドにアノテートしてハイパーリンクを表します。

use BEAR\Resource\Annotation\Link;

/**
 * @Link(rel="blog", href="app://self/blog?author_id={id}")
 */

rel でリレーション名を href (hyper reference)でリンク先URIを指定します。 URIは URIテンプレート(rfc6570)を用いて現在のリソースの値をアサインすることができます。

リンクには self, new, crawl といくつか種類があり効果的にリソースグラフを作成することができます。

selfリンク

linkSelfはリンク先のリソースを取得します。

$blog = $resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 0])
    ->linkSelf('blog')
    ->eager
    ->request();

app://self/user リソースをリクエストした結果で blog リンクを辿り app://self/blogリソースを取得します。 Webページでリンクをクリックしたように次のリソースに入れ替わります。

newリンク

linkNew はリンク先のリソースも追加取得します。

$user = $resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 0])
    ->linkNew('blog')
    ->eager
    ->request();
    
$blog = $user['blog'];

Webページで「新しいウインドウでリンクを表示」を行うように現在のリソースは保持したまま次のリソースを取得します。

クロール

クロールはリスト(配列)になっているリソースを順番にリンクを辿り、複雑なリソースグラフを構成することができます。 クローラーがwebページをクロールするように、リソースクライアントはハイパーリンクをクロールしソースグラフを生成します。

author, post, meta, tag, tag/name がそれぞれ関連づけられてあるリソースグラフを考えてみます。 それぞれのリソースはハイパーリンクを持ちます。 このリソースグラフに post-tree という名前を付け、それぞれのリソースの@Linkアノテーションでハイパーリファレンス href を指定します。

authorリソースにはpostリソースへのハイパーリンクがあります。1:nの関係です。

/**
 * @Link(crawl="post-tree", rel="post", href="app://self/post?author_id={id}")
 */
public function onGet($id = null)

postリソースにはmetaリソースとtagリソースのハイパーリンクがあります。1:nの関係です。

/**
 * @Link(crawl="post-tree", rel="meta", href="app://self/meta?post_id={id}")
 * @Link(crawl="post-tree", rel="tag",  href="app://self/tag?post_id={id}")
 */
public function onGet($author_id)
{

tagリソースはIDだけでそのIDに対応するtag/nameリソースへのハイパーリンクがあります。1:1の関係です。

/**
 * @Link(crawl="post-tree", rel="tag_name",  href="app://self/tag/name?tag_id={tag_id}")
 */
public function onGet($post_id)

クロール名を指定してリクエストします。

$graph = $resource
  ->get
  ->uri('app://self/marshal/author')
  ->linkCrawl('post-tree')
  ->eager
  ->request();

リソースクライアントは@Linkアノテーションに指定されたクロール名を発見するとその rel 名でリソースを接続してリソースグラフを作成します。

var_export($graph->body);

array (
    0 =>
    array (
        'name' => 'Athos',
        'post' =>
        array (
            0 =>
            array (
                'author_id' => '1',
                'body' => 'Anna post #1',
                'meta' =>
                array (
                    0 =>
                    array (
                        'data' => 'meta 1',
                    ),
                ),
                'tag' =>
                array (
                    0 =>
                    array (
                        'tag_name' =>
                        array (
                            0 =>
                            array (
                                'name' => 'zim',
                            ),
                        ),
                    ),
 ...

HATEOAS アプリケーション状態のエンジンとしてのハイパーメディア

リソースはクライアントの次の動作をハイパーリンクにして、クライアントはそのリンクを辿りアプリケーションの状態を変更します。 例えば注文リソースに POST して注文を作成、その注文の状態から支払リソースに PUTして支払を行います。

Order リソース

/**
 * @Link(rel="payment", href="app://self/payment{?order_id, credit_card_number, expires, name, amount}", method="put")
 */
public function onPost($drink)

クライアントコード

    $order = $resource
        ->post
        ->uri('app://self/order')
        ->withQuery(['drink' => 'latte'])
        ->eager
        ->request();

    $payment = [
        'credit_card_number' => '123456789',
        'expires' => '07/07',
        'name' => 'Koriym',
        'amount' => '4.00'
    ];

    // then use hyper link to pay
    $response = $resource->href('payment', $payment);
    
    echo $response->code; // 201

支払の方法は注文リソースがハイパーリンクと提供しています。 支払と注文の関係が変更されてもクライアントコードに変更はありません。 HATEOAS について詳しくはHow to GET a Cup of Coffeeをご覧ください。

リソース表現

リソースはそれぞれ表現のためのリソースレンダラーを持っています。 文字列評価されるとリソースはインジェクトされたリソースレンダラーを使ってリソース表現になります。

echo $user;

// {
//     "name": "Aramis",
//     "age": 16,
//     "blog_id": 1
// }

このときの$userはレンダラーが内蔵されたResourceObjectリソースオブジェクトです。 配列やオブジェクトとしても取り扱うことができます。

echo $user['name'];

// Aramis

echo $user->onGet(2);

// {
//     "name": "Yumi",
//     "age": 15,
//     "blog_id": 2
// }

遅延評価

$user = $resource
  ->get
  ->uri('app://self/user')
  ->withQuery(['id' => 1])
  ->request();

$templateEngine->assign('user', $user);

eagerのないrequest()ではリソースリクエストの結果ではなく、リクエストオブジェクトが取得できます。 テンプレートエンジンにアサインするとテンプレートにリソースリクエスト{$user}が現れたタイミングでリソースリクエストリソースレンダリングを行い文字列表現になります。 リソース表現はAPI用の他にも、テンプレートエンジンを用いてHTMLにする事もできます。

埋め込みリソース

@Embedアノテーションを使って他のリソースを自身のリソースに埋め込む事が出来ます。HTML<img src="image_url"><iframe src="content_url">と同じ様にsrcで埋め込むリソースを指定します。

class News extends ResourceObject
{
    /**
     * @Embed(rel="weather",src="app://self/weather/today")
     */
    public function onGet()
    {
        $this['headline'] = "...";
        $this['sports'] = "...";
        
        return $this;
    }
}

このNewsリソースではheadlinesportsと同様にweatherというリソースのリクエストを埋め込みます。

HAL (Hypertext Application Language)

HAL Moduleを使うとリソース表現がHALになります。リソースに埋め込まれたリクエストはHALでも埋め込みリソースとして評価されます。

    // create resource client with HalModule
    $resource = Injector::create([new ResourceModule('MyVendor\MyApp'), new HalModule])->getInstance('BEAR\Resource\ResourceInterface');
    // request
    $news = $resource
        ->get
        ->uri('app://self/news')
        ->withQuery(['date' => 'today'])
        ->request();
    // output
    echo $news . PHP_EOL;

結果

{
    "headline": "40th anniversary of Rubik's Cube invention.",
    "sports": "Pieter Weening wins Giro d'Italia.",
    "_links": {
        "self": {
            "href": "/api/news?date=today"
        }
    },
    "_embedded": {
        "weather": [
            {
                "today": "the weather of today is sunny",
                "_links": {
                    "self": {
                        "href": "/api/weather?date=today"
                    },
                    "tomorrow": {
                        "href": "/api/weather/tomorrow"
                    }
                }
            }
        ]
    }
}

リソース表現

ResourceObject を文字列評価するとリソース表現(リプレゼンテーション)が取得できます。

$userView = (string) $resource->get('app://self/user?id=1');
echo $userView; // get JSON

レンダラーを変えて、リソースを他のメディアタイプで表現する事ができます。通常はDIでレンダラーを依存として注入します。

class User extends ResourceObject
{
    public function __construct()
    {
        $this->setRenderer(new class implements RenderInterface{
            public function render(ResourceObject $ro)
            {
                $ro->headers['content-type'] = 'application/json';
                $ro->view = json_encode($ro->body);

                return $ro->view;
            }
        });
    }
}

転送

REST は representational state "transfer" の略です。 ResourceObjecttransfer() メソッドでリソースをクライアントに出力します。

$user = $resource->get('app://self/user?id=1');
$user->transfer(new class implements TransferInterface {
	public function __invoke(ResourceObject $ro, array $server)
	{
	    foreach ($ro->headers as $label => $value) {
	        header("{$label}: {$value}", false);
	    }
	    http_response_code($ro->code);
	    echo $ro->view;
	}
);

インストール

composer require bear/resource ^1.10

A Resource Oriented Framework

BEAR.Sunday はリソース指向のフレームワークです。BEAR.Resourceに Webでの振る舞いやアプリケーションスタックの機能を、 Google GuiceスタイルのDI/AOPシステムのRayで追加してフルスタックのWebアプリケーションフレームワークとして機能します。 BEAR.Sunday GitHubをご覧下さい。

See Also

Testing BEAR.Resource

以下はインストールしてテスト実行するための手順です。

composer create-project bear/resource BEAR.Resource
cd BEAR.Resource
./vendor/bin/phpunit
php demo/run.php