Skip to content

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:实现动态验证规则系统

练习描述:创建一个支持动态配置的验证规则系统。

解题思路

  1. 定义验证规则存储结构
  2. 实现规则管理接口
  3. 支持规则的增删改查

参考代码

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:实现计算字段存储

练习描述:存储带有计算逻辑的字段定义。

解题思路

  1. 定义计算字段结构
  2. 存储计算公式
  3. 支持公式查询

参考代码

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:实现规则引擎

练习描述:创建一个简单的业务规则引擎。

解题思路

  1. 定义规则结构
  2. 实现规则匹配逻辑
  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过滤文档⭐⭐
MapReducemap/reduce函数使用JavaScript⭐⭐
安全风险JavaScript注入、权限控制⭐⭐⭐
性能影响无法使用索引,全表扫描⭐⭐⭐

关键技能掌握

必须掌握

  1. Javascript对象的创建和基本操作
  2. 理解JavaScript类型的应用场景
  3. 掌握$where查询的基本用法
  4. 了解JavaScript执行的安全风险

建议掌握

  1. MapReduce操作中JavaScript的使用
  2. JavaScript注入防护方法
  3. 代码审计和安全检查
  4. 性能优化策略

最佳实践清单

创建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执行权限
不做代码审计检查危险模式

扩展学习方向

  1. 聚合管道:学习$expr和$function操作符
  2. MapReduce:深入了解MapReduce编程模型
  3. 安全最佳实践:MongoDB安全配置
  4. 性能优化:查询优化和索引策略

11. 拓展参考资料

官方文档

PHP驱动文档

相关技术文章

相关设计模式

  • 规则引擎模式:将业务规则存储为可执行代码
  • 策略模式:使用JavaScript实现可配置的策略
  • 验证器模式:存储验证逻辑以便复用
  • 计算字段模式:存储计算公式实现动态计算

相关章节

版本兼容性

MongoDB版本特性支持
4.0+限制JavaScript执行
4.4+$function操作符
5.0+增强的聚合功能
6.0+更严格的安全控制

社区资源