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;
}
WebQuery
属性でWeb APIのIDを指定します。
interface PostItemInterface
{
#[WebQuery('user_item')]
public function item(string $id): Post;
}
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.example.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);
}
}
メソッドをコールするとIDで指定されたSQLをメソッドの引数でバインドして実行します。
例えばIDがtodo_item
の指定ではtodo_item.sql
SQL文を['id => $id]
でバインドして実行します。
interface TodoItemInterface
{
#[DbQuery('todo_item', type: 'row')]
public function item(string $id): array;
#[DbQuery('todo_list')]
/** @return array<Todo> */
public function list(string $id): array;
}
- 結果が
row
(array<string, scalar>
)の場合はtype:'row'
を指定します。row_list
(array<int, array<string, scalar>>
)にはtype指定は不要です。 - SQLファイルには複数のSQL文が記述できます。その場合には最後の行のSELECTが戻り値になります。
メソッドの戻り値をエンティティクラスにするとSQL実行結果がハイドレートされます。
interface TodoItemInterface
{
#[DbQuery('todo_item')]
public function item(string $id): Todo;
#[DbQuery('todo_list')]
/** @return array<Todo> */
public function list(string $id): array;
}
final class Todo
{
public readonly string $id;
public readonly string $title;
}
プロパティをキャメルケースに変換する場合にはCameCaseTrait
を使います。
use Ray\MediaQuery\CamelCaseTrait;
class Invoice
{
use CamelCaseTrait;
public $userName;
}
エンティティにコンストラクタがあると、フェッチしたデータでコールされます。
final class Todo
{
public function __construct(
public readonly string $id,
public readonly string $title
) {}
}
ファクトリークラスでエンティティを生成するにはfactory
属性ででファクトリークラスを指定します。
interface TodoItemInterface
{
#[DbQuery('todo_item', factory: TodoEntityFactory::class)]
public function item(string $id): Todo;
#[DbQuery('todo_list', factory: TodoEntityFactory::class)]
/** @return array<Todo> */
public function list(string $id): array;
}
ファクトリークラスのfactory
メソッドがフェッチしたデータでコールされます。データに応じてエンティティを変えることもできます。
final class TodoEntityFactory
{
public static function factory(string $id, string $name): Todo
{
return new Todo($id, $name);
}
}
ファクトリーメソッドがstaticでない場合はfactoryクラスの依存解決が行われます。
final class TodoEntityFactory
{
public function __construct(
private HelperInterface $helper
){}
public function factory(string $id, string $name): Todo
{
return new Todo($id, $this->helper($name));
}
}
- メソッドの引数が
uri
で指定されたURI templateにバインドされ、Web APIリクエストオブジェクトが生成されます。 - 認証のためのヘッダーなどのカスタムはGuzzleの
ClinetInterface
をバインドして行います。
$this->bind(ClientInterface::class)->toProvider(YourGuzzleClientProvicer::class);
パラメーターにバリューオブジェクトを渡すことができます。
例えば、DateTimeInterface
オブジェクトをこのように指定できます。
interface TaskAddInterface
{
#[DbQuery('task_add')]
public function __invoke(string $title, DateTimeInterface $cratedAt = 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);
DateTime
以外のバリューオブジェクトが渡されるとToScalar
インターフェイスを実装したtoScalar()
メソッド、もしくは__toString()
メソッドの返り値が引数になります。
interface MemoAddInterface
{
#[DbQuery('memo_add')]
public function __invoke(string $memo, UserId $userId = null): void;
}
class UserId implements ToScalarInterface
{
public function __construct(
private LoginUser $user;
){}
public function toScalar(): int
{
return $this->user->id;
}
}
INSERT INTO memo (user_id, memo) VALUES (:userId, :memo);
バリューオブジェクトの引数のデフォルトの値のnull
がSQLやWebリクエストで使われることは無い事に注意してください。値が渡されないと、nullの代わりにパラメーターの型でインジェクトされたバリューオブジェクトのスカラー値が使われます。
public function __invoke(Uuid $uuid = null): void; // UUIDが生成され渡される
DBの場合、#[Pager]
属性でSELECTクエリーをページングする事ができます。
use Ray\MediaQuery\PagesInterface;
interface TodoList
{
#[DbQuery('todo_list'), Pager(perPage: 10, template: '/{?page}')]
public function __invoke(): Pages;
}
ページ毎のアイテム数をperPageで指定しますが、動的な値の場合は以下のようにページ数を表す引数の名前を文字列を指定します。
#[DbQuery('todo_list'), Pager(perPage: 'pageNum', template: '/{?page}')]
public function __invoke($pageNum): Pages;
count()
で件数が取得でき、ページ番号で配列アクセスをするとページオブジェクトが取得できます。
Pages
はSQL遅延実行オブジェクトです。
$pages = ($todoList)();
$cnt = count($page); // count()をした時にカウントSQLが生成されクエリーが行われます。
$page = $pages[2]; // 配列アクセスをした時にそのページのDBクエリーが行われます。
// $page->data // sliced data
// $page->current;
// $page->total
// $page->hasNext
// $page->hasPrevious
// $page->maxPerPage;
// (string) $page // pager html
エンティティクラスにハイドレーションを行うときは@return
で指定します。
#[DbQuery('todo_list'), Pager(perPage: 'pageNum', template: '/{?page}')]
/** @return array<Todo> */
public function __invoke($pageNum): Pages;
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]);
}
}
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 も利用できます。
Parameter Injectionと同様、DateTimeIntetface
オブジェクトを渡すと日付フォーマットされた文字列に変換されます。
$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;
テストとデモを行うためには以下のようにします。
$ git clone https://github.com/ray-di/Ray.MediaQuery.git
$ cd Ray.MediaQuery
$ composer tests
$ php demo/run.php