Appearance
MongoDB JavaScript 类型详解
本知识点承接《MongoDB数据类型概述》,后续延伸至《MongoDB服务端JavaScript执行》,建议学习顺序:MongoDB基础→数据类型概述→本知识点→服务端JavaScript执行
1. 概述
在MongoDB数据库中,JavaScript类型是一种特殊的BSON类型,用于存储JavaScript代码字符串。MongoDB从设计之初就深度集成了JavaScript引擎,允许在服务端执行JavaScript代码。JavaScript类型(BSON type 0x0D)专门用于存储纯JavaScript代码片段,而JavaScript with Scope类型(BSON type 0x0F)则可以存储带有作用域变量的JavaScript代码。
在PHP中,我们使用MongoDB\BSON\Javascript类来创建和操作MongoDB的JavaScript代码对象。这个类封装了JavaScript代码字符串,使得我们可以在PHP代码中构建JavaScript表达式,并将其存储到MongoDB中或在查询中使用。
JavaScript类型的主要应用场景包括:存储可执行的JavaScript表达式、MapReduce操作中的map和reduce函数、$where查询中的条件表达式、存储计算逻辑以便在服务端执行等。然而,需要注意的是,出于安全考虑,现代MongoDB版本默认限制了JavaScript的执行,开发者应谨慎使用服务端JavaScript功能。
2. 基本概念
2.1 语法
MongoDB JavaScript类型在PHP中使用MongoDB\BSON\Javascript类表示,其基本语法如下:
php
use MongoDB\BSON\Javascript;
// 创建Javascript对象的基本语法
$js = new Javascript(string $code);
// 参数说明:
// $code: JavaScript代码字符串常用JavaScript代码示例:
| 用途 | 代码示例 |
|---|---|
| 简单表达式 | this.age > 18 |
| 函数定义 | function() { return this.x + this.y; } |
| 条件判断 | this.status === 'active' |
| 数学运算 | Math.pow(this.value, 2) |
| 字符串操作 | this.name.toUpperCase() |
MongoDB中JavaScript执行上下文:
| 上下文 | 说明 | this指向 |
|---|---|---|
| $where查询 | 文档过滤 | 当前文档 |
| MapReduce map | 映射函数 | 当前文档 |
| MapReduce reduce | 归约函数 | 无 |
| $function | 自定义函数 | 可指定 |
2.2 语义
JavaScript类型在MongoDB中的语义主要体现在以下几个方面:
存储语义:
- JavaScript代码作为BSON类型存储,类型码为0x0D
- 代码以字符串形式存储,不进行语法检查
- 存储时保留原始格式,包括空白和注释
执行语义:
- 在$where查询中,JavaScript代码针对每个文档执行
- 返回true的文档被包含在结果集中
- 执行环境包含当前文档的所有字段
安全语义:
- JavaScript执行可能带来安全风险
- 需要适当的权限才能执行
- 建议使用其他查询方式替代
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
echo "=== JavaScript类型基本语义 ===\n\n";
// 1. 创建Javascript对象
echo "1. 创建Javascript对象:\n";
$simpleExpr = new Javascript('this.age > 18');
echo " 简单表达式: " . $simpleExpr->getCode() . "\n";
$funcExpr = new Javascript('function() { return this.price * 1.1; }');
echo " 函数表达式: " . $funcExpr->getCode() . "\n";
// 2. Javascript对象的字符串表示
echo "\n2. 字符串表示:\n";
echo " 简单表达式: " . (string)$simpleExpr . "\n";
// 3. JSON序列化
echo "\n3. JSON序列化:\n";
echo " " . json_encode(['expr' => $simpleExpr], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
?>输出结果:
=== JavaScript类型基本语义 ===
1. 创建Javascript对象:
简单表达式: this.age > 18
函数表达式: function() { return this.price * 1.1; }
2. 字符串表示:
简单表达式: this.age > 18
3. JSON序列化:
{
"expr": {
"$code": "this.age > 18"
}
}2.3 存储结构
JavaScript类型在BSON中的存储结构:
┌─────────────────────────────────────────────────────┐
│ BSON Javascript │
├─────────────────────────────────────────────────────┤
│ Type (1 byte): 0x0D │
│ Name (cstring): 字段名 │
│ Code (string): JavaScript代码字符串 │
│ ├─ Length (int32): 字符串长度 │
│ └─ Code bytes: UTF-8编码的代码 │
│ └─ Terminator (1 byte): 0x00 │
└─────────────────────────────────────────────────────┘存储示例:
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== JavaScript存储结构示例 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->js_examples;
// 存储JavaScript代码
$doc = [
'name' => 'validation_rule',
'code' => new Javascript('this.value >= 0 && this.value <= 100'),
'description' => '验证值在0-100范围内'
];
$result = $collection->insertOne($doc);
echo "插入文档ID: " . $result->getInsertedId() . "\n";
// 查看存储的文档
$stored = $collection->findOne(['_id' => $result->getInsertedId()]);
echo "\n存储的文档:\n";
echo " name: " . $stored->name . "\n";
echo " code: " . $stored->code . "\n";
echo " description: " . $stored->description . "\n";
?>3. 基础用法
3.1 创建Javascript对象
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
echo "=== 创建Javascript对象 ===\n\n";
// 方式1:创建简单表达式
echo "1. 简单表达式:\n";
$expr1 = new Javascript('this.age > 18');
echo " 年龄判断: " . $expr1->getCode() . "\n";
// 方式2:创建复杂表达式
echo "\n2. 复杂表达式:\n";
$expr2 = new Javascript('this.price > 100 && this.category === "premium"');
echo " 价格和类别判断: " . $expr2->getCode() . "\n";
// 方式3:创建函数表达式
echo "\n3. 函数表达式:\n";
$expr3 = new Javascript('function() { return this.firstName + " " + this.lastName; }');
echo " 全名拼接: " . $expr3->getCode() . "\n";
// 方式4:创建数学计算表达式
echo "\n4. 数学计算表达式:\n";
$expr4 = new Javascript('Math.sqrt(this.x * this.x + this.y * this.y)');
echo " 欧几里得距离: " . $expr4->getCode() . "\n";
// 方式5:创建条件表达式
echo "\n5. 条件表达式:\n";
$expr5 = new Javascript('this.score >= 90 ? "A" : this.score >= 80 ? "B" : "C"');
echo " 成绩等级: " . $expr5->getCode() . "\n";
?>3.2 在$where查询中使用
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== \$where查询中使用JavaScript ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->products;
// 准备测试数据
$collection->drop();
$collection->insertMany([
['name' => 'Product A', 'price' => 100, 'discount' => 20],
['name' => 'Product B', 'price' => 200, 'discount' => 30],
['name' => 'Product C', 'price' => 150, 'discount' => 10],
['name' => 'Product D', 'price' => 80, 'discount' => 5]
]);
// 使用$where查询:折扣后价格大于100
echo "查询折扣后价格大于100的产品:\n";
$jsCode = new Javascript('this.price - this.discount > 100');
$results = $collection->find(['$where' => $jsCode]);
foreach ($results as $doc) {
$finalPrice = $doc->price - $doc->discount;
echo " - {$doc->name}: 原价{$doc->price}, 折扣{$doc->discount}, 最终{$finalPrice}\n";
}
// 使用$where查询:复杂条件
echo "\n查询价格是折扣的整数倍的产品:\n";
$jsCode2 = new Javascript('this.price % this.discount === 0');
$results2 = $collection->find(['$where' => $jsCode2]);
foreach ($results2 as $doc) {
echo " - {$doc->name}: 价格{$doc->price}, 折扣{$doc->discount}\n";
}
?>3.3 存储和读取JavaScript代码
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 存储和读取JavaScript代码 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->stored_functions;
// 清空集合
$collection->drop();
// 存储多个JavaScript函数
$functions = [
[
'name' => 'calculateTax',
'code' => new Javascript('function(price, rate) { return price * rate; }'),
'description' => '计算税费'
],
[
'name' => 'formatCurrency',
'code' => new Javascript('function(amount, symbol) { return symbol + amount.toFixed(2); }'),
'description' => '格式化货币'
],
[
'name' => 'validateEmail',
'code' => new Javascript('function(email) { return /^[\\w-]+@[\\w-]+\\.[a-z]{2,}$/i.test(email); }'),
'description' => '验证邮箱格式'
]
];
foreach ($functions as $func) {
$collection->insertOne($func);
echo "存储函数: {$func['name']}\n";
}
// 读取存储的函数
echo "\n读取存储的函数:\n";
$stored = $collection->find([], ['sort' => ['name' => 1]]);
foreach ($stored as $doc) {
echo "\n函数名: {$doc->name}\n";
echo "描述: {$doc->description}\n";
echo "代码: " . $doc->code->getCode() . "\n";
}
?>4. 进阶用法
4.1 与聚合管道配合使用
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 聚合管道中使用JavaScript ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->sales;
// 准备测试数据
$collection->drop();
$collection->insertMany([
['product' => 'A', 'quantity' => 10, 'price' => 100],
['product' => 'B', 'quantity' => 5, 'price' => 200],
['product' => 'C', 'quantity' => 20, 'price' => 50]
]);
// 使用$function操作符(MongoDB 4.4+)
echo "使用$function计算加权分数:\n";
try {
$pipeline = [
[
'$addFields' => [
'weightedScore' => [
'$function' => [
'body' => new Javascript('function(quantity, price) { return quantity * price * 0.1; }'),
'args' => ['$quantity', '$price'],
'lang' => 'js'
]
]
]
]
];
$results = $collection->aggregate($pipeline);
foreach ($results as $doc) {
echo " - {$doc->product}: 数量{$doc->quantity}, 价格{$doc->price}, 加权分数{$doc->weightedScore}\n";
}
} catch (Exception $e) {
echo " 需要 MongoDB 4.4+ 支持: " . $e->getMessage() . "\n";
}
?>4.2 MapReduce操作
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== MapReduce操作 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->orders;
// 准备测试数据
$collection->drop();
$collection->insertMany([
['customer' => 'Alice', 'amount' => 100, 'status' => 'completed'],
['customer' => 'Bob', 'amount' => 200, 'status' => 'completed'],
['customer' => 'Alice', 'amount' => 150, 'status' => 'completed'],
['customer' => 'Charlie', 'amount' => 300, 'status' => 'pending'],
['customer' => 'Bob', 'amount' => 50, 'status' => 'completed']
]);
// MapReduce: 按客户统计订单总额
$map = new Javascript('function() { emit(this.customer, this.amount); }');
$reduce = new Javascript('function(key, values) { return Array.sum(values); }');
echo "执行MapReduce统计客户订单总额:\n";
$result = $collection->mapReduce(
$map,
$reduce,
['merge' => 'customer_totals']
);
$totalsCollection = $client->test->customer_totals;
$totals = $totalsCollection->find([], ['sort' => ['value' => -1]]);
foreach ($totals as $doc) {
echo " - {$doc->_id}: 总额 {$doc->value}\n";
}
?>4.3 条件表达式存储
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 条件表达式存储 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->validation_rules;
// 清空集合
$collection->drop();
// 存储验证规则
$rules = [
[
'field' => 'age',
'rule' => new Javascript('this.age >= 0 && this.age <= 150'),
'message' => '年龄必须在0-150之间'
],
[
'field' => 'email',
'rule' => new Javascript('/^[\\w-]+@[\\w-]+\\.[a-z]{2,}$/i.test(this.email)'),
'message' => '邮箱格式不正确'
],
[
'field' => 'password',
'rule' => new Javascript('this.password.length >= 8'),
'message' => '密码长度至少8位'
],
[
'field' => 'score',
'rule' => new Javascript('this.score >= 0 && this.score <= 100'),
'message' => '分数必须在0-100之间'
]
];
foreach ($rules as $rule) {
$collection->insertOne($rule);
echo "存储规则: {$rule['field']} - {$rule['message']}\n";
}
// 查询规则
echo "\n查询验证规则:\n";
$storedRules = $collection->find([]);
foreach ($storedRules as $rule) {
echo "\n字段: {$rule->field}\n";
echo "规则: " . $rule->rule->getCode() . "\n";
echo "消息: {$rule->message}\n";
}
?>5. 实际应用场景
5.1 动态计算字段
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 动态计算字段 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->computed_fields;
// 清空集合并插入测试数据
$collection->drop();
$collection->insertMany([
[
'name' => 'Rectangle A',
'width' => 10,
'height' => 5,
'computeArea' => new Javascript('this.width * this.height')
],
[
'name' => 'Circle B',
'radius' => 7,
'computeArea' => new Javascript('Math.PI * this.radius * this.radius')
],
[
'name' => 'Triangle C',
'base' => 8,
'height' => 6,
'computeArea' => new Javascript('0.5 * this.base * this.height')
]
]);
echo "存储的形状及其面积计算公式:\n";
$shapes = $collection->find([]);
foreach ($shapes as $shape) {
echo "\n形状: {$shape->name}\n";
echo "面积公式: " . $shape->computeArea->getCode() . "\n";
}
?>5.2 业务规则引擎
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 业务规则引擎 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$rulesCollection = $client->test->business_rules;
$ordersCollection = $client->test->orders;
// 存储业务规则
$rulesCollection->drop();
$rulesCollection->insertMany([
[
'name' => 'VIP折扣',
'condition' => new Javascript('this.customerLevel === "VIP" && this.totalAmount > 1000'),
'action' => 'applyDiscount',
'params' => ['discount' => 0.2]
],
[
'name' => '新用户优惠',
'condition' => new Javascript('this.isNewCustomer && this.totalAmount >= 100'),
'action' => 'applyDiscount',
'params' => ['discount' => 0.1]
],
[
'name' => '免运费',
'condition' => new Javascript('this.totalAmount >= 500'),
'action' => 'freeShipping',
'params' => []
]
]);
// 存储订单数据
$ordersCollection->drop();
$ordersCollection->insertMany([
['orderId' => 1, 'customerLevel' => 'VIP', 'totalAmount' => 1500, 'isNewCustomer' => false],
['orderId' => 2, 'customerLevel' => 'Normal', 'totalAmount' => 200, 'isNewCustomer' => true],
['orderId' => 3, 'customerLevel' => 'Normal', 'totalAmount' => 600, 'isNewCustomer' => false]
]);
// 应用规则
echo "应用业务规则:\n\n";
$rules = $rulesCollection->find([]);
$orders = $ordersCollection->find([]);
foreach ($orders as $order) {
echo "订单 #{$order->orderId}:\n";
echo " 客户等级: {$order->customerLevel}\n";
echo " 订单金额: {$order->totalAmount}\n";
echo " 新客户: " . ($order->isNewCustomer ? '是' : '否') . "\n";
echo " 适用规则: ";
$applicableRules = [];
foreach ($rules as $rule) {
// 注意:实际应用中需要在服务端执行JavaScript
// 这里仅演示规则存储
$applicableRules[] = $rule->name;
}
echo implode(', ', $applicableRules) . "\n\n";
}
?>5.3 数据验证配置
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 数据验证配置 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->validation_config;
// 清空集合
$collection->drop();
// 存储验证配置
$validations = [
[
'collection' => 'users',
'validations' => [
[
'field' => 'username',
'rules' => [
new Javascript('this.username.length >= 3'),
new Javascript('this.username.length <= 20'),
new Javascript('/^[a-zA-Z0-9_]+$/.test(this.username)')
],
'messages' => [
'用户名至少3个字符',
'用户名最多20个字符',
'用户名只能包含字母、数字和下划线'
]
],
[
'field' => 'age',
'rules' => [
new Javascript('this.age >= 18'),
new Javascript('this.age <= 120')
],
'messages' => [
'年龄必须大于等于18岁',
'年龄必须小于等于120岁'
]
]
]
]
];
foreach ($validations as $config) {
$collection->insertOne($config);
echo "存储验证配置: {$config['collection']}\n";
}
// 显示验证规则
echo "\n验证规则详情:\n";
$config = $collection->findOne(['collection' => 'users']);
foreach ($config->validations as $validation) {
echo "\n字段: {$validation['field']}\n";
for ($i = 0; $i < count($validation['rules']); $i++) {
echo " 规则" . ($i + 1) . ": " . $validation['rules'][$i]->getCode() . "\n";
echo " 消息: {$validation['messages'][$i]}\n";
}
}
?>6. 性能优化
6.1 避免不必要的JavaScript执行
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 避免不必要的JavaScript执行 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->products;
// 准备测试数据
$collection->drop();
$collection->insertMany([
['name' => 'Product A', 'price' => 100, 'stock' => 50],
['name' => 'Product B', 'price' => 200, 'stock' => 30],
['name' => 'Product C', 'price' => 150, 'stock' => 0]
]);
// 不推荐:使用$where查询
echo "不推荐方式 - \$where查询:\n";
echo " 问题: 需要为每个文档执行JavaScript\n";
echo " 代码: ['\$where' => new Javascript('this.price > 100')]\n";
// 推荐:使用标准查询操作符
echo "\n推荐方式 - 标准查询操作符:\n";
echo " 优势: 使用索引,性能更高\n";
echo " 代码: ['price' => ['\$gt' => 100]]\n";
$results = $collection->find(['price' => ['$gt' => 100]]);
foreach ($results as $doc) {
echo " - {$doc->name}: 价格 {$doc->price}\n";
}
// 复杂条件对比
echo "\n复杂条件对比:\n";
echo "不推荐: \$where = 'this.price > 100 && this.stock > 0'\n";
echo "推荐: ['price' => ['\$gt' => 100], 'stock' => ['\$gt' => 0]]\n";
$results2 = $collection->find([
'price' => ['$gt' => 100],
'stock' => ['$gt' => 0]
]);
foreach ($results2 as $doc) {
echo " - {$doc->name}: 价格 {$doc->price}, 库存 {$doc->stock}\n";
}
?>6.2 代码缓存策略
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 代码缓存策略 ===\n\n";
class JavascriptCodeCache
{
private $cache = [];
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function get(string $name): ?Javascript
{
if (isset($this->cache[$name])) {
echo "从内存缓存获取: {$name}\n";
return $this->cache[$name];
}
$doc = $this->collection->findOne(['name' => $name]);
if ($doc && isset($doc->code)) {
$this->cache[$name] = $doc->code;
echo "从数据库加载并缓存: {$name}\n";
return $doc->code;
}
return null;
}
public function store(string $name, string $code): void
{
$js = new Javascript($code);
$this->collection->replaceOne(
['name' => $name],
['name' => $name, 'code' => $js],
['upsert' => true]
);
$this->cache[$name] = $js;
echo "存储并缓存: {$name}\n";
}
public function clearCache(): void
{
$this->cache = [];
echo "清除内存缓存\n";
}
}
$client = new Client('mongodb://localhost:27017');
$cacheCollection = $client->test->js_cache;
$cacheCollection->drop();
$cache = new JavascriptCodeCache($cacheCollection);
// 存储代码
$cache->store('validateAge', 'this.age >= 18 && this.age <= 120');
$cache->store('calculateDiscount', 'this.price * 0.9');
echo "\n";
// 获取代码(从缓存)
$code1 = $cache->get('validateAge');
$code2 = $cache->get('calculateDiscount');
echo "\n";
// 再次获取(从内存缓存)
$code3 = $cache->get('validateAge');
echo "\n缓存内容:\n";
echo " validateAge: " . $code1->getCode() . "\n";
echo " calculateDiscount: " . $code2->getCode() . "\n";
?>6.3 批量操作优化
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 批量操作优化 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->batch_js;
// 清空集合
$collection->drop();
// 批量插入JavaScript代码
$codes = [];
for ($i = 1; $i <= 100; $i++) {
$codes[] = [
'name' => "rule_{$i}",
'code' => new Javascript("this.value >= {$i}"),
'category' => 'validation'
];
}
echo "批量插入100条JavaScript规则...\n";
$startTime = microtime(true);
$collection->insertMany($codes);
$endTime = microtime(true);
echo "耗时: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
// 批量读取
echo "\n批量读取规则...\n";
$startTime = microtime(true);
$rules = $collection->find(['category' => 'validation'])->toArray();
$endTime = microtime(true);
echo "读取 " . count($rules) . " 条规则\n";
echo "耗时: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
// 批量更新
echo "\n批量更新规则...\n";
$startTime = microtime(true);
$collection->updateMany(
['category' => 'validation'],
['$set' => ['updated' => true]]
);
$endTime = microtime(true);
echo "耗时: " . round(($endTime - $startTime) * 1000, 2) . " ms\n";
?>7. 安全注意事项
7.1 JavaScript注入防护
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== JavaScript注入防护 ===\n\n";
class SafeJavascriptBuilder
{
public static function buildComparison(string $field, string $operator, $value): Javascript
{
$safeField = self::escapeIdentifier($field);
$safeValue = self::escapeValue($value);
$operators = [
'>' => '>',
'<' => '<',
'>=' => '>=',
'<=' => '<=',
'===' => '===',
'!==' => '!=='
];
$safeOperator = $operators[$operator] ?? '===';
return new Javascript("this.{$safeField} {$safeOperator} {$safeValue}");
}
public static function buildRangeCheck(string $field, $min, $max): Javascript
{
$safeField = self::escapeIdentifier($field);
$safeMin = self::escapeValue($min);
$safeMax = self::escapeValue($max);
return new Javascript("this.{$safeField} >= {$safeMin} && this.{$safeField} <= {$safeMax}");
}
private static function escapeIdentifier(string $identifier): string
{
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $identifier)) {
throw new InvalidArgumentException("Invalid identifier: {$identifier}");
}
return $identifier;
}
private static function escapeValue($value): string
{
if (is_numeric($value)) {
return (string)$value;
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_string($value)) {
$escaped = addslashes($value);
return '"' . $escaped . '"';
}
if (is_null($value)) {
return 'null';
}
throw new InvalidArgumentException("Unsupported value type");
}
}
// 安全使用示例
echo "安全构建JavaScript表达式:\n";
$expr1 = SafeJavascriptBuilder::buildComparison('age', '>=', 18);
echo " 年龄判断: " . $expr1->getCode() . "\n";
$expr2 = SafeJavascriptBuilder::buildRangeCheck('score', 0, 100);
echo " 分数范围: " . $expr2->getCode() . "\n";
$expr3 = SafeJavascriptBuilder::buildComparison('status', '===', 'active');
echo " 状态判断: " . $expr3->getCode() . "\n";
// 危险示例(不要这样做)
echo "\n危险示例 - 直接拼接用户输入:\n";
echo " 危险: new Javascript('this.name === \"' . \$userInput . '\"')\n";
echo " 问题: 用户可能注入恶意代码\n";
?>7.2 权限控制
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== 权限控制 ===\n\n";
class SecureJavascriptExecutor
{
private $client;
private $allowedFunctions = [
'validateAge',
'calculateTotal',
'formatName'
];
public function __construct(Client $client)
{
$this->client = $client;
}
public function executeAllowed(string $functionName, array $context = []): bool
{
if (!in_array($functionName, $this->allowedFunctions)) {
echo "错误: 函数 '{$functionName}' 不在允许列表中\n";
return false;
}
echo "执行允许的函数: {$functionName}\n";
return true;
}
public function getAllowedFunctions(): array
{
return $this->allowedFunctions;
}
public function isJavaScriptEnabled(): bool
{
try {
$admin = $this->client->admin;
$result = $admin->command(['getCmdLineOpts' => 1]);
if (isset($result['parsed']['security']['javascriptEnabled'])) {
return $result['parsed']['security']['javascriptEnabled'];
}
return true;
} catch (Exception $e) {
return false;
}
}
}
$client = new Client('mongodb://localhost:27017');
$executor = new SecureJavascriptExecutor($client);
echo "允许的函数列表:\n";
foreach ($executor->getAllowedFunctions() as $func) {
echo " - {$func}\n";
}
echo "\n尝试执行函数:\n";
$executor->executeAllowed('validateAge');
$executor->executeAllowed('maliciousFunction');
?>7.3 代码审计
php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use MongoDB\BSON\Javascript;
echo "=== JavaScript代码审计 ===\n\n";
class JavascriptAuditor
{
private $dangerousPatterns = [
'/\beval\s*\(/i' => 'eval()函数可能执行任意代码',
'/Function\s*\(/i' => 'Function构造器可能执行任意代码',
'/this\s*\.\s*_/i' => '访问内部属性可能导致问题',
'/db\./i' => '直接访问数据库对象',
'/load\s*\(/i' => 'load()函数可能加载外部代码',
'/exit\s*\(/i' => 'exit()可能终止进程',
'/quit\s*\(/i' => 'quit()可能终止进程'
];
public function audit(Javascript $js): array
{
$code = $js->getCode();
$issues = [];
foreach ($this->dangerousPatterns as $pattern => $message) {
if (preg_match($pattern, $code)) {
$issues[] = [
'pattern' => $pattern,
'message' => $message,
'severity' => 'high'
];
}
}
return $issues;
}
public function isSafe(Javascript $js): bool
{
return count($this->audit($js)) === 0;
}
}
$auditor = new JavascriptAuditor();
// 安全代码
$safeCode = new Javascript('this.age >= 18 && this.status === "active"');
echo "审计安全代码:\n";
echo " 代码: " . $safeCode->getCode() . "\n";
echo " 结果: " . ($auditor->isSafe($safeCode) ? '安全' : '存在风险') . "\n";
// 危险代码
$dangerousCode = new Javascript('eval(this.userInput)');
echo "\n审计危险代码:\n";
echo " 代码: " . $dangerousCode->getCode() . "\n";
$issues = $auditor->audit($dangerousCode);
if (!empty($issues)) {
echo " 发现问题:\n";
foreach ($issues as $issue) {
echo " - {$issue['message']}\n";
}
}
?>8. 常见问题与解决方案
问题1:JavaScript执行被禁用怎么办?
问题描述:执行$where查询时报错JavaScript执行被禁用。
回答内容:
MongoDB出于安全考虑,可能禁用JavaScript执行。可以通过配置启用或使用替代方案。
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== JavaScript执行被禁用的解决方案 ===\n\n";
echo "方案1: 使用聚合管道替代\n";
echo " 原查询: ['\$where' => new Javascript('this.price > 100')]\n";
echo " 替代: ['price' => ['\$gt' => 100]]\n\n";
echo "方案2: 使用\$expr操作符\n";
echo " 原查询: ['\$where' => new Javascript('this.a + this.b > 100')]\n";
echo " 替代: ['\$expr' => ['\$gt' => [['\$add' => ['\$a', '\$b']], 100]]]\n\n";
echo "方案3: 启用JavaScript执行(需管理员权限)\n";
echo " mongod --security javascriptEnabled:true\n";
?>问题2:JavaScript代码太长如何处理?
问题描述:复杂的JavaScript代码难以维护和调试。
回答内容:
建议将复杂逻辑拆分为多个小函数,或使用服务端计算。
php
<?php
use MongoDB\BSON\Javascript;
echo "=== 处理复杂JavaScript代码 ===\n\n";
// 不推荐:过长的代码
$longCode = new Javascript('
function() {
var result = 0;
for (var i = 0; i < this.items.length; i++) {
if (this.items[i].price > 100) {
result += this.items[i].price * 0.9;
} else {
result += this.items[i].price;
}
}
return result > 1000;
}
');
echo "不推荐: 过长的内联代码\n\n";
// 推荐:拆分为多个规则
$rules = [
'checkItems' => new Javascript('this.items && this.items.length > 0'),
'calculateTotal' => new Javascript('this.items.reduce((sum, item) => sum + item.price, 0)'),
'checkThreshold' => new Javascript('this.total > 1000')
];
echo "推荐: 拆分为多个小规则\n";
foreach ($rules as $name => $rule) {
echo " {$name}: " . $rule->getCode() . "\n";
}
?>问题3:如何在JavaScript中访问其他集合?
问题描述:$where中的JavaScript无法直接访问其他集合。
回答内容:
$where查询中的JavaScript只能访问当前文档。如需跨集合操作,应使用聚合管道的$lookup。
php
<?php
use MongoDB\Client;
echo "=== 跨集合查询解决方案 ===\n\n";
echo "问题: \$where中无法访问其他集合\n\n";
echo "解决方案: 使用聚合管道\$lookup\n";
echo "\$pipeline = [\n";
echo " [\n";
echo " '\$lookup' => [\n";
echo " 'from' => 'orders',\n";
echo " 'localField' => '_id',\n";
echo " 'foreignField' => 'customerId',\n";
echo " 'as' => 'orders'\n";
echo " ]\n";
echo " ],\n";
echo " [\n";
echo " '\$match' => ['orders' => ['\$ne' => []]]\n";
echo " ]\n";
echo "];\n";
?>问题4:JavaScript执行性能差怎么办?
问题描述:使用$where查询时性能很差。
回答内容:
$where查询需要为每个文档执行JavaScript,无法使用索引。应优先使用标准查询操作符。
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
echo "=== JavaScript性能优化 ===\n\n";
echo "性能问题原因:\n";
echo " 1. \$where为每个文档执行JavaScript\n";
echo " 2. 无法使用索引\n";
echo " 3. 需要序列化/反序列化文档\n\n";
echo "优化策略:\n";
echo " 1. 先用标准查询过滤,再用\$where\n";
echo " 2. 使用\$expr替代\$where\n";
echo " 3. 预计算并存储结果\n\n";
echo "示例 - 先过滤再执行:\n";
echo " 优化前: ['\$where' => js]\n";
echo " 优化后: ['status' => 'active', '\$where' => js]\n";
?>问题5:如何调试JavaScript代码?
问题描述:存储的JavaScript代码难以调试。
回答内容:
可以通过日志输出和单元测试来调试JavaScript代码。
php
<?php
use MongoDB\BSON\Javascript;
echo "=== JavaScript调试技巧 ===\n\n";
class JavascriptDebugger
{
public function test(Javascript $js, array $testData): array
{
$results = [];
foreach ($testData as $data) {
$results[] = [
'input' => $data,
'code' => $js->getCode()
];
}
return $results;
}
public function validateSyntax(Javascript $js): bool
{
$code = $js->getCode();
$patterns = [
'/^\s*function\s*\(/' => true,
'/^\s*this\./' => true,
'/^\s*return\s/' => true,
'/^\s*[a-zA-Z_$]/' => true
];
foreach ($patterns as $pattern => $valid) {
if (preg_match($pattern, $code)) {
return true;
}
}
return false;
}
}
$debugger = new JavascriptDebugger();
$js = new Javascript('this.age >= 18');
echo "语法验证: " . ($debugger->validateSyntax($js) ? '通过' : '失败') . "\n";
$testData = [
['age' => 20],
['age' => 15],
['age' => 18]
];
echo "\n测试数据:\n";
foreach ($testData as $data) {
echo " age={$data['age']}: " . ($data['age'] >= 18 ? 'true' : 'false') . "\n";
}
?>问题6:Javascript与JavascriptWithScope的区别?
问题描述:什么时候使用Javascript,什么时候使用JavascriptWithScope?
回答内容:
Javascript只存储代码,JavascriptWithScope可以携带作用域变量。
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\BSON\JavascriptWithScope;
echo "=== Javascript vs JavascriptWithScope ===\n\n";
echo "1. Javascript (纯代码):\n";
$js = new Javascript('this.value > threshold');
echo " 代码: " . $js->getCode() . "\n";
echo " 问题: threshold未定义\n\n";
echo "2. JavascriptWithScope (带作用域):\n";
$jsws = new JavascriptWithScope(
'this.value > threshold',
['threshold' => 100]
);
echo " 代码: " . $jsws->getCode() . "\n";
echo " 作用域: threshold = 100\n";
echo " 说明: threshold在执行时可访问\n";
?>9. 实战练习
练习1:实现动态验证规则系统
练习描述:创建一个支持动态配置的验证规则系统。
解题思路:
- 定义验证规则存储结构
- 实现规则管理接口
- 支持规则的增删改查
参考代码:
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
class ValidationRuleSystem
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function addRule(string $name, string $field, string $code, string $message): void
{
$this->collection->insertOne([
'name' => $name,
'field' => $field,
'code' => new Javascript($code),
'message' => $message,
'createdAt' => new MongoDB\BSON\UTCDateTime()
]);
}
public function getRules(string $field = null): array
{
$query = $field ? ['field' => $field] : [];
return $this->collection->find($query)->toArray();
}
public function deleteRule(string $name): bool
{
$result = $this->collection->deleteOne(['name' => $name]);
return $result->getDeletedCount() > 0;
}
}
echo "=== 动态验证规则系统 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$ruleSystem = new ValidationRuleSystem($client->test->validation_rules);
$client->test->validation_rules->drop();
$ruleSystem->addRule('age_check', 'age', 'this.age >= 18', '年龄必须大于等于18岁');
$ruleSystem->addRule('email_check', 'email', '/^[\\w-]+@[\\w-]+\\.[a-z]{2,}$/i.test(this.email)', '邮箱格式不正确');
echo "已添加的规则:\n";
foreach ($ruleSystem->getRules() as $rule) {
echo " - {$rule->name}: " . $rule->code->getCode() . "\n";
}
?>练习2:实现计算字段存储
练习描述:存储带有计算逻辑的字段定义。
解题思路:
- 定义计算字段结构
- 存储计算公式
- 支持公式查询
参考代码:
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
class ComputedFieldManager
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function defineField(string $name, string $formula, array $dependencies = []): void
{
$this->collection->replaceOne(
['name' => $name],
[
'name' => $name,
'formula' => new Javascript($formula),
'dependencies' => $dependencies
],
['upsert' => true]
);
}
public function getField(string $name): ?array
{
$doc = $this->collection->findOne(['name' => $name]);
return $doc ? (array)$doc : null;
}
public function listFields(): array
{
return $this->collection->find()->toArray();
}
}
echo "=== 计算字段存储 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$manager = new ComputedFieldManager($client->test->computed_fields);
$client->test->computed_fields->drop();
$manager->defineField('total_price', 'this.price * this.quantity', ['price', 'quantity']);
$manager->defineField('discounted_price', 'this.price * (1 - this.discount)', ['price', 'discount']);
$manager->defineField('full_name', 'this.firstName + " " + this.lastName', ['firstName', 'lastName']);
echo "定义的计算字段:\n";
foreach ($manager->listFields() as $field) {
echo " - {$field->name}: " . $field->formula->getCode() . "\n";
}
?>练习3:实现规则引擎
练习描述:创建一个简单的业务规则引擎。
解题思路:
- 定义规则结构
- 实现规则匹配逻辑
- 返回匹配的规则列表
参考代码:
php
<?php
use MongoDB\BSON\Javascript;
use MongoDB\Client;
class SimpleRuleEngine
{
private $rulesCollection;
public function __construct($rulesCollection)
{
$this->rulesCollection = $rulesCollection;
}
public function addRule(string $name, string $condition, array $actions): void
{
$this->rulesCollection->insertOne([
'name' => $name,
'condition' => new Javascript($condition),
'actions' => $actions,
'priority' => 0
]);
}
public function getMatchingRules(array $context): array
{
return $this->rulesCollection->find([], ['sort' => ['priority' => -1]])->toArray();
}
public function executeRule(string $ruleName): array
{
$rule = $this->rulesCollection->findOne(['name' => $ruleName]);
if (!$rule) {
return [];
}
return [
'rule' => $rule->name,
'condition' => $rule->condition->getCode(),
'actions' => $rule->actions
];
}
}
echo "=== 简单规则引擎 ===\n\n";
$client = new Client('mongodb://localhost:27017');
$engine = new SimpleRuleEngine($client->test->rules);
$client->test->rules->drop();
$engine->addRule('vip_discount', 'this.customerLevel === "VIP"', [
['type' => 'discount', 'value' => 0.2]
]);
$engine->addRule('new_customer_bonus', 'this.isNewCustomer === true', [
['type' => 'bonus', 'value' => 100]
]);
echo "存储的规则:\n";
foreach ($engine->getMatchingRules([]) as $rule) {
echo " - {$rule->name}: " . $rule->condition->getCode() . "\n";
}
?>10. 知识点总结
核心概念回顾
| 概念 | 说明 | 重要程度 |
|---|---|---|
| BSON类型码 | 0x0D,存储JavaScript代码 | ⭐⭐⭐ |
| PHP类 | MongoDB\BSON\Javascript | ⭐⭐⭐ |
| $where查询 | 使用JavaScript过滤文档 | ⭐⭐ |
| MapReduce | map/reduce函数使用JavaScript | ⭐⭐ |
| 安全风险 | JavaScript注入、权限控制 | ⭐⭐⭐ |
| 性能影响 | 无法使用索引,全表扫描 | ⭐⭐⭐ |
关键技能掌握
必须掌握:
- Javascript对象的创建和基本操作
- 理解JavaScript类型的应用场景
- 掌握$where查询的基本用法
- 了解JavaScript执行的安全风险
建议掌握:
- MapReduce操作中JavaScript的使用
- JavaScript注入防护方法
- 代码审计和安全检查
- 性能优化策略
最佳实践清单
创建Javascript对象:
php
$js = new Javascript('this.age >= 18');安全使用$where:
php
$collection->find(['status' => 'active', '$where' => $js]);存储JavaScript代码:
php
$doc = ['name' => 'rule', 'code' => new Javascript('this.x > 0')];常见错误避免
| 错误 | 正确做法 |
|---|---|
| 直接拼接用户输入 | 使用安全的构建器 |
| 过度使用$where | 优先使用标准操作符 |
| 忽略权限控制 | 限制JavaScript执行权限 |
| 不做代码审计 | 检查危险模式 |
扩展学习方向
- 聚合管道:学习$expr和$function操作符
- MapReduce:深入了解MapReduce编程模型
- 安全最佳实践:MongoDB安全配置
- 性能优化:查询优化和索引策略
11. 拓展参考资料
官方文档
PHP驱动文档
相关技术文章
相关设计模式
- 规则引擎模式:将业务规则存储为可执行代码
- 策略模式:使用JavaScript实现可配置的策略
- 验证器模式:存储验证逻辑以便复用
- 计算字段模式:存储计算公式实现动态计算
相关章节
- ObjectId类型:文档唯一标识
- Date类型:时间处理
- Regex类型:正则表达式匹配
- Binary类型:二进制数据存储
版本兼容性
| MongoDB版本 | 特性支持 |
|---|---|
| 4.0+ | 限制JavaScript执行 |
| 4.4+ | $function操作符 |
| 5.0+ | 增强的聚合功能 |
| 6.0+ | 更严格的安全控制 |
