ray / media-query
媒体访问映射框架
Requires
- php: ^8.1
- ext-mbstring: *
- ext-pdo: *
- aura/sql: ^4.0 || ^5.0
- doctrine/annotations: ^1.12 || ^2.0
- guzzlehttp/guzzle: ^6.3 || ^7.2
- koriym/csv-entities: ^1.0
- koriym/null-object: ^1.0.1
- nikic/php-parser: ^4.15 || ^5.0
- pagerfanta/pagerfanta: ^3.5
- phpdocumentor/reflection-docblock: ^5.3
- phpdocumentor/type-resolver: ^1.6.1
- ray/aop: ^2.10.4
- ray/aura-sql-module: ^1.12.0
- ray/di: ^2.14
- roave/better-reflection: ^4.12 || ^5.6 || ^6.25
- symfony/polyfill-php81: ^1.24
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.8
- phpunit/phpunit: ^10.5
Suggests
- koriym/csv-entities: Provides one-to-many entity relation
This package is auto-updated.
Last update: 2024-09-13 16:04:12 UTC
README
媒体访问映射框架
概述
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 通过设置 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); } }
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 annotations 或 PHP8 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