リソース

BEAR.Sundayアプリケーションはリンクで接続されたリソースの集合です。

サービスとしてのオブジェクト

ResourceObjectはHTTPのメソッドがPHPのメソッドにマップされたサービスとしてのオブジェクト (Object as a servie)です。ステートレスなリクエストで自身のリソース状態を表現にして転送します。 (Representational State Transfer)

class Index extends ResourceObject
{
    public $code = 200;
    public $headers = [];

    public function onGet(int $a, int $b) : ResourceObject
    {
        $this->body = [
            'result' => $a + $b  // $_GET['a'] + $_GET['b']
        ];

        return $this;
    }
}
class Todo extends ResourceObject
{
    public function onPost(string $id, string $todo) : ResourceOjbect
    {
        $this->code = 201; // ステータスコード
        $this->headers['Location'] = '/todo/new_id'; // ヘッダー

        return $this;
    }
}

PHPのリソースクラスはWebのURIと同じようなapp://self/blog/posts/?id=3, page://self/indexなどのURIを持ち、HTTPのメソッドに準じたonGet, onPost, onPut, onPatch, onDeleteインターフェイスを持ちます。

メソッドの引数にはonGetには$_GET、onPostには$_POSTが変数名に応じて渡されます、それ以外のonPut,onPatch, onDeleteのメソッドにはcontent-type(x-www-form-urlencoded or application/json)に応じた値が引数になります。

メソッドでは自身のリソース状態code,headers,bodyを変更し$thisを返します。

bodyのシンタックスシュガー

$thisへのarrayアクセスは$this->bodyのアクセスになります。

$this['price'] = 10;
// is same as
$this->body['price'] = 10;

スキーマ

URI Class
page://self/index Koriym\Todo\Resource\Page\Index
app://self/blog/posts Koriym\Todo\Resource\App\Blog\Posts

アプリケーション名がkoriym\todoというアプリケーションの場合、URIとクラスはこのように対応します。 アプリケーションではクラス名の代わりにURIを使ってリソースにアクセスします。

標準ではリソースは二種類用意されています。1つはAppリソースでアプリケーションのプログラミングインタフェース(API)です。 もう1つはPageリソースでHTTPに近いレイヤーのリソースです。PageリソースはAppリソースを利用してWebページを作成します。

メソッド

リソースはHTTPのメソッドに対応した6つのメソッドでアクセスすることができます。

GET

リソースの読み込み。このメソッドではリソースの状態(body)を変えてはいけません。安全なメソッドでサイドエフェクトがありません。

PUT

リソースの作成または変更を行います。メソッドには冪等性があり、メソッドを何度実行しても結果は同じです。

PATCH

部分的な更新を行います。

POST

リソースの作成を行います。メソッドには冪等性はありません。リクエストの回数分、リソースが作成されます。

DELETE

リソースの削除をします。PUTと同じで冪等性があります。

OPTIONS

リソースのリクエストに必要なパラメーターとレスポンスに関する情報を取得します。GETと同じように安全なメソッドです。

レンダリング

ResourceObjectクラスのリクエストメソッド(onGetなど)はリソースがHTMLで表現されるかJSONで表現されるかなどの表現に対して関心を持ちません。 コンテキストによってResourceObjectにインジェクトされたリソースレンダラーがJSONやHTMLにレンダリングしてリソース表現(view)にします。

レンダリングはリソースが文字列評価された時に行われます。


$weekday = $api->resource->uri('app://self/weekday')(['year' => 2000, 'month'=>1, 'day'=>1]);
var_dump($weekday->body); // as array
//array(1) {
//    ["weekday"]=>
//  string(3) "Sat"
//}

echo $weekday; // as string
//{
//    "weekday": "Sat",
//    "_links": {
//    "self": {
//        "href": "/weekday/2000/1/1"
//        }
//    }
//}

コンテキストに応じてインジェクトされるので普段は意識する必要はありません。

リソース特有の表現が必要な時は以下のように独自のレンダラーをインジェクトします。

class Index
{
    // ...
    /**
     * @Inject
     * @Named("my_renderer")
     */
    public function setRenderer(RenderInterface $renderer)
    {
        parent::setRenderer($renderer);
    }
}

or

