リソース

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

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

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

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

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

        return $this;
    }
}
class Todo extends ResourceObject
{
    public function onPost(string $id, string $todo): static
    {
        $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インターフェイスを持ちます。

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

URI

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

URI Class
page://self/index Koriym\Todo\Resource\Page\Index
app://self/blog/posts Koriym\Todo\Resource\App\Blog\Posts
  • page://self/indexのindexは省略する事ができます。page://self/indexとpage://self/は同じです。
  • HTTPのURIと同じようにクエリを使う事もできます。 (app://self/blog/posts/?id=3)

標準ではリソースは二種類用意されています。1つはAppアプリケーションリソースです。アプリケーションのAPIです。 もう1つはPageリソースです。PageリソースはAppリソースを利用してWebページを表現します。

メソッド

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

GET

リソースの状態を取得します。安全なメソッドです。このメソッドではリソースの状態を変えてはいけません。

PUT

リクエストしたURIでリソースの状態を置き換えます。このメソッドは安全ではなくリソースの状態を変更します。 メソッドには冪等性がありメソッドを何度実行しても結果は同じです。

PATCH

リソースを部分的に変更します。

POST

リクエストしたURIに新しいリソースを追加します。このメソッドは安全ではなくリソースの状態を変更します。冪等性はなくリクエストの回数分リソースが追加されます。

DELETE

リソースの削除をします。冪等性があります。

OPTIONS

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

パラメーター

HTTPからリクエストされた時にonGetメソッドの引数には$_GETonPostには$_POSTが変数名に応じて渡されます。例えば下記の$idは$_GET[‘id’]が渡されます。

class Index extends ResourceObject
{
    public function onGet(int $id): static
    {

GET,POST以外のメソッドonPut,onPatch, onDeleteの引数にはリクエストボディの値がcontent-typeヘッダーで指定されたフォーマットで与えられます。 1 例えばapplication/jsonならJSONフォーマットで、x-www-form-urlencodedならURL経由で渡されるクエリ文字列と同じフォーマット key1=val1&key2=vale2&..として扱われます。

パラメーターはネストされたデータ 2 でも構いません。 JSONやネストされたクエリ文字列で送信されたデータは配列やクラスでも受け取る事ができます。

class Index extends ResourceObject
{
    public function onPost(array $user): static
    {
        $name = $user['name']; // bear
class Index extends ResourceObject
{
    public function onPost(User $user): static
    {
        $name = $user->name; // bear

受け取るクラス(Inputクラス)は事前にパラメーターをpublicプロパティにしたものを定義しておきます。

<?php

namespace Vendor\App\Input;

final class User
{
    public $id;
    public $name;
}

ネームスペースは任意です。Inputクラスでは入力データをまとめたり検証したりするメソッドを実装する事ができます。3

final class User
{
    public $givenName;
    public $familyName;
    
    public function getFullName() : string
    {
        return "{$this->givenName} {$this->familyName}";
    }
}

配列受け取りはInputクラスの集合として入力を受け取る時にも便利です。

バインドパラメーター

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

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

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

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

use Ray\WebContextParam\Annotation\QueryParam;

class News extends ResourceObject
{
    /**
     * @QueryParam("id")
     */
    public function foo(string $id): static
    {
      // $id = $_GET['id'];

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

use Ray\WebContextParam\Annotation\CookieParam;

class News extends ResourceObject
{
    /**
     * @CookieParam(key="id", param="tokenId")
     */
    public function foo(string $tokenId): static
    {
      // $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 extends ResourceObject
{
    /**
     * @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'];
    ): static {

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

リソースパラメーター

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

use BEAR\Resource\Annotation\ResourceParam;

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

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

レンダリング

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

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


$weekday = $api->resource->get('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 extends ResourceObject
{
    // ...
    /**
     * @Inject
     * @Named("my_renderer")
     */
    public function setRenderer(RenderInterface $renderer)
    {
        parent::setRenderer($renderer);
    }
}

or

class Index extends ResourceObject
{
    /**
     * @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 extends ResourceObject
{
    // ...
    public function transfer(TransferInterface $responder, array $server)
    {
        $responder($this, $server);
    }
}

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

クライアント

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

use BEAR\Sunday\Inject\ResourceInject;

class Index extends ResourceObject
{
    use ResourceInject;

    public function onGet(): static
    {
        $this->body = [
            'posts' => $this->resource->get('app://self/blog/posts', ['id' => 1])
        ];
    }
}

このリクエストはapp://self/blog/postsリソースに?id=1というクエリーでリクエストを実行します。 この他にも以下の表記があります。

// PHP 5.x and up 
$posts = $this->resource->get->uri('app://self/posts')->withQuery(['id' => 1])->eager->request();
// PHP 7.x and up
$posts = $this->resource->get->uri('app://self/posts')(['id' => 1]);
// getは省略可
$posts = $this->resource->uri('app://self/posts')(['id' => 1]);
// bear/resource 1.11 and up 
$posts = $this->resource->get('app://self/posts', ['id' => 1]);

以上はリクエストをすぐに行うeagerリクエストですが、リクエスト結果ではなくリクエストそのものを取得し、実行を遅延することもできます。

$request = $this->resource->uri('app://self/posts'); // callable
$posts = $request(['id' => 1]);

このリクエストをテンプレートやリソースに埋め込むと、その要素が使用されるときに評価されリクエストが実行されます。つまり評価されない時はリクエストは行われず実行コストがかかりません。

リンクリクエスト

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

$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) リンクをクロールして”リソースツリー”を作成します。

クロールリンク

ツリー構造をもつリソース、例えばユーザーからブログ、ブログから記事、記事からコメント、コメントから評価にリンクされているようなツリー構造のリソースはlinkCrawl()で上手く取得できます。 詳しくはクロールをご覧ください。

リンクアノテーション

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

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

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

        return $this;
    }

@LinkhrefのURIの{?id}$bodyの値です。(メソッドの$idではありません)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): static

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

埋め込みリソース

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

use BEAR\Resource\Annotation\Embed;

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

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

srcにはURI templateが利用でき、リクエストメソッドの引数がバインドされます。(リソースの$bodyではありません)

use BEAR\Resource\Annotation\Embed;

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

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

リソースキャッシュ

@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 extends ResourceObject
{
    public function onGet(string $id): static
    {
        // read
    }

    public function onPost(string $id, string $name): static
    {
        // 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 extends ResourceObject
{
  /**
   * @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)): static

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

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

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

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

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

クエリーリポジトリに格納されているデータは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->repository->purge(new Uri('app://self/ad/?id={id}', ['id' => 1]));

        // 読み込み
        list($code, $headers, $body, $view) = $this->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): static
    {
        $this->body = [
            'todos' => $this->resource->uri('app://self/todos')(['status' => $status]) // lazy request
        ];

        return $this;
    }
}

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

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

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

    public function onPost(string $title): static
    {
        $this->resource->post('app://self/todo', ['title' => $title]);
        $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): static
    {
        $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): static
    {
        $nickname = $this->resource->get('app://self/login-user', ['id' => $id])->body['nickname'];
        $this->body = [
            'profile'=> $this->resource->get('app://self/profile', ['name' => $nickname])->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): static
    {
        $this->body = [
            'profile' => $this->resource->get('app://self/profile', ['name' => $name])->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): static
    {
        $this->body['profile']->addQuery(['name'=>$name]);

        return $this;
    }
}

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

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

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

BEAR.Resource

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


  1. PUT メソッドのサポート参照 

  2. parse_str参照 

  3. publicプロパティとして定義しないで、__set()マジックメソッドでバリデーションをする事もできます。