リソース

BEAR.SundayアプリケーションはRESTリソースの集合です。

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

リソースクラスはHTTPのメソッドをPHPのメソッドにマップしてPHPのクラスをサービスとして扱います。

class Index extends ResourceObject
{
    public function onGet($a, $b)
    {
        $this->code = 200; // 省略可
        $this['result'] = $a + $b; // $_GET['a'] + $_GET['b']

        return $this;
    }
}
class Todo extends ResourceObject
{
    public function onPost($id, $todo)
    {
        $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に応じて対応可能な値が引数になります。

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

bodyのアクセスは$this->body['price'] = 10;$this['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ページを作成します。

クライアント

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

use BEAR\Sunday\Inject\ResourceInject;

class Index extends ResourceObject
{
    use ResourceInject;

    public function onGet($a, $b)
    {
        $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;

リンクアノテーション

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

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

    /**
     * @Link(rel="profile", href="/profile{?id}")
     */
    public function onGet($id)
    {
        $this['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)

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

埋め込みリソース

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

use BEAR\Resource\Annotation\Embed;

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

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

use BEAR\Resource\Annotation\Embed;

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

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

バインドパラメーター

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

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

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

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

use Ray\WebContextParam\Annotation\QueryParam;

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

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

use Ray\WebContextParam\Annotation\CookieParam;

class News
{
    /**
     * @CookieParam(key="id", param="tokenId")
     */
    public function foo($tokenId = null)
    {
      // $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($userId = null, $tokenId = "0000", $app_mode = null, $token = null, $server = null)
    {
       // $userId   = $_GET['id'];
       // $tokenId  = $_COOKIE['id'] or "0000" when unset;
       // $app_mode = $_ENV['app_mode'];
       // $token    = $_POST['token'];
       // $server   = $_SERVER['SERVER_NAME'];

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

リソースパラメーター

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

use BEAR\Resource\Annotation\ResourceParam;

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

この例ではメソッドが呼ばれると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($id)
    {
        // read
    }

    public function onPost($id, $name)
    {
        // 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($id, $name, $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)

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

クエリージポジトリに格納されているデータは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($status)
    {
        $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($id)
    {
        $nickname = $this->resource
            ->get
            ->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($id, $name = null)
    {
        $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($id, $name = null)
    {
        $this['profile']->addQuery(['name'=>$name]);

        return $this;
    }
}

BEAR.Resource

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