sad_spirit/pg_wrapper

复杂 PostgreSQL 类型的转换器以及 PHP pgsql 扩展的面向对象包装器

v2.5.0 2024-09-12 10:15 UTC

README

注意:master 分支正在升级以支持 PHP 8.2+

分支 2.x 包含与 PHP 7.2+ 兼容的稳定版本

Build Status

Static Analysis

此包包含两个部分和目的

虽然转换部分可以单独使用,例如与 PDO 一起使用,但像透明转换查询结果这样的功能只能在包装器中使用。

安装

使用 composer 需求此包

composer require sad_spirit/pg_wrapper

pg_wrapper 需要 PHP 8.2 或更高版本。要使用访问数据库的类,必须启用原生 pgsql 扩展(该扩展不是强制要求)。

最低支持的 PostgreSQL 版本是 9.3

强烈建议在生产环境中使用 PSR-6 兼容 的元数据缓存,以防止在每个页面请求时从数据库中查找可能的元数据。

为什么需要进行类型转换?

PostgreSQL 支持一组大型(且可扩展)的复杂数据库类型:数组、范围、几何和日期/时间类型、组合(行)类型、JSON...

create table test (
    strings  text[],
    coords   point,
    occupied daterange,
    age      interval,
    document json
);

insert into test values (
    array['Mary had', 'a little lamb'], point(55.75, 37.61),
    daterange('2014-01-13', '2014-09-19'), age('2014-09-19', '2014-01-13'),
    '{"title":"pg_wrapper","text":"pg_wrapper is cool"}'
);

不幸的是,与 PostgreSQL 通信的 PHP 扩展(pgsql 和 PDO_pgsql)都无法将这些复杂类型映射到其 PHP 等效类型。它们返回字符串表示形式

var_dump(pg_fetch_assoc(pg_query($conn, 'select * from test')));

产生

array(5) {
  'strings' =>
  string(28) "{"Mary had","a little lamb"}"
  'coords' =>
  string(13) "(55.75,37.61)"
  'occupied' =>
  string(23) "[2014-01-13,2014-09-19)"
  'age' =>
  string(13) "8 mons 6 days"
  'document' =>
  string(50) "{"title":"pg_wrapper","text":"pg_wrapper is cool"}"
}

这就是这个库发挥作用的地方

$result = $connection->execute('select * from test');
var_dump($result[0]);

产生

array(5) {
  'strings' =>
  array(2) {
    [0] =>
    string(8) "Mary had"
    [1] =>
    string(13) "a little lamb"
  }
  'coords' =>
  class sad_spirit\pg_wrapper\types\Point#18 (1) {
    private $_coordinates =>
    array(2) {
      'x' =>
      double(55.75)
      'y' =>
      double(37.61)
    }
  }
  'occupied' =>
  class sad_spirit\pg_wrapper\types\DateTimeRange#19 (1) {
    ...
  }
  'age' =>
  class sad_spirit\pg_wrapper\types\DateInterval#22 (16) {
    ...
  }
  'document' =>
  array(2) {
    'title' =>
    string(10) "pg_wrapper"
    'text' =>
    string(18) "pg_wrapper is cool"
  }
}

为什么还需要另一个面向对象的包装器,当我们已经有 PDO、Doctrine DBAL 等工具时?

抽象层的目的是针对最低通用语言,因此它有意隐藏了一些我们可以使用原生扩展以及 / 或添加另一层复杂性的低级 API。

  • PDO 不暴露 pg_query_params(),因此即使你只执行一次查询,你也必须 prepare() / execute() 每个查询。
  • Postgres 只支持原生的 $1 位置参数,而 PDO 有位置 ? 和命名 :foo 参数。PDO 实际上重写了查询以将后者转换为前者,这在 PHP 7.4 之前防止了使用 PDO 与包含 ? 的 Postgres 操作符,并且在使用美元引号字符串时仍然可能导致问题。
  • PDO 不暴露 pg_field_type_oid() 和其 PDOStatement::getColumnMeta() 返回类型名称,而不包含模式名称 并且 可能每次都会运行元数据查询来获取它。

另一个例子:数据库抽象的一个非常常见问题是使用 IN 子句向查询提供参数列表

SELECT * FROM stuff WHERE id IN (?)

其中 ? 实际上代表一个可变数量的参数。

一方面,如果您不需要抽象,Postgres 本身就有原生数组类型,这可以通过以下查询轻松实现

-- in case of using PDO just replace $1 with a PDO-compatible placeholder
SELECT * FROM stuff WHERE id = ANY($1::INTEGER[])

传递数组字面量作为其参数值

use sad_spirit\pg_wrapper\converters\DefaultTypeConverterFactory;

$arrayLiteral = (new DefaultTypeConverterFactory())
    ->getConverterForTypeSpecification('INTEGER[]')
    ->output([1, 2, 3]);

另一方面,Doctrine DBAL 有自己的参数列表解决方案,这再次依赖于重写 SQL,并且不与 prepare() / execute() 一起使用。它也具有 "支持" 数组类型,但这只是(反)序列化 PHP 数组,而不是将它们从/到原生数据库表示形式转换,这显然与上述查询不兼容。

文档

位于 wiki

类型转换

与 PostgreSQL 一起工作