チュートリアル
このチュートリアルではBEAR.Sundayの基本機能のDI、AOP、REST APIを紹介します。1
プロジェクト作成
年月日を入力すると曜日を返すWebサービスを作成してみましょう。 まずプロジェクトを作成します。
composer create-project bear/skeleton MyVendor.Weekday
vendor名をMyVendor
にproject名をWeekday
として入力します。2
リソース
最初にアプリケーションリソースファイルをsrc/Resource/App/Weekday.php
に作成します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Resource\App;
use BEAR\Resource\ResourceObject;
use DateTimeImmutable;
class Weekday extends ResourceObject
{
public function onGet(int $year, int $month, int $day): static
{
$dateTime = DateTimeImmutable::createFromFormat('Y-m-d', "$year-$month-$day");
$weekday = $dateTime->format('D');
$this->body = ['weekday' => $weekday];
return $this;
}
}
このMyVendor\Weekday\Resource\App\Weekday
リソースクラスは/weekday
というパスでアクセスすることができます。
GET
メソッドのクエリーがonGet
メソッドの引数に渡されます。
コンソールでアクセスしてみましょう。まずはエラーを試してみます。
php bin/app.php get /weekday
400 Bad Request
content-type: application/vnd.error+json
{
"message": "Bad Request",
"logref": "e29567cd",
エラーはapplication/vnd.error+jsonメディアタイプで返されます。
400はリクエストに問題があるエラーコードです。エラーにはlogref
IDがつけられvar/log/
でエラーの詳しい内容を参照することができます。
次は引数をつけて正しいリクエストを試します。
php bin/app.php get '/weekday?year=2001&month=1&day=1'
200 OK
Content-Type: application/hal+json
{
"weekday": "Mon",
"_links": {
"self": {
"href": "/weekday?year=2001&month=1&day=1"
}
}
}
application/hal+json
というメディアタイプで結果が正しく返って来ました。
これをWeb APIサービスにしてみましょう。 Built-inサーバーを立ち上げます。
php -S 127.0.0.1:8080 bin/app.php
curl
でHTTPのGET
リクエストを行って確かめてみましょう。
curl -i 'http://127.0.0.1:8080/weekday?year=2001&month=1&day=1'
HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Fri, 01 Sep 2017 09:31:13 +0200
Connection: close
X-Powered-By: PHP/7.1.8
content-type: application/hal+json
{
"weekday": "Mon",
"_links": {
"self": {
"href": "/weekday/2001/1/1"
}
}
}
このリソースクラスにはGET以外のメソッドは用意されていないので、他のメソッドを試すと405 Method Not Allowed
が返されます。これも試してみましょう。
curl -i -X POST 'http://127.0.0.1:8080/weekday?year=2001&month=1&day=1'
HTTP/1.1 405 Method Not Allowed
...
HTTP OPTIONS
メソッドリクエストで利用可能なHTTPメソッドと必要なパラメーターを調べることができます。(RFC7231)
curl -i -X OPTIONS http://127.0.0.1:8080/weekday
HTTP/1.1 200 OK
...
Content-Type: application/json
Allow: GET
{
"GET": {
"parameters": {
"year": {
"type": "integer"
},
"month": {
"type": "integer"
},
"day": {
"type": "integer"
}
},
"required": [
"year",
"month",
"day"
]
}
}
テスト
PHPUnitを使ったリソースのテストを作成しましょう。
tests/Resource/App/WeekdayTest.php
に以下のテストコードを記述します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Resource\App;
use BEAR\Resource\ResourceInterface;
use MyVendor\Weekday\Injector;
use PHPUnit\Framework\TestCase;
class WeekdayTest extends TestCase
{
private ResourceInterface $resource;
protected function setUp(): void
{
$injector = Injector::getInstance('app');
$this->resource = $injector->getInstance(ResourceInterface::class);
}
public function testOnGet(): void
{
$ro = $this->resource->get('app://self/weekday', ['year' => '2001', 'month' => '1', 'day' => '1']);
$this->assertSame(200, $ro->code);
$this->assertSame('Mon', $ro->body['weekday']);
}
}
setUp()
ではコンテキスト(app)を指定するとアプリケーションのどのオブジェクトでも生成できるアプリケーションのインジェクター
Injector
を使ってリソースクライアント(ResourceInterface
)を取得していて、テストメソッドtestOnGet
でリソースをリクエストしてテストします。
実行してみましょう。
./vendor/bin/phpunit
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 00:00.281, Memory: 14.00 MB
インストールされたプロジェクトには他にはテストやコード検査を実行するコマンドが用意されています。
テストカバレッジを取得するにはcomposer coverage
を実行します。
composer coverage
pcovはより高速にカバレッジ計測を行います。
composer pcov
カバレッジの詳細をbuild/coverage/index.html
をWebブラウザで開くことで見ることができます。
コーディングスタンダードにしたがっているかのチェックはcomposer cs
コマンドで確認できます。
その自動修正はcomposer cs-fix
コマンドでできます。
composer cs
composer cs-fix
静的解析はcomposer sa
コマンドでおこないます。
これまでのコードで実行してみましょう。以下のエラーがphpstanで検出されます。
------ ---------------------------------------------------------
Line src/Resource/App/Weekday.php
------ ---------------------------------------------------------
15 Cannot call method format() on DateTimeImmutable|false.
------ ---------------------------------------------------------
先程のコードはDateTimeImmutable::createFromFormat
で不正な値(年が-1など)を渡した時にfalseが帰ってくる事を考慮していませんでした。
試してみましょう。
php bin/app.php get '/weekday?year=-1&month=1&day=1'
PHPエラーが発生した場合でもエラーハンドラーがキャッチして、正しいapplication/vnd.error+json
メディアタイプでエラーメッセージが表示されていますが、
静的解析の検査をパスするにはDateTimeImmutable
の結果をassert
するか型を検査して例外を投げるコードを追加します。
assertの場合
$dateTime =DateTimeImmutable::createFromFormat('Y-m-d', "$year-$month-$day");
assert($dateTime instanceof DateTimeImmutable);
例外を投げる場合
まず専用の例外src/Exception/InvalidDateTime.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Exception;
use RuntimeException;
class InvalidDateTime extends RuntimeException
{
}
値の検査をしたコードに修正します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Resource\App;
use BEAR\Resource\ResourceObject;
+use DateTimeImmutable;
use MyVendor\Weekday\Exception\InvalidDateTime;
class Weekday extends ResourceObject
{
public function onGet(int $year, int $month, int $day): static
{
$dateTime = DateTimeImmutable::createFromFormat('Y-m-d', "$year-$month-$day");
+ if (! $dateTime instanceof DateTimeImmutable) {
+ throw new InvalidDateTime("$year-$month-$day");
+ }
$weekday = $dateTime->format('D');
$this->body = ['weekday' => $weekday];
return $this;
}
}
テストも追加します。
+ public function tesInvalidDateTime(): void
+ {
+ $this->expectException(InvalidDateTime::class);
+ $this->resource->get('app://self/weekday', ['year' => '-1', 'month' => '1', 'day' => '1']);
+ }
例外作成のベストプラクティス:
入力のミスのために起こった例外なので、コード自身には問題がありません。このような実行時になって判明する例外はRuntimeException
です。それを拡張して専用の例外を作成しました。
反対に例外の発生がバグによるものでコードの修正が必要ならLogicException
を拡張して例外を作成します。
これで $dateTime->format('D');
の実行時に$dateTime
にfalseが入る可能性がなくなりました。このように値を逐一検査するプログラミングを防御的プログラミング(defensive programming)と呼びます。
phpstanやpsalmの静的解析ツールが役立ちます。しかし、この検査がプロジェクトが求めるものに対してあまりにも厳しすぎると感じるなら設定ファイルの値を変えて検査レベルを変えてもいいでしょう。
composer tests
は composer test
に加えて、コーディング規約(cs)、静的解析の検査(sa)も行います。コミット前に行うのが良いでしょう。
composer tests
ルーティング
デフォルトのルーターはURLをディレクトリにマップするWebRouter
です。
ここでは動的なパラメーターをパスで受け取るためにAuraルーターを使用します。
最初にcomposerでインストールします。
composer require bear/aura-router-module ^2.0
次にsrc/Module/AppModule.php
でAuraRouterModule
をPackageModule
の前でインストールします。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Module;
use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;
+use BEAR\Package\Provide\Router\AuraRouterModule;
use function dirname;
class AppModule extends AbstractAppModule
{
protected function configure(): void
{
(new Dotenv())->load(dirname(__DIR__, 2));
+ $appDir = $this->appMeta->appDir;
+ $this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
$this->install(new PackageModule());
}
}
ルータースクリプトファイルをvar/conf/aura.route.php
に設置します。
<?php
/**
* @see http://bearsunday.github.io/manuals/1.0/ja/router.html
* @var \Aura\Router\Map $map
*/
$map->route('/weekday', '/weekday/{year}/{month}/{day}');
試してみましょう。
php bin/app.php get /weekday/1981/09/08
200 OK
Content-Type: application/hal+json
{
"weekday": "Tue",
"_links": {
"self": {
"href": "/weekday/1981/09/08"
}
}
}
DI
求めた曜日をログする機能を追加してみましょう。
まず曜日をログするsrc/MyLoggerInterface.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday;
interface MyLoggerInterface
{
public function log(string $message): void;
}
リソースはこのログ機能を使うように変更します。
<?php
namespace MyVendor\Weekday\Resource\App;
use BEAR\Resource\ResourceObject;
use MyVendor\Weekday\MyLoggerInterface;
class Weekday extends ResourceObject
{
+ public function __construct(public MyLoggerInterface $logger)
+ {
+ }
public function onGet(int $year, int $month, int $day) : ResourceObject
{
$weekday = \DateTime::createFromFormat('Y-m-d', "$year-$month-$day")->format('D');
$this->body = [
'weekday' => $weekday
];
+ $this->logger->log("$year-$month-$day {$weekday}");
return $this;
}
}
Weekday
クラスはロガーサービスをコンストラクタで受け取って利用しています。
このように必要なもの(依存)をnew
で生成したりコンテナから取得しないで、外部から代入してもらう仕組みを DI といいます。
次にMyLoggerInterface
をMyLogger
に実装します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday;
use BEAR\AppMeta\AbstractAppMeta;
use function error_log;
use const PHP_EOL;
class MyLogger implements MyLoggerInterface
{
private string $logFile;
public function __construct(AbstractAppMeta $meta)
{
$this->logFile = $meta->logDir . '/weekday.log';
}
public function log(string $message): void
{
error_log($message . PHP_EOL, 3, $this->logFile);
}
}
MyLogger
を実装するためにはアプリケーションのログディレクトリの情報(AbstractAppMeta
)が必要ですが、これも依存
としてコンストラクタで受け取ります。
つまりWeekday
リソースはMyLogger
に依存していますが、MyLogger
もログディレクトリ情報を依存にしています。このようにDIで構築されたオブジェクトは、依存が依存を..と繰り返し依存の代入が行われます。
この依存解決を行うのがDIツール(dependency injector)です。
DIツールでMyLoggerInterface
とMyLogger
を束縛(bind)するためにsrc/Module/AppModule.php
のconfigure
メソッドを編集します。
class AppModule extends AbstractAppModule
{
protected function configure(): void
{
(new Dotenv())->load(dirname(__DIR__, 2));
$appDir = $this->appMeta->appDir;
$this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
+ $this->bind(MyLoggerInterface::class)->to(MyLogger::class);
$this->install(new PackageModule());
}
}
これでどのクラスでもコンストラクタでMyLoggerInterface
でロガーを受け取ることができるようになりました。
実行してvar/log/cli-hal-api-app/weekday.log
に結果が出力されていることを確認しましょう。
php bin/app.php get /weekday/2011/05/23
cat var/log/cli-hal-api-app/weekday.log
AOP
メソッドの実行時間を計測するためのベンチマーク処理を考えてみます。
$start = microtime(true);
// メソッド実行
$time = microtime(true) - $start;
ベンチマークを行う度にこのコードを付加して、不要になれば取り除くのは大変です。 アスペクト指向プログラミング(AOP)はこのようなメソッドの前後の特定処理をうまく合成することが出来ます。
まずAOPを実現するためにメソッドの実行を横取り(インターセプト)してベンチマークを行うインターセプターをsrc/Interceptor/BenchMarker.php
に作成します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Interceptor;
use MyVendor\Weekday\MyLoggerInterface;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
use function microtime;
use function sprintf;
class BenchMarker implements MethodInterceptor
{
public function __construct(private MyLoggerInterface $logger)
{
}
public function invoke(MethodInvocation $invocation)
{
$start = microtime(true);
$result = $invocation->proceed(); // 元のメソッドの実行
$time = microtime(true) - $start;
$message = sprintf('%s: %0.5f(µs)', $invocation->getMethod()->getName(), $time);
$this->logger->log($message);
return $result;
}
}
元のメソッドを横取りしたインターセプターのinvoke
メソッドでは、元メソッドの実行を$invocation->proceed();
で行うことができます。
その前後にタイマーのリセット、計測記録の処理を行います。(メソッド実行オブジェクトMethodInvocation $invocation
から元メソッドのオブジェクトとメソッドの名前を取得しています。)
次にベンチマークをしたいメソッドに目印をつけるためのアトリビュートをsrc/Annotation/BenchMark.php
に作成します。
<?php
declare(strict_types=1);
namespace MyVendor\Weekday\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class BenchMark
{
}
AppModule
でMatcherを使ってインターセプターを適用するメソッドを束縛(バインド)します。
+use MyVendor\Weekday\Annotation\BenchMark;
+use MyVendor\Weekday\Interceptor\BenchMarker;
class AppModule extends AbstractAppModule
{
protected function configure(): void
{
(new Dotenv())->load(dirname(__DIR__, 2));
$appDir = $this->appMeta->appDir;
$this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
$this->bind(MyLoggerInterface::class)->to(MyLogger::class);
+ $this->bindInterceptor(
+ $this->matcher->any(), // どのクラスでも
+ $this->matcher->annotatedWith(BenchMark::class), // #[Attribute]と属性の付けられたメソッドに
+ [BenchMarker::class] // BenchMarkerインターセプターを適用
+ );
$this->install(new PackageModule());
}
}
ベンチマークを行いたいメソッドに#[BenchMark]
とアトリビュートを加えます。
+use MyVendor\Weekday\Annotation\BenchMark;
class Weekday extends ResourceObject
{
+ #[BenchMark]
public function onGet(int $year, int $month, int $day): static
{
これで計測したいメソッドに#[BenchMark]
とアトリビュートを加えればいつでもベンチマークできるようになりました。
アトリビュートとインターセプターによる機能追加は柔軟です。対象メソッドやメソッドを呼ぶ側に変更はありません。 アトリビュートはそのままでも束縛を外せばベンチマークを行いません。例えば、開発時にのみ束縛を行い特定の秒数を越すと警告を行うこともできます。
実行してvar/log/weekday.log
に実行時間のログが出力されることを確認しましょう。
php bin/app.php get '/weekday/2015/05/28'
cat var/log/cli-hal-api-app/weekday.log
HTML
次に今のAPIアプリケーションをHTMLアプリケーションにしてみましょう。
今のapp
リソースに加えて、src/Resource/Page/Index.php
にpage
リソースを追加します。
page
リソースクラスは場所と役割が違うだけでapp
リソースと基本的に同じクラスです。
<?php
namespace MyVendor\Weekday\Resource\Page;
use BEAR\Resource\Annotation\Embed;
use BEAR\Resource\ResourceObject;
class Index extends ResourceObject
{
/**
* @Embed(rel="weekday", src="app://self/weekday{?year,month,day}")
*/
public function onGet(int $year, int $month, int $day) : ResourceObject
{
$this->body += [
'year' => $year,
'month' => $month,
'day' => $day
];
return $this;
}
}
@Embed
アノテーションでapp://self/weekday
リソースをbodyのweekdayキーに埋め込んでいます。+=で配列をmergeしているのはonGet実行前に@Embed
でbodyに埋め込まれたweekdayと合成するためです
その際にクエリーをURI template (RFC6570)を使って{?year,month,day}
として同じものを渡しています。
下記の2つのURI templateは同じURIを示しています。
app://self/weekday{?year,month,day}
app://self/weekday?year={year}&month={month}&day={day}
<iframe>
や<img>
タグで他のリソースを含むページをイメージしてください。これらもHTMLページが画像や他のHTMLなどのリソースを自身に埋め込んでいます。
@Embed
でリソースを埋め込むかわりにuse ResourceInject;
でresource
リソースクライアントをインジェクトしてそのクライアントでappリソースをセットすることもできます。3
<?php
namespace MyVendor\Weekday\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
class Index extends ResourceObject
{
use ResourceInject;
public function onGet(int $year, int $month, int $day) : ResourceObject
{
$params = get_defined_vars(); // ['year' => $year, 'month' => $month, 'day' => $day]
$this->body = $params + [
'weekday' => $this->resource->get('app://self/weekday', $params)
];
return $this;
}
}
このリソースがどのような表現になるのか試してみましょう。
php bin/page.php get '/?year=2000&month=1&day=1'
200 OK
content-type: application/hal+json
{
"year": 2000,
"month": 1,
"day": 1,
"_embedded": {
"weekday": {
"weekday": "Sat"
}
},
"_links": {
"self": {
"href": "/index?year=2000&month=1&day=1"
}
}
}
他のリソースが_embedded
されているのが確認できます。
リソースはapplication/hal+json
メディアタイプで出力されていますが、これをHTML(text/html)で出力するためにHTMLモジュールをインストールします。HTMLのマニュアル参照。
composerインストール
composer require madapaja/twig-module ^2.0
src/Module/HtmlModule.php
を作成します。
<?php
namespace MyVendor\Weekday\Module;
use Madapaja\TwigModule\TwigErrorPageModule;
use Madapaja\TwigModule\TwigModule;
use Ray\Di\AbstractModule;
class HtmlModule extends AbstractModule
{
protected function configure()
{
$this->install(new TwigModule);
$this->install(new TwigErrorPageModule);
}
}
templates
フォルダをコピーします
cp -r vendor/madapaja/twig-module/var/templates var
bin/page.php
を変更してコンテキストをhtml-app
にします。
<?php
use MyVendor\Weekday\Bootstrap;
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())(PHP_SAPI === 'cli' ? 'cli-html-app' : 'html-app', $GLOBALS, $_SERVER));
これでtext/html
出力の準備はできました。
最後にvar/templates/Page/Index.html.twig
ファイルを編集します。
{% extends 'layout/base.html.twig' %}
{% block title %}Weekday{% endblock %}
{% block content %}
The weekday of {{ year }}/{{ month }}/{{ day }} is {{ weekday.weekday }}.
{% endblock %}
準備完了です。まずはコンソールでこのようなHTMLが出力されるか確認してみましょう。
php bin/page.php get '/?year=1991&month=8&day=1'
200 OK
content-type: text/html; charset=utf-8
<!doctype html>
...
もしこの時htmlが表示されなければ、テンプレートエンジンのエラーが発生しています。
その時はログファイル(var/log/app.cli-html-app.log
)でエラーを確認しましょう。
次にWebサービスを行うためにpublic/index.php
も変更します。
<?php
use MyVendor\Weekday\Bootstrap;
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'html-app' : 'prod-html-app', $GLOBALS, $_SERVER));
PHPサーバーを立ち上げてwebブラウザでhttp://127.0.0.1:8080/?year=2001&month=1&day=1をアクセスして確認してみましょう。
php -S 127.0.0.1:8080 public/index.php
コンテキストはアプリケーションの実行モードのようなもので、複数指定できます。試してみましょう。
<?php
use MyVendor\Weekday\Bootstrap;
// JSONアプリケーション (最小)
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER));
<?php
use MyVendor\Weekday\Bootstrap;
// プロダクション用HALアプリケーション
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('prod-hal-app', $GLOBALS, $_SERVER));
prod
コンテクストを付加した時はコンテキストに応じたインスタンスを生成するPHPコードが生成されます。アプリケーションのvar/tmp/{context}/di
フォルダを確認してみましょう。
これらのファイルは普段見る必要はありませんが、インスタンスがどのように作られているかを確認することができます。
コンテキストを変える事でどの依存がどう変わったかをdiff
コマンドで調べることもできます。
diff -q var/tmp/prod-app/di/ var/tmp/prod-hal-app/di/
REST API
sqlite3を使ったアプリケーションリソースを作成してみましょう。
まずはコンソールでvar/db/todo.sqlite3
にDBを作成します。
mkdir var/db
sqlite3 var/db/todo.sqlite3
sqlite> create table todo(id integer primary key, todo, created_at);
sqlite> .exit
データベースはAuraSqlや, Doctrine Dbal、CakeDBなどから選べますが ここではCakePHP3でも使われているCakeDBをインストールしてみましょう。
composer require ray/cake-database-module ^1.0
src/Module/AppModule::configure()
でモジュールのインストールをします。
<?php
// ...
use Ray\CakeDbModule\CakeDbModule; // add this line
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$dbConfig = [
'driver' => 'Cake\Database\Driver\Sqlite',
'database' => $appDir . '/var/db/todo.sqlite3'
];
$this->install(new CakeDbModule($dbConfig));
$this->install(new PackageModule());
}
}
セッターメソッドのtrait DatabaseInject
を使うと$this->db
でCakeDBオブジェクトを使えます。
Todoリソースをsrc/Resource/App/Todos.php
に設置します。
<?php
namespace MyVendor\Weekday\Resource\App;
use BEAR\Package\Annotation\ReturnCreatedResource;
use BEAR\RepositoryModule\Annotation\Cacheable;
use BEAR\Resource\ResourceObject;
use Ray\CakeDbModule\Annotation\Transactional;
use Ray\CakeDbModule\DatabaseInject;
/**
* @Cacheable
*/
class Todos extends ResourceObject
{
use DatabaseInject;
public function onGet(int $id) : ResourceObject
{
$this->body = $this
->db
->newQuery()
->select('*')
->from('todo')
->where(['id' => $id])
->execute()
->fetch('assoc');
return $this;
}
/**
* @Transactional
* @ReturnCreatedResource
*/
public function onPost(string $todo) : ResourceObject
{
$statement = $this->db->insert(
'todo',
['todo' => $todo, 'created_at' => new \DateTime('now')],
['created_at' => 'datetime']
);
// created
$this->code = 201;
// hyperlink
$id = $statement->lastInsertId();
$this->headers['Location'] = '/todos?id=' . $id;
return $this;
}
/**
* @Transactional
*/
public function onPut(int $id, string $todo) : ResourceObject
{
$this->db->update(
'todo',
['todo' => $todo],
['id' => $id]
);
// no content
$this->code = 204;
return $this;
}
}
アノテーションに注目してください。クラスに付いている@Cacheable
はこのリソースのGETメソッドがキャッシュ可能なことを示しています。
onPost
やonPut
の@Transactional
はデータベースアクセスのトランザクションを示しています。
onPost
の@ReturnCreatedResource
は作成したリソースをbodyに含みます。
この時Location
ヘッダーのURIで実際にonGet
がコールされるのでLocation
ヘッダーの内容が正しいことが保証されると同時にonGet
をコールすることでキャッシュも作られます。
POST
してみましょう。
まずキャッシュを有効にしたテストをするためにbin/test.php
コンテキストのブートファイルbin/test.php
を作成します。
<?php
use MyVendor\Weekday\Bootstrap;
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('cli-prod-hal-api-app', $GLOBALS, $_SERVER));
コンソールコマンドでリクエストします。POST
ですが便宜上クエリーの形でパラメーターを渡します。
php bin/test.php post '/todos?todo=shopping'
201 Created
Location: /todos?id=1
{
"id": "1",
"todo": "shopping",
"created": "2017-06-04 15:58:03",
"_links": {
"self": {
"href": "/todos?id=1"
}
}
}
ステータスコードは201 Created
。Location
ヘッダーで新しいリソースが/todos/?id=1
に作成された事がわかります。
RFC7231 Section-6.3.2 日本語訳
@ReturnCreatedResource
とアノテートされているのでボディに作成されたリソースを返します。
次にこのリソースをGET
します。
php bin/test.php get '/todos?id=1'
200 OK
ETag: 2527085682
Last-Modified: Sun, 04 Jun 2017 15:23:39 GMT
content-type: application/hal+json
{
"id": "1",
"todo": "shopping",
"created": "2017-06-04 15:58:03",
"_links": {
"self": {
"href": "/todos?id=1"
}
}
}
ハイパーメディアAPIの完成です!APIサーバーを立ち上げましょう。
php -S 127.0.0.1:8081 bin/app.php
curl
コマンドでGETします。
curl -i 'http://127.0.0.1:8081/todos?id=1'
HTTP/1.1 200 OK
Host: 127.0.0.1:8081
Date: Sun, 04 Jun 2017 18:02:55 +0200
Connection: close
X-Powered-By: PHP/7.1.4
ETag: 2527085682
Last-Modified: Sun, 04 Jun 2017 16:02:55 GMT
content-type: application/hal+json
{
"id": "1",
"todo": "shopping",
"created": "2017-06-04 15:58:03",
"_links": {
"self": {
"href": "/todos?id=1"
}
}
}
何回かリクエストしてLast-Modified
の日付が変わらないことを確認しましょう。この時onGet
メソッド内は実行されていません。(試しにメソッド内でecho
などを追加して確認してみましょう)
expiry
を設定してないCacheable
アノテーションのキャッシュは時間でキャッシュが無効になる事はありません。
onPut($id, $todo)
やonDelete($id)
でリソースの変更が行われるとキャッシュが再生成されます。
次にPUT
メソッドでこのリソースを変更します。
curl -i http://127.0.0.1:8081/todos -X PUT -d "id=1&todo=think"
ボディがない事を示す204 No Content
のレスポンスが返ってきます。
HTTP/1.1 204 No Content
...
Content-Type
ヘッダーでメディアタイプを指定する事ができます。application/json
でも試してみましょう。
curl -i http://127.0.0.1:8081/todos -X PUT -H 'Content-Type: application/json' -d '{"id": 1, "todo":"think" }'
再度GETを行うとEtag
とLast-Modified
が変わっているのが確認できます。
curl -i 'http://127.0.0.1:8081/todos?id=1'
このLast-Modified
の日付は@Cacheable
で提供されるものです。
アプリケーションが管理したり、データベースのカラムを用意したりする必要はありません。
@Cacheable
を使うと、リソースコンテンツは書き込み用のデータベースとは違うリソースの保存専用の「クエリーリポジトリ」で管理されEtag
やLast-Modified
のヘッダーの付加が自動で行われます。
BEAR
Because Everything is A Resource. BEARでは全てがリソースです。
リソースの識別子URI、統一されたインターフェイス、ステートレスなアクセス、強力なキャッシュシステム、ハイパーリンク、レイヤードシステム、自己記述性。 BEAR.SundayアプリケーションのリソースはこれらのRESTの特徴を備えたもので、再利用性に優れています。
BEAR.SundayはDIで構造を結び、AOPで横断的関心事を結び、RESTの力でアプリケーションのコンポーネントをリソースとして結ぶコネクティングレイヤーのフレームワークです。
-
このプロジェクトのソースコードは各セクション毎にbearsunday/Tutorialにコミットしています。適宜参照してください。 ↩
-
通常はvendor名は個人またはチーム(組織)の名前を入力します。githubのアカウント名やチーム名が適当でしょう。projectにはアプリケーション名を入力します。 ↩
-
最初の
@Embed
を使った方法は宣言型プログラミング(Declative Programming) 、後者は命令型プログラミング(Imperative Programming)です。@Embed
を使った前者は簡潔で可読性が高くリソースの関係を良く表しています。 ↩