クイックAPI
このチュートリアルではデータベースを用いたAPI用のパッケージと以下の4つのSQLファイルを使ってWeb APIを作成します。
SELECT id, title, completed FROM task;
SELECT id, title, completed FROM task WHERE id = :id;
INSERT INTO task (title, completed, created) VALUES (:title, :completed, :created);
UPDATE task SET completed = 1 WHERE id = :id;
インストール
API用プロジェクトのスケルトンをcomposerインストールします。
composer create-project bear/skeleton MyVendor.Task
ベンダー名とパッケージ名をそれぞれMyVendor
、Task
と入力します。
What is the vendor name ?
(MyVendor):
What is the project name ?
(MyProject):Task
データベース
データベースパッケージをcomposerインストールします。
cd MyVendor.Task
composer require koriym/db-app-package
php vendor/koriym/db-app-package/bin/install.php
AppModule::configure()
でインストールしているPackageModule
をDbAppPackage
に変更します。
class AppModule extends AbstractAppModule
{
/**
* {@inheritdoc}
*/
protected function configure()
{
// ...
$this->install(new DbAppPackage($_ENV['DB_DSN'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_READ']));
}
}
DbAppPackage
はPackageModule
に以下の特定のパッケージを追加したものです。
- Aura.Router v2 Webルーター
- Aura.Sql v2 PDOを拡張したSQLアダプター
- Aura.SqlQuery v2 クエリービルダー
- Phinx データベースマイグレーション
- Koriym.QueryLocator SQLロケーター
- Koriym.DevPdoDtatement SQLクエリー調査のための開発用PDOStatement
- Koriym.Now 現在時刻
接続設定
.env
ファイルでデータベース接続を設定します。DB_DSN
のフォーマットはPDOです。環境に合わせて適宜変更します。
MySQL
DB_DSN=mysql:host=localhost;dbname=task
DB_USER=root
DB_PASS=
DB_READ=
sqlite
DB_DSN=sqlite:/tmp/task.sq3
DB_USER=
DB_PASS=
DB_READ=
スレイブDBを利用する場合は複数のサーバーリストをカンマ区切りでDB_READ
に設定します。
DB_READ=slave1.example.com,slave2.example.com
作成
データベースを作成します。(sqlite
の場合は必要ありません)
php bin/create_db.php
Phinxでマイグレーションファイルの生成を行います。
php vendor/bin/phinx create -c var/db/phinx.php MyNewMigration
作成されたマイグレーション編集します。[?]
var/db/20160222042911_my_new_migration.php
<?php
use Phinx\Migration\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class MyNewMigration extends AbstractMigration
{
public function change()
{
// create the table
$table = $this->table('task');
$table->addColumn('title', 'string', ['limit' => 100])
->addColumn('completed', 'text', ['limit' => MysqlAdapter::INT_TINY])
->addColumn('created', 'datetime')
->create();
}
}
マイグレーションを実行します。
php vendor/bin/phinx migrate -c var/db/phinx.php
php vendor/bin/phinx migrate -c var/db/phinx.php -e test
ルーティング
GET /task/1
WebリクエストをTask::onGet($id)
メソッドにルートするためにvar/conf/aura.route.php
を編集します。
/** @var $router \BEAR\Package\Provide\Router\AuraRoute */
$router->route('/task', '/task/{id}');
POST
やPATCH
もそれぞ対応するメソッドにルートされます。
SQL
SQLファイルを設置します。
var/db/sql/task_list.sql
SELECT id, title, completed FROM task;
var/db/sql/task_item.sql
SELECT id, title, completed FROM task WHERE id = :id;
var/db/sql/task_insert.sql
INSERT INTO task (title, completed, created) VALUES (:title, :completed, :created);
var/db/sql/task_update.sql
UPDATE task SET completed = 1 WHERE id = :id;
リソース
SQLを実行するリソースクラスをsrc/Resource/App/Task.php
に作成します。
declare(strict_types=1);
namespace MyVendor\Task\Resource\App;
use BEAR\Resource\ResourceObject;
use Koriym\Now\NowInject;
use Koriym\QueryLocator\QueryLocatorInject;
use Ray\AuraSqlModule\AuraSqlInject;
class Task extends ResourceObject
{
use AuraSqlInject;
use NowInject;
use QueryLocatorInject;
public function onGet(string $id = null): static
{
$this->body = $id ?
$this->pdo->fetchOne($this->query['task_item'], ['id' => $id]) :
$this->pdo->fetchAssoc($this->query['task_list']);
return $this;
}
public function onPost(string $title): static
{
$params = [
'title' => $title,
'created' => $this->now,
'completed' => false
];
$this->pdo->perform($this->query['task_insert'], $params);
$id = $this->pdo->lastInsertId('id');
$this->code = 201;
$this->headers['Location'] = "/task?id={$id}";
return $this;
}
public function onPatch(string $id): static
{
$params = [
'id' => $id,
'completed' => true
];
$this->pdo->perform($this->query['task_update'], $params);
return $this;
}
}
実行
まずはリソースをコンソールで実行します。
php bin/app.php options /task
php bin/app.php post '/task?title=run'
php bin/app.php patch /task/1
php bin/app.php get /task/1
次に同じリソースをWebでアクセスするためにWebサーバーをスタートさせます。
php -S 127.0.0.1:8080 bin/app.php
curl
コマンドでアクセスします。
curl -i -X OPTIONS http://127.0.0.1:8080/task
curl -i -X POST --form "title=mail" http://127.0.0.1:8080/task
curl -i -X PATCH http://127.0.0.1:8080/task/1
curl -i -X GET http://127.0.0.1:8080/task/1
テスト
リソースの操作をテストするためにTaskリソースのテストコードを/tests/Resource/App/TaskTest.php
に追加します。[?]
namespace MyVendor\Task\Resource\Page;
use BEAR\Resource\ResourceObject;
use Koriym\DbAppPackage\AbstractDatabaseTestCase;
class TaskTest extends AbstractDatabaseTestCase
{
const URI = 'app://self/task';
public function testOnPost()
{
$query = ['title' => 'shopping'];
$page = $this->resource->post->uri(self::URI)->withQuery($query)->eager->request();
$this->assertSame(201, $page->code);
$this->assertArrayHasKey('Location', $page->headers);
return $page;
}
/**
* @depends testOnPost
*/
public function testPatch(ResourceObject $page)
{
$uri = sprintf('app://self%s', $page->headers['Location']);
$page = $this->resource->patch->uri($uri)->eager->request();
$this->assertSame(200, $page->code);
return $page;
}
/**
* @depends testOnPost
*/
public function testGet(ResourceObject $page)
{
$uri = sprintf('app://self%s', $page->headers['Location']);
$page = $this->resource->get->uri($uri)->eager->request();
$this->assertSame('shopping', $page->body['title']);
$this->assertSame('1', $page->body['completed']);
}
}
phpunit
を実行します。
phpunit
...
OK (5 tests, 8 assertions)
composer test
を実行するとコーディングスタイルをチェックするphpcs, phpmdも合わせて実行されます。
composer test
フィクスチャ
フィクスチャとは、アプリケーションやデータベースの初期状態のことです。テストによっては特定のデータベースの状態を前提にしたいことがあります。MySql専用ですがフィクスチャの用意はを簡単です。
まずDBUnitのための接続情報をtests/phpunit.xml
に追加します
<phpunit bootstrap="tests/bootstrap.php">
<php>
<var name="DB_DSN" value="mysql:host=localhost" />
<var name="DB_USER" value="root" />
<var name="DB_PASSWD" value="" />
<var name="DB_DBNAME" value="task_test" />
</php>
```
DBを保存したい状態にしておいて、テストクラスと同じ階層のfixtures
ディレクトリにmysqldump
コマンドで既存のデータベースの状態をdumpします。
mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml
├── TaskTest.php
└── fixtures
├── tag.xml
└── task.xml
複数のxmlは合成され1つのフィクスチャになり、テスト実行前のデータベース状態を再現できます。
スクリプト
再度環境構築をするためにcomposer.json
のscripts
に以下のコードを追加します。
"scripts": {
"setup": [
"php bin/create_db.php",
"php vendor/bin/phinx migrate -c var/db/phinx.php"
],
setupコマンドで環境構築できます。
composer setup
その他の方法
依存をコンストラクタではなく、メソッドのパラメーターで受け取る事もできます。(アシスティッドインジェクション)
declare(strict_types=1);
namespace MyVendor\Task\Resource\App;
use Aura\Sql\ExtendedPdoInterface;
use BEAR\Resource\ResourceObject;
use Koriym\Now\NowInterface;
use Koriym\QueryLocator\QueryLocatorInterface;
use Ray\Di\Di\Assisted;
class Task extends ResourceObject
{
/**
* @Assisted({"pdo", "query"})
*/
public function onGet(string $id = null, ExtendedPdoInterface $pdo = null, QueryLocatorInterface $query = null): static
{
$this->body = $id ?
$pdo->fetchOne($query['task_item'], ['id' => $id]) :
$pdo->fetchAssoc($query['task_list']);
return $this;
}
/**
* @Assisted({"pdo", "query", "now"})
*/
public function onPost(string $title, ExtendedPdoInterface $pdo = null, QueryLocatorInterface $query = null, NowInterface $now = null): static
{
$params = [
'title' => $title,
'created' => (string) $now,
'completed' => false
];
$pdo->perform($query['task_insert'], $params);
$id = $pdo->lastInsertId('id');
$this->code = 201;
$this->headers['Location'] = "/task?id={$id}";
return $this;
}
/**
* @Assisted({"pdo", "query"})
*/
public function onPatch(string $id, ExtendedPdoInterface $pdo = null, QueryLocatorInterface $query = null): static
{
$params = [
'id' => $id,
'completed' => true
];
$pdo->perform($query['task_update'], $params);
return $this;
}
}
条件によって動的に変わるSQLはAura.SqlQueryクエリービルダーを利用するのが良いでしょう。
declare(strict_types=1);
namespace MyVendor\Task\Resource\App;
use Aura\Sql\ExtendedPdoInterface;
use Aura\SqlQuery\Common\InsertInterface;
use Aura\SqlQuery\Common\SelectInterface;
use Aura\SqlQuery\Common\UpdateInterface;
use BEAR\Resource\ResourceObject;
use Koriym\Now\NowInterface;
use Ray\Di\Di\Assisted;
/**
* with assisted injection + query builder
*/
class TaskQb extends ResourceObject
{
/**
* @Assisted({"pdo", "select"})
*/
public function onGet(string $id = null, ExtendedPdoInterface $pdo = null, SelectInterface $select = null): static
{
$select->cols(['id', 'title', 'completed'])->from('task');
if ($id) {
return $this->onGetItem($id, $pdo, $select);
}
$sql = $select->getStatement();
$this->body = $pdo->fetchAssoc($sql);
return $this;
}
/**
* @Assisted({"pdo", "insert", "now"})
*/
public function onPost(string $title, ExtendedPdoInterface $pdo = null, InsertInterface $insert = null, NowInterface $now = null): static
{
$params = [
'title' => $title,
'created' => (string) $now,
'completed' => false
];
$insert
->into('task')
->cols(['title', 'completed', 'created'])
->bindValues($params);
$pdo->perform($insert->getStatement(), $insert->getBindValues());
$name = $insert->getLastInsertIdName('id');
$id = $pdo->lastInsertId($name);
$this->code = 201;
$this->headers['Location'] = "/task?id={$id}";
return $this;
}
/**
* @Assisted({"pdo", "query"})
*/
public function onPatch(string $id, ExtendedPdoInterface $pdo = null, UpdateInterface $update = null): static
{
$values = [
'id' => $id,
'completed' => true
];
$update
->table('task')
->cols(['title', 'completed', 'created'])
->where('id = :id')
->bindValues($values);
$pdo->perform($update->getStatement(), $update->getBindValues());
return $this;
}
private function onGetItem(string $id, ExtendedPdoInterface $pdo, SelectInterface $select): static
{
$select->where('id = :id')->bindValue('id', $id);
$this->body = $pdo->fetchOne($select->getStatement(), $select->getBindValues());
return $this;
}
}
まとめ
DBの接続情報を.env
で設定して、SQLのファイルをHTTPのURLにマップされたリソースから呼び出してWeb APIを作成しました。テストでDB状態を復元するにはフィクスチャを使います。フィクスチャにはphpunit.xml
に接続情報が必要です。
sql
フォルダに集められたSQLは一覧性に優れ、SQL単体テストも容易ですがSQL文を直接リソースクラスに記述することもできます。動的に生成するSQLクエリーはクエリービルダーが向いています。依存はコンストラクタだけではなく、実行時のメソッドでも受け取れます。
このチュートリアルで作成したプロジェクトの作成履歴はMyVendor.Taskのcommitログで見ることができます。