リソース
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
メソッドの引数には$_GET
、onPost
には$_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'];
キーの名前と引数の名前が違う場合はkey
とparam
で指定します。
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
アノテーションが利用できます。
@Link
リンクをrel
とhref
で指定します。hal
コンテキストではHALのリンクフォーマットとして扱われます。
/**
* @Link(rel="profile", href="/profile{?id}")
*/
public function onGet($id): static
{
$this->body = [
'id' => 10
];
return $this;
}
@Link
のhref
の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
linkCrawl
はcrawl
の付いたリンクをクロールしてリソースを集めます。
埋め込みリソース
リソースの中に@Embed
のsrc
で指定した別のリソースを埋め込むことができます。
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-Modified
とETag
ヘッダーが付加されます。
同一クラスの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=10
のQueryRepository
の内容が更新されます。
この自動更新を利用しない時は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
他のリソースの状態を変えるときに@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もご覧ください。
-
publicプロパティとして定義しないで、
__set()
マジックメソッドでバリデーションをする事もできます。 ↩