Ray.MediaQuery
Ray.MediaQuery
はDBやWeb APIなどの外部メディアのクエリーのインターフェイスから、クエリー実行オブジェクトを生成しインジェクトします。
- ドメイン層とインフラ層の境界を明確にします。
- ボイラープレートコードを削減します。
- 外部メディアの実体には無関係なので、後からストレージを変更することができます。並列開発やスタブ作成が容易です。
インストール
$ composer require ray/media-query
利用方法
メディアアクセスするインターフェイスを定義します。
データベースの場合
DbQuery
アトリビュートでSQLのIDを指定します。
interface TodoAddInterface
{
#[DbQuery('user_add')]
public function add(string $id, string $title): void;
}
Web APIの場合
WebQuery
アトリビュートでWeb APIのIDを指定します。
interface PostItemInterface
{
#[WebQuery('user_item')]
public function get(string $id): array;
}
APIパスリストのファイルをmedia_query.json
として作成します。
{
"$schema": "https://ray-di.github.io/Ray.MediaQuery/schema/web_query.json",
"webQuery": [
{
"id": "user_item",
"method": "GET",
"path": "https://{domain}/users/{id}"
}
]
}
MediaQueryModuleは、DbQueryConfig
やWebQueryConfig
、またはその両方の設定でSQLやWeb APIリクエストの実行をインターフェイスに束縛します。
use Ray\AuraSqlModule\AuraSqlModule;
use Ray\MediaQuery\ApiDomainModule;
use Ray\MediaQuery\DbQueryConfig;
use Ray\MediaQuery\MediaQueryModule;
use Ray\MediaQuery\Queries;
use Ray\MediaQuery\WebQueryConfig;
protected function configure(): void
{
$this->install(
new MediaQueryModule(
Queries::fromDir('/path/to/queryInterface'),
[
new DbQueryConfig('/path/to/sql'),
new WebQueryConfig('/path/to/web_query.json', ['domain' => 'api.exmaple.com'])
],
),
);
$this->install(new AuraSqlModule(
'mysql:host=localhost;dbname=test',
'username',
'password'
));
}
MediaQueryModuleはAuraSqlModuleのインストールが必要です。
注入
インターフェイスからオブジェクトが直接生成され、インジェクトされます。実装クラスのコーディングが不要です。
class Todo
{
public function __construct(
private TodoAddInterface $todoAdd
) {}
public function add(string $id, string $title): void
{
$this->todoAdd->add($id, $title);
}
}
DbQuery
SQL実行がメソッドにマップされ、IDで指定されたSQLをメソッドの引数でバインドして実行します。例えばIDがtodo_item
の指定ではtodo_item.sql
SQL文に['id => $id]
をバインドして実行します。
$sqlDir
ディレクトリにSQLファイルを用意します。- SQLファイルには複数のSQL文が記述できます。最後の行のSELECTが返り値になります。
Entity
SQL実行結果を用意したエンティティクラスをentity
で指定して変換(hydrate)することができます。
interface TodoItemInterface
{
#[DbQuery('todo_item', entity: Todo::class)]
public function getItem(string $id): Todo;
}
final class Todo
{
public string $id;
public string $title;
}
プロパティをキャメルケースに変換する場合にはCamelCaseTrait
を使います。
use Ray\MediaQuery\CamelCaseTrait;
class Invoice
{
use CamelCaseTrait;
public $userName;
}
コンストラクタがあると、フェッチしたデータでコールされます。
final class Todo
{
public function __construct(
public string $id,
public string $title
) {}
}
type: ‘row’
SQL実行の戻り値が単一行ならtype: 'row'
のアトリビュートを指定します。ただし、インターフェイスの戻り値がエンティティクラスなら省略することができます。
/** 返り値がEntityの場合 */
interface TodoItemInterface
{
#[DbQuery('todo_item', entity: Todo::class)]
public function getItem(string $id): Todo;
}
/** 返り値がarrayの場合 */
interface TodoItemInterface
{
#[DbQuery('todo_item', entity: Todo::class, type: 'row')]
public function getItem(string $id): array;
}
Web API
- メソッドの引数が
uri
で指定されたURI templateにバインドされ、Web APIリクエストオブジェクトが生成されます。 - 認証のためのヘッダーなどのカスタムはGuzzleの
ClinetInterface
をバインドして行います。
$this->bind(ClientInterface::class)->toProvider(YourGuzzleClientProvider::class);
パラメーター
日付時刻
パラメーターにバリューオブジェクトを渡すことができます。例えば、DateTimeInterface
オブジェクトをこのように指定できます。
interface TaskAddInterface
{
#[DbQuery('task_add')]
public function __invoke(string $title, DateTimeInterface $createdAt = null): void;
}
値はSQL実行時やWeb APIリクエスト時に日付フォーマットされた文字列に変換されます。
INSERT INTO task (title, created_at) VALUES (:title, :createdAt); # 2021-2-14 00:00:00
値を渡さないとバインドされている現在時刻がインジェクションされます。SQL内部でNOW()
とハードコーディングする事や、毎回現在時刻を渡す手間を省きます。
テスト時刻
テストの時には以下のようにDateTimeInterface
の束縛を1つの時刻にすることもできます。
$this->bind(DateTimeInterface::class)->to(UnixEpochTime::class);
バリューオブジェクト(VO)
DateTime
以外のバリューオブジェクトが渡されるとToScalarInterface
を実装したtoScalar()
メソッド、もしくは__toString()
メソッドの返り値が引数になります。
interface MemoAddInterface
{
public function __invoke(string $memo, UserId $userId = null): void;
}
class UserId implements ToScalarInterface
{
public function __construct(
private readonly LoginUser $user
) {}
public function toScalar(): int
{
return $this->user->id;
}
}
INSERT INTO memo (user_id, memo) VALUES (:user_id, :memo);
パラメーターインジェクション
バリューオブジェクトの引数のデフォルトの値のnull
がSQLやWebリクエストで使われることはないことに注意してください。値が渡されないと、nullの代わりにパラメーターの型でインジェクトされたバリューオブジェクトのスカラー値が使われます。
public function __invoke(Uuid $uuid = null): void; // UUIDが生成され渡される
ページネーション
DBの場合、#[Pager]
アトリビュートでSELECTクエリーをページングすることができます。
use Ray\MediaQuery\PagesInterface;
interface TodoList
{
#[DbQuery, Pager(perPage: 10, template: '/{?page}')]
public function __invoke(): PagesInterface;
}
count()
で件数が取得でき、ページ番号で配列アクセスをするとページオブジェクトが取得できます。Pages
はSQL遅延実行オブジェクトです。
$pages = ($todoList)();
$cnt = count($pages); // count()をした時にカウントSQLが生成されクエリーが行われます。
$page = $pages[2]; // 配列アクセスをした時にそのページのDBクエリーが行われます。
// $page->data // sliced data
// $page->current; // 現在のページ番号
// $page->total // 総件数
// $page->hasNext // 次ページの有無
// $page->hasPrevious // 前ページの有無
// $page->maxPerPage; // 1ページあたりの最大件数
// (string) $page // ページャーHTML
SqlQuery
SqlQuery
はSQLファイルのIDを指定してSQLを実行します。実装クラスを用意して詳細な実装を行う時に使用します。
class TodoItem implements TodoItemInterface
{
public function __construct(
private SqlQueryInterface $sqlQuery
) {}
public function __invoke(string $id): array
{
return $this->sqlQuery->getRow('todo_item', ['id' => $id]);
}
}
get* メソッド
SELECT結果を取得するためには取得する結果に応じたget*
を使います。
$sqlQuery->getRow($queryId, $params); // 結果が単数行
$sqlQuery->getRowList($queryId, $params); // 結果が複数行
$statement = $sqlQuery->getStatement(); // PDO Statementを取得
$pages = $sqlQuery->getPages(); // ページャーを取得
Ray.MediaQueryはRay.AuraSqlModuleを含んでいます。さらに低レイヤーの操作が必要な時はAura.SqlのQuery BuilderやPDOを拡張したAura.SqlのExtended PDOをお使いください。doctrine/dbalも利用できます。
パラメーターインジェクションと同様、DateTimeInterface
オブジェクトを渡すと日付フォーマットされた文字列に変換されます。
$sqlQuery->exec('memo_add', [
'memo' => 'run',
'created_at' => new DateTime()
]);
他のオブジェクトが渡されるとtoScalar()
または__toString()
の値に変換されます。
プロファイラー
メディアアクセスはロガーで記録されます。標準ではテストに使うメモリロガーがバインドされています。
public function testAdd(): void
{
$this->sqlQuery->exec('todo_add', $todoRun);
$this->assertStringContainsString(
'query: todo_add({"id":"1","title":"run"})',
(string) $this->log
);
}
独自のMediaQueryLoggerInterfaceを実装して、各メディアクエリーのベンチマークを行ったり、インジェクトしたPSRロガーでログをすることもできます。
アノテーション / アトリビュート
属性を表すのにdoctrineアノテーション、アトリビュートどちらも利用できます。次の2つは同じものです。
use Ray\MediaQuery\Annotation\DbQuery;
#[DbQuery('user_add')]
public function add1(string $id, string $title): void;
/** @DbQuery("user_add") */
public function add2(string $id, string $title): void;