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;
}

プロパティをキャメルケースに変換する場合には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;