ray/media-query

媒体访问映射框架

0.13.0 2024-05-11 12:59 UTC

README

媒体访问映射框架

codecov Type Coverage Continuous Integration

日语 (Japanese)

概述

Ray.QueryModule 使用一个注入的函数对象对外部媒体(如数据库或Web API)进行查询。

动机

  • 在代码中,您可以清楚地区分领域层(使用代码)和基础设施层(注入的函数)。
  • 执行对象会自动生成,因此您不需要编写执行过程代码。
  • 由于使用代码对外部媒体的实际状态无感知,因此以后可以更改存储。易于并行开发和调试。

Composer 安装

$ 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 请求 ID。

interface PostItemInterface
{
    #[WebQuery('user_item')]
    public function get(string $id): array;
}

创建名为 web_query.json 的 Web API 路径列表文件。

{
    "$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.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);
    }
}

DbQuery

当调用方法时,会绑定指定的 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>>),不需要指定类型。
  • 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 将属性转换为 camelCase。

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

如果工厂方法不是静态的,则执行工厂类依赖关系解析。

final class TodoEntityFactory
{
    public function __construct(
        private HelperInterface $helper
    ){}
    
    public function factory(string $id, string $name): Todo
    {
        return new Todo($id, $this->helper($name));
    }
}

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,如下所示。

$this->bind(DateTimeInterface::class)->to(UnixEpochTime::class);

值对象 (VO)

如果传递的值对象不是 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 中从不使用。如果没有传递值,将使用带有参数类型的值对象的标量值而不是 null

public function __invoke(Uuid $uuid = null): void; // UUID is generated and passed.

分页

#[Pager] 注解允许对 SELECT 查询进行分页。

use Ray\MediaQuery\PagesInterface;

interface TodoList
{
    #[DbQuery('todo_list'), Pager(perPage: 10, template: '/{?page}')]
    public function __invoke(): PagesInterface;
}

您可以使用 count() 获取页数,您可以通过数组访问按页码获取页面对象。 Pages 是一个 SQL 懒执行对象。

每页项目数由 perPage 指定,但对于动态值,请指定一个字符串,其中包含表示页数的参数名称,如下所示

    #[DbQuery('todo_list'), Pager(perPage: 'pageNum', template: '/{?page}')]
    public function __invoke($pageNum): Pages;
$pages = ($todoList)();
$cnt = count($page); // When count() is called, the count SQL is generated and queried.
$page = $pages[2]; // A page query is executed when an array access is made.

// $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

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); // Result is a single row
$sqlQuery->getRowList($queryId, $params); // result is multiple rows
$statement = $sqlQuery->getStatement(); // Retrieve the PDO Statement
$pages = $sqlQuery->getPages(); // Get the pager

Ray.MediaQuery 包含了 Ray.AuraSqlModule。如果您需要更多的底层操作,您可以使用 Aura.Sql 的 查询构建器Aura.Sql,它扩展了 PDO。也提供了 doctrine/dbal

分析器

媒体访问由记录器记录。默认情况下,内存记录器绑定用于测试。

public function testAdd(): void
{
    $this->sqlQuery->exec('todo_add', $todoRun);
    $this->assertStringContainsString('query: todo_add({"id": "1", "title": "run"})', (string) $this->log);
}

实现您自己的 MediaQueryLoggerInterface 并运行。您还可以实现自己的 MediaQueryLoggerInterface 来基准测试每个媒体查询,并将其与注入的 PSR 记录器一起记录。

注解/属性

您可以使用 doctrine annotationsPHP8 attributes,两者都可以使用。接下来的两个是相同的。

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;

测试 Ray.MediaQuery

以下是安装 Ray.MediaQuery 的源代码并运行单元测试和演示的方法。

$ git clone https://github.com/ray-di/Ray.MediaQuery.git
$ cd Ray.MediaQuery
$ composer tests
$ php demo/run.php