soyuka/esql

PHP 扩展结构化查询语言

v2.0.1 2022-05-25 11:33 UTC

README

PHP 扩展 SQL 是 DQL(Doctrine 查询语言)的替代品。它结合了 SQL 的灵活性与强大的 Doctrine 元数据,让您对查询有更多的控制。

<?php
use App\Entity\Car;
use App\Entity\Model;
use Soyuka\ESQL\Bridge\Doctrine\ESQL;
use Soyuka\ESQL\Bridge\Automapper\ESQLMapper;

$connection = $managerRegistry->getConnection();
$mapper = new ESQLMapper($autoMapper, $managerRegistry);
$esql = new ESQL($managerRegistry, $mapper);
$car = $esql(Car::class);
$model = $car(Model::class);

$query = <<<SQL
SELECT {$car->columns()}, {$model->columns()} FROM {$car->table()}
INNER JOIN {$model->table()} ON {$car->join(Model::class)}
WHERE {$car->identifier()}
SQL;

$stmt = $connection->prepare($query);
$stmt->execute(['id' => 1]);

var_dump($esql->map($stmt->fetch()));

跳转到文档阅读此博客文章 以查看其使用示例。

API 平台桥接器

此包包含一个 API 平台桥接器,支持过滤和分页。要使用我们的桥接器,请使用 esql 属性。

<?php

use ApiPlatform\Metadata\ApiResource;
use Soyuka\ESQL\Bridge\ApiPlatform\State\Provider;
use Soyuka\ESQL\Bridge\ApiPlatform\State\Processor;

/**
 * #[ApiResource(provider: Provider::class, processor: Processor::class)]
 */
 class Car {}

这将自动启用以下功能的使用:

  • 使用原始 SQL 的 CollectionProvider
  • 使用原始 SQL 的 ItemProvider
  • 使用 Postgrest 规范构建的可组合过滤。
  • 遵循 Postgrest 规范的强大排序扩展。
  • 我们自己的可扩展的 DataPaginator

您可以在 排序过滤 的示例中找到。

常见问题解答

您刚刚重新创建 DQL 吗?

不是。此库在编写使用 Doctrine 元数据的 SQL 时提供了对冗余操作的快捷方式。我们仍然受益于 Doctrine 的元数据,您仍然可以使用它来管理您的模式或 fixtures。

关于 Eloquent 或其他 ORM 呢?

计划在 API 稳定到足够程度时添加对 Eloquent 或其他 ORM 系统的支持。

支持哪些数据库管理系统?

使用此库您将编写原生 SQL。我们所有的辅助工具都将输出可在标准 SQL 规范中使用的字符串,因此应该被每个关系型数据库管理系统支持。API 平台桥接器已在 SQLite 和 Postgres 上进行了测试。添加对 MariaDB 和 Mysql 的测试只是时间问题。

是否有任何限制或注意事项?

您仍然会编写 SQL,所以我想应该没有吧?唯一值得注意的是,绑定的参数将采用以 : 为前缀的字段名称。例如 identifier() 将输出 alias.identifier_column = :identifier_fieldname。我们的 FilterParser 使用唯一的参数名称。

关于映射器有什么?

映射器将通过 PHP 数据对象 (PDO) 语句 接收到的数组映射到普通的 PHP 对象,也称为实体。这就是对象关系映射的全部内容。内部我们使用 JanePHP 的自动映射器或 Symfony 的序列化器。

关于 API 平台桥接器的写操作呢?

写支持,扩展到如何 Doctrine 所做的是相当复杂的,尤其是如果您想要支持嵌入写(在写入主实体时写入关系)。这是可能的,但在此桥接器上添加此功能没有太多好处。然而,您可以使用我们的一些助手来进行更新和插入。

简单的更新

<?php
$data = new Car();
$car = $esql($data);
$binding = $this->automapper->map($data, 'array'); // map your object to an array somehow

$query = <<<SQL
UPDATE {$car->table()} SET {$car->predicates()}
WHERE {$car->identifier()}
SQL;

$connection->beginTransaction();
$stmt = $connection->prepare($query);
$stmt->execute($binding);
$connection->commit();

同样的,对于插入值也适用

<?php
$data = new Car();
$binding = $this->automapper->map($data, 'array'); // map your object to an array somehow
$car = $esql($data)
$query = <<<SQL
INSERT INTO {$car->table()} ({$car->columns()}) VALUES ({$car->parameters($binding)});
SQL;

$connection->beginTransaction();
$stmt = $connection->prepare($query);
$stmt->execute($binding);
$connection->commit();

注意,如果您使用了序列号,您需要自己处理。

文档

Doctrine

ESQL实例提供了一些方法,帮助您在Doctrine元数据帮助下编写SQL。为了简化在HEREDOC中使用,通过在ESQL类上调用__invoke($classOrObject),将返回一个包含以下闭包的数组:

<?php
use Soyuka\ESQL\Bridge\Doctrine\ESQL;
use App\Entity\Car;

