MongoDB 提供了包括 PHP 在内的各种语言的驱动程序。为了简化在 PHP 中创建聚合管道的过程,我们需要将所有阶段和运算符建模为可以组合的函数。
聚合管道是“阶段”文档的列表。我们将举一个使用 进行查询$match和连接的示例$lookup:
db.orders.aggregate([
{
$match: {
$or: [
{ status: "shipped" },
{ created_at: { $gte: ISODate("2023-01-01T00:00:00Z") } }
]
}
},
{
$lookup: {
from: "inventory",
localField: "product_id",
foreignField: "product_id",
as: "inventory_docs"
}
}
])
每个带有美元前缀的键都是我们要为其提供工厂方法的运算符。
命名空间函数
最明显的解决方案是创建命名空间函数,例如:MongoDB\Operator\eqof$eq运算符。
namespace MongoDB\Operator;
function eq(mixed $value): array {
return ['$eq' => $value];
}
function lookup(string $from, string $localField, string $foreignField, string $as): array {
return ['$lookup' => [
'from' => $from,
'localField' => $localField,
'foreignField' => $foreignField,
'as' => $as,
]];
}
使用带有命名参数的函数,管道将用 PHP 编写:
pipeline(
match(
or(
query(status: eq('shipped')),
query(date: gte(new UTCDateTime())),
),
),
lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);
但是,某些运算符名称与PHP 中的保留关键字冲突。我们不能创建具有以下名称的函数(全局或命名空间):
and,
or,
match,
unset,
set,
为函数名添加后缀
为了避免保留名称的问题,我们可以在函数名称中添加前缀或后缀。
以运算符类型作为后缀:
function andQuery(...) { /* ... */ }
function matchStage(...) { /* ... */ }
带下划线:
function _and(...) { /* ... */ }
function _match(...) { /* ... */ }
或者使用表情符号。漂亮,但不实用:
function ?and(...) { /* ... */ }
function ?match(...) { /* ... */ }
静态类方法
碰巧的是,方法名称的保留关键字列表较短。我们可以在类上创建静态方法。
final class Stage {
public static function lookup(...) { /* ... */ }
public static function match(...) { /* ... */ }
}
final class Query {
public static function and(...) { /* ... */ }
public static function eq(...) { /* ... */ }
}
字写得有点长了,不过还是可读的。
new Pipeline(
Stage::match(
Query::or(
Query::query(status: Query::eq('shipped')),
Query::query(date: Query::gte(new UTCDateTime())),
),
),
Stage::lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);
为了防止任何人创建此类的实例,我们可以将构造函数设为私有。
final class Operator {
// ...
private function __construct() {} // This constructor cannot be called
}
我们也可以使用enum不带外壳的。Enum 接受静态方法并且不能实例化。
enum Query {
public static function and() { /* ... */ }
public static function eq() { /* ... */ }
}
类和枚举静态方法都可以以相同的方式调用。
变量中的闭包
由于找不到理想的解决方案,我们开始热衷于不太可能的解决方案。
如果我们想要一个看起来与 MongoDB 语法非常相似且没有名称限制的简短语法,那么我们就会想到使用变量来存储闭包。请注意,这(...)是PHP 8.1 中创建闭包的新语法。
$eq = Operator::eq(...);
$and = Operator::and(...);
$PHP 使用美元符号作为变量前缀,MongoDB 使用相同的运算符作为前缀。
pipeline(
$match(
$or(
$query(status: $eq('shipped')),
$query(date: $gte(new UTCDateTime())),
),
),
$lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);
库可以将这些闭包作为数组提供。
enum Query {
public static function and(array ...$queries) { /* ... */ }
public static function eq(mixed $value) { /* ... */ }
public static function query(mixed ...$query) { /* ... */ }
/** @return array{and:callable,eq:callable,query:callable} */
public static function functions(): array {
return [
'and' => self::and(...),
'eq' => self::eq(...),
'query' => self::query(...),
];
}
}
获取所有变量的语法有点冗长,但仍然可读。
['and' => $and, 'eq' => $eq, 'query' => $query] = Query::functions();
extract我们可以使用 Laravel 中经常使用但 PHPStorm 和静态分析工具非常讨厌的神奇功能将所有变量导入到当前作用域中。
extract(Query::functions());
var_dump($and(
$query(foo: $eq(5)),
$query(bar: $eq(10))
));
// INFO: MixedFunctionCall - Cannot call function on mixed
结论
正如您所看到的,在使用保留关键字时,PHP 中的函数命名并不那么简单。