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は、DbQueryConfigWebQueryConfig、またはその両方の設定で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.sqlSQL文に['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;
}

プロパティをキャメルケースに変換する場合にはCameCaseTraitを使います。

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'のアトリビュートを指定します。しかしインターフェイスの戻り値がエンティティクラスなら省略することができます。1

/** 返り値が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 $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);

VO

DateTime以外のバリューオブジェクトが渡されるとtoScalarインターフェイスを実装したtoScalar()メソッド、もしくは__toString()メソッドの返り値が引数になります。

interface MemoAddInterface
{
    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 (: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;
// (string) $page // pager 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 も利用できます。

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;

  1. 以前のバージョン0.5までは次のようにSQLファイル名で判別していました。”SQL実行の戻り値が単一行ならitem、複数行ならlistのpostfixを付けます。”