// Doctrine's ManagerRegistry and an ESQLMapperInterface (see below)
$esql = new ESQL($managerRegistry, $mapper);
$car = $esql(Car::class);

// the Table name
// outputs "car"
echo $car->table();

// the sql alias
// outputs "car"
echo $car->alias();

// Get columns: columns(?array $fields = null, string $output = $car::AS_STRING): string
// columns() outputs "car.id, car.name, car.model_id"
// output can also take: $car::AS_ARRAY | $car::WITHOUT_ALIASES | $car::WITHOUT_JOIN_COLUMNS | $car::IDENTIFIERS
echo $car->columns();

// Get a single column: column(string $fieldName): string
// column('id') outputs "car.id"
echo $car->column('id');

// Get an identifier predicate: identifier(): string
// identifier() outputs "car.id = :id"
echo $car->identifier();

// Get a join predicate: join(string $relationClass): string
// join(Model::class) outputs "car.model_id = model.id"
echo $car->join(Model::class);

// All kinds of predicates: predicates(?array $fields = null, string $glue = ', '): string
// predicates() outputs "car.id = :id, car.name = :name"
echo $car->predicates();

更高级的实用工具作为以下内容提供:

<?php

// Get a normalized value for SQL, sometimes booleans are in fact integer: toSQLValue(string $fieldName, $value)
// toSQLValue('sold', true) output "1" on sqlite but "true" on postgresql
$car->toSQLValue('sold', true);

// Given an array of bindings, will output keys prefixed by `:`: parameters(array $bindings): string
// parameters(['id' => 1, 'color' => 'blue']) will output ":id, :color"
$car->parameters();

这些工具用于构建过滤器、编写系统甚至自定义映射器。

ESQL通过别名将它们映射到类及其属性上工作。当处理关系时,您将必须使用

<?php

$car = $esql(Car::class);
$car->alias(); // car
$model = $car(Model::class);
$model->alias(); // car_model

这样,ESQL就知道将Model映射到Car->model属性。当与DTO一起工作时,可能找不到关系,您可以自己为关系分配别名

<?php

// Let's compute statistics and map car properties to the Aggregate class
// The entity used is Car, mapped to Aggregate and using an SQL alias "car"
$car = $esql(Car::class, Aggregate::class, 'car');
// The model relation doesn't exist, let's just use the model property
$model = $car(Model::class, 'model');

完整接口作为ESQLInterface提供。

映射

自动映射器

ESQLMapper将PDOStatement fetchfetchAll方法检索到的数组转换为相应的PHP对象。

<?php

// AutoMapper is an instance of JanePHP's automapper (https://github.com/janephp/automapper)
$mapper = new ESQLMapper($autoMapper, $managerRegistry);
$model = new Model();
$model->id = 1;
$model->name = 'Volkswagen';

$car = new Car();
$car->id = 1;
$car->name = 'Caddy';
$car->model = $model;

$car2 = new Car();
$car2->id = 2;
$car2->name = 'Passat';
$car2->model = $model;

// Aliases should be generated by ESQL to map properties and relation properly
$this->assertEquals([$car, $car2], $mapper->map([
    ['car_id' => '1', 'car_name' => 'Caddy', 'model_id' => '1', 'model_name' => 'Volkswagen'],
    ['car_id' => '2', 'car_name' => 'Passat', 'model_id' => '1', 'model_name' => 'Volkswagen'],
], Car::class));

还有一个使用symfony/serializer构建的映射器。

包配置

esql:
  mapper: Soyuka\ESQL\Bridge\Automapper\ESQLMapper
  api-platform:
    enabled: true

分页器

API Platform为分页提供了很好的默认设置。使用Soyuka\ESQL\Bridge\ApiPlatform\DataProvider\DataPaginator,获取数据将如下所示:

<?php

$esql = $this->esql->__invoke(Car::class);
$parameters = [];

$query = <<<SQL
SELECT {$esql->columns()} FROM {$esql->table()}
SQL;

if ($paginator = $this->dataPaginator->getPaginator($resourceClass, $operationName)) {
    return $paginator($esql, $query, $parameters, $context);
}

如果您想自己处理分页,我们提供了这样做的方法

<?php

$resourceClass = Car::class;
$operationName = 'get';
$esql = $this->esql->__invoke($resourceClass);
['itemsPerPage' => $itemsPerPage, 'firstResult' => $firstResult, 'nextResult' => $nextResult, 'page' => $page, 'partial' => $isPartialEnabled] = $this->dataPaginator->getPaginationOptions($resourceClass, $operationName);

$query = <<<SQL
SELECT {$esql->columns()} FROM {$esql->table()}
LIMIT $itemsPerPage OFFSET $firstResult
SQL;

// fetch data somehow and map
$data = $esql->map($data);

$countQuery = <<< SQL
SELECT COUNT(1) as count FROM {$esql->table()}
SQL;

// get count results somehow
$count = $countResult['count'];

return $isPartialEnabled ? new PartialPaginator($data, $page, $itemsPerPage) : new Paginator($data, $page, $itemsPerPage, $count);

示例