class Index
{
    /**
     * @Inject
     */
    public function setRenderer(RenderInterface $renderer)
    {
        $this->renderer = new class implements RenderInterface {
            public function render(ResourceObject $ro)
            {
                $ro->headers['content-type'] = 'application/json;';
                $ro->view = json_encode($ro->body);

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

転送

トランスポンダーが表現(view)をクライント(コンソールやWebクライアント)に転送します。 転送は単にheader関数やechoで行われることがほとんどですが、ストリーム出力で転送することもできます。

レンダラーと同じように普段は意識する必要はありません。

リソース特有の転送を行う時は以下のメソッドをオーバーライドします。

class Index
{
    // ...
    public function transfer(TransferInterface $responder, array $server)
    {
        $responder($this, $server);
    }
}

このようにリソースはリクエストによって自身のリソース状態を変更、それを表現にして転送する機能を各クラスが持っています。

クライアント

リソースクライアントを使用して他のリソースのリクエストをします。


use BEAR\Sunday\Inject\ResourceInject;

class Index extends ResourceObject
{
    use ResourceInject;

    public function onGet() : ResourceOjbect
    {
        $this['post'] = $this
            ->resource
            ->get
            ->uri('app://self/blog/posts')
            ->withQuery(['id' => 1])
            ->eager
            ->request();
        ];
    }
}

このリクエストはapp://self/blog/postsリソースに?id=1というクエリーでリクエストをすぐeagerに行います。

リソースのリクエストはlazyとeagerがあります。リクエストにeagerがついてないものがlazyリクエストです。

$posts = $this->resource->get->uri('app://self/posts')->request(); //lazy
$posts = $this->resource->get->uri('app://self/posts')->eager->request(); // eager

lazy request()で帰って来るオブジェクトは実行可能なリクエストオブジェクトです。$posts()で実行することができます。 このリクエストをテンプレートやリソースに埋め込むと、その要素が使用されるときに評価されます。

リンクリクエスト

クラインアントはハイパーリンクで接続されているリソースをリンクすることができます。

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

リンクは3種類あります。$relをキーにして元のリソースのbodyリンク先のリソースが埋め込まれます。

  • linkSelf($rel) リンク先と入れ替わります。
  • linkNew($rel) リンク先のリソースがリンク元のリソースに追加されます
  • linkCrawl($rel) リンクをクロールして”リソースツリー”を作成します。

シンタックスシュガー

eagerリクエストの場合はシンタックスシュガーが利用できます。以下のリクエストは全て同じです。(php7)

$this->resource->get->uri('app://self/user')->withQuery(['id' => 1])->eager->request()->body;
$this->resource->get->uri('app://self/user')(['id' => 1])->body;
$this->resource->uri('app://self/user')(['id' => 1])->body; // getは省略化
$this->resource->uri('app://self/user?id=1')()->body;

PHP7ではクライントでのコードはこのように記述できます。

<?php
use BEAR\Sunday\Inject\ResourceInject;

class Index extends ResourceObject
{
    use ResourceInject;

    public function onGet() : ResourceOjbect
    {
        $this->body = [
            'post' => $this->uri('app://self/blog/posts')(['id' => 1])
        ];
    }
}

リンクアノテーション

他のリソースをハイパーリンクする@Linkと他のリソースを内部に埋め込む@Embedアノテーションが利用できます。

リンクをrelhrefで指定します。halコンテキストではHALのリンクフォーマットとして扱われます。

    /**
     * @Link(rel="profile", href="/profile{?id}")
     */
    public function onGet($id) : ResourceOjbect
    {
        $this->body = [
            'id' => 10
        ];

        return $this;
    }

hrefのURIの{?id}$bodyの値です。RFC6570 URI templateでURIが生成され、HALでの出力は以下のようになります。

{
    "id": 10,
    "_links": {
        "self": {
            "href": "/test"
        },
        "profile": {
            "href": "/profile?id=10"
        }
    }
}

BEARのリソースリクエストではlinkSelf(), linkNew, linkCrawlの時にリソースリンクとして使われます。

use BEAR\Resource\Annotation\Link;

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

linkCrawlcrawlの付いたリンクをクロールしてリソースを集めます。

埋め込みリソース

リソースの中にsrcで指定した別のリソースを埋め込むことができます。

use BEAR\Resource\Annotation\Embed;

class News
{
    /**
     * @Embed(rel="sports", src="/news/sports")
     * @Embed(rel="weater", src="/news/weather")
     */
    public function onGet() : ResourceOjbect

埋め込まれるのはリソースリクエストです。レンダリングの時に実行されますが、その前にaddQuery()メソッドで引数を加えたりwithQuery()で引数を置き換えることができます。

use BEAR\Resource\Annotation\Embed;

class News
{
    /**
     * @Embed(rel="website", src="/website{?id}")
     */
    public function onGet(string $id) : ResourceOjbect
    {
        // ...
        $this['website']->addQuery(['title' => $title]); // 引数追加

HALレンダラーでは_embedded として扱われます。

バインドパラメーター

リソースクラスのメソッドの引数をWebコンテキストや他リソースの状態と束縛することができます。

Webコンテキストパラメーター

$_GET$_COOKIEなどのPHPのスーパーグローバルの値をメソッド内で取得するのではなく、メソッドの引数に束縛することができます。

キーの名前と引数の名前が同じ場合

use Ray\WebContextParam\Annotation\QueryParam;

class News
{
    /**
     * @QueryParam("id")
     */
    public function foo(strin $id) : ResourceOjbect
    {
      // $id = $_GET['id'];

キーの名前と引数の名前が違う場合はkeyparamで指定

use Ray\WebContextParam\Annotation\CookieParam;

class News
{
    /**
     * @CookieParam(key="id", param="tokenId")
     */
    public function foo(string $tokenId) : ResourceOjbect
    {
      // $tokenId = $_COOKIE['id'];

フルリスト

use Ray\WebContextParam\Annotation\QueryParam;
use Ray\WebContextParam\Annotation\CookieParam;
use Ray\WebContextParam\Annotation\EnvParam;
use Ray\WebContextParam\Annotation\FormParam;
use Ray\WebContextParam\Annotation\ServerParam;

class News
{
    /**
     * @QueryParam(key="id", param="userId")
     * @CookieParam(key="id", param="tokenId")
     * @EnvParam("app_mode")
     * @FormParam("token")
     * @ServerParam(key="SERVER_NAME", param="server")
     */
    public function foo(
        string $userId,           // $_GET['id'];
        string $tokenId = "0000", // $_COOKIE['id'] or "0000" when unset;
        string $app_mode,         // $_ENV['app_mode'];
        string $token,            // $_POST['token'];
        string $server            // $_SERVER['SERVER_NAME'];
    ) : ResourceOjbect {

この機能を使うためには引数のデフォルトにnullが必要です。 またクライアントが値を指定した時は指定した値が優先され、束縛した値は無効になります。

リソースパラメーター

@ResourceParamアノテーションを使えば他のリソースリクエストの結果をメソッドの引数に束縛できます。

use BEAR\Resource\Annotation\ResourceParam;

class News
{
    /**
     * @ResourceParam(param=“name”, uri="app://self//login#nickname")
     */
    public function onGet(string $name) : ResoureObject
    {

この例ではメソッドが呼ばれるとloginリソースにgetリクエストを行い$body['nickname']$nameで受け取ります。

リソースキャッシュ

@Cacheable

use BEAR\RepositoryModule\Annotation\Cacheable;

/**
 * @Cacheable
 */
class User extends ResourceObject

@Cacheableとアノテートするとgetリクエストは読み込み用のレポジトリQueryRepositoryが使われ、時間無制限のキャッシュとして機能します。 get以外のリクエストがあると該当するQueryRepositoryのリソースが更新されます。

@Cacheableから読まれるリソースオブジェクトはHTTPに準じたLast-ModifiedETagヘッダーが付加されます。

同一クラスのonGet以外のリクエストメソッドがリクエストされ引数を見てリソースが変更されたと判断するとQueryRepositoryの内容も更新されます。

use BEAR\RepositoryModule\Annotation\Cacheable;

/**
 * @Cacheable
 */
class Todo
{
    public function onGet(string $id) : ResoureObject
    {
        // read
    }

    public function onPost(string $id, string $name) : ResoureObject
    {
        // update
    }
}

例えばこのクラスでは->post(10, 'shopping')というリクエストがあるとid=10QueryRepositoryの内容が更新されます。 この自動更新を利用しない時はupdateをfalseにします。

/**
 * @Cacheable(update=false)
 */

時間を指定するには、expiryを使って、short, mediumあるいはlongのいずれかを指定できます。

/**
 * @Cacheable(expiry="short")
 */

@Purge @Refresh

もう1つの方法は@Purgeアノテーションや、@Refreshアノテーションで更新対象のURIを指定することです。

use BEAR\RepositoryModule\Annotation\Purge;
use BEAR\RepositoryModule\Annotation\Refresh;

class News
{
  /**
   * @Purge(uri="app://self/user/friend?user_id={id}")
   * @Refresh(uri="app://self/user/profile?user_id={id}")
   */
   public function onPut(string $id, string $name, int $age)

別のクラスのリソースや関連する複数のリソースのQueryRepositoryの内容を更新することができます。 @Purgeはリソースのキャッシュを消去し@Refreshはキャッシュの再生成をメソッド実行直後に行います。

uri-templateに与えられる値は他と同様に$bodyにアサインした値が実引数に優先したものです。

use BEAR\RepositoryModule\Annotation\Purge;
use BEAR\RepositoryModule\Annotation\Refresh;

class News
{
  /**
   * @Purge(uri="app://self/user/friend?user_id={id}")
   * @Refresh(uri="app://self/user/profile?user_id={id}")
   */
   public function onPut($id, $name, $age) : ResoureObject

クエリーリポジトリの直接操作

クエリーリポジトリに格納されているデータはQueryRepositoryInterfaceで受け取ったクライアントで直接put(保存)したりgetしたりすることができます。

use BEAR\QueryRepository\QueryRepositoryInterface;

class Foo
{
    public function __construct(QueryRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function foo()
    {
        // 保存
        $this->repository->put($this);
        $this->repository->put($resourceObject);

        // 消去
        $this->repository->purge($resourceObject->uri);
        $this->repository->purge(new Uri('app://self/user'));
        $this->queryRepository->purge(new Uri('app://self/ad/?id={id}', ['id' => 1]));

        // 読み込み
        list($code, $headers, $body, $view) = $repository->get(new Uri('app://self/user'));
     }

ベストプラクティス

RESTではリソースは他のリソースと接続されています。リンクをうまく使うとコードは簡潔になり、読みやすくテストや変更が容易なコードになります。

@Embed

他のリソースの状態をgetする代わりに@Embedでリソースを埋め込みます。

// OK but not the best
class Index extends ResourceObject
{
    use ResourceInject;

    public function onGet(string $status) : ResoureObject
    {
        $this['todos'] = $this->resource
            ->get
            ->uri('app://self/todos')
            ->withQuery(['status' => $status])
            ->eager
            ->request();

        return $this;
    }
}

// Better
class Index extends ResourceObject
{
    /**
     * @Embed(rel="todos", src="app://self/todos{?status}")
     */
    public function onGet(string $status) : ResourceObject
    {
        return $this;
    }
}

他のリソースの状態を変えるときに@Linkで示された次のアクションをhref()(ハイパーリファレンス)を使って辿ります。

// OK but not the best
class Todo extends ResourceObject
{
    use ResourceInject;

    public function onPost(string $title) : ResourceObject
    {
        $this->resource
            ->post
            ->uri('app://self/todo')
            ->withQuery(['title' => $title])
            ->eager
            ->request();
        $this->code = 301;
        $this->headers[ResponseHeader::LOCATION] = '/';

        return $this;
    }
}

// Better
class Todo extends ResourceObject
{
    use ResourceInject;

    /**
     * @Link(rel="create", href="app://self/todo", method="post")
     */
    public function onPost(string $title) : ResourceObject
    {
        $this->resource->href('create', ['title' => $title]);
        $this->code = 301;
        $this->headers[ResponseHeader::LOCATION] = '/';

        return $this;
    }
}

@ResourceParam

他のリソースをリクエストするために他のリソース結果が必要な場合は@ResourceParamを使います。

// OK but not the best
class User extends ResourceObject
{
    use ResourceInject;

    public function onGet(string $id) : ResoureObject
    {
        $nickname = $this->resource
            ->uri('app://self/login-user')
            ->withQuery(['id' => $id])
            ->eager
            ->request()
            ->body['nickname'];
        $this['profile'] = $this->resource
            ->get
            ->uri('app://self/profile')
            ->withQuery(['name' => $nickname])
            ->eager
            ->request()
            ->body;

        return $this;
    }
}

// Better
class User extends ResourceObject
{
    use ResourceInject;

    /**
     * @ResourceParam(param=“name”, uri="app://self//login-user#nickname")
     */
    public function onGet(string $id, string $name) : ResoureObject
    {
        $this['profile'] = $this->resource
            ->get
            ->uri('app://self/profile')
            ->withQuery(['name' => $name])
            ->eager
            ->request()
            ->body;

        return $this;
    }
}

// Best
class User extends ResourceObject
{
    /**
     * @ResourceParam(param=“name”, uri="app://self//login-user#nickname")
     * @Embed(rel="profile", src="app://self/profile")
     */
    public function onGet(string $id, string $name) : ResoureObject
    {
        $this['profile']->addQuery(['name'=>$name]);

        return $this;
    }
}

BEAR.Resource

リソースクラスに関するより詳しい情報はBEAR.ResourceのREADMEもご覧ください。