7
什么是字符串表达式?即,将我们常见的表达式文本写到了字符串中,如:"$age >= 20"$age 的值是动态的整型变量。

什么是字符串表达式计算?即,我们需要一段程序来执行动态的表达式,如给定一个含表达式的字符串变量并计算其结果,而表达式字符串是动态的,比如为客户A执行的表达式是 $orderCount >= 10,而为客户B执行的表达式是 $orderTotal >= 1000

场景在哪儿?同一份程序具有完全通用性,但差异就其中一个表达式而已,那么我们需要将其抽象出来,让表达式变成动态的、可配置的、或可生成的。

方案一:eval 函数

eval 函数可能是我们第一个想到的方案,也是最简单直接的方案。我们来试验下:

$a = 10;
var_dump(eval('return $a > 5;'));

// 输出:
// bool(true)

嗯~完全能满足我们的需求,因为 eval 函数执行的 PHP 表达式,只要字符串内表达式符合 PHP 语法就行。

但需注意的是,eval 函数可执行任意 PHP 代码,也就意味着权限大、风险高、不安全。如果你的字符串表达式来自于外部输入,那务必注意了请自行做好安全检查和过滤,并考虑风险。当然,执行的是外部输入表达式,非常不建议使用此函数。

方案二:include 临时文件

如何实现?将字符串表达式写入一个临时文件,然后 include 这个临时文件,执行完成后再删除这个临时文件。

方案依然很简单。需要考虑的有:

  • 临时文件会很多,一个请求就有很多个,文件的过期和删除务必考虑在内
  • 文件的读写,也就牵扯到了磁盘 IO,那性能必定受到严重影响

那这个方案我们还采用吗?

方案三:assert 断言

其实 assert 做不到字符串表达式的计算,但提出来也算个猜想,因为能实现 PHP 表达式是否合法的校验。

下例演示了如何验证某个字符串表达式是否为合法的 PHP 表达式:

try {
    assert('a +== 1');
} catch (Throwable $e) {
    echo $e->getMessage(), "\n";
}

运行结果:

Failure evaluating code: 
a +== 1

可依然面临一个问题,那就是安全性,因为与 eval 一样能执行任意代码。所以,从 PHP 7.2 开始就不可以再执行字符串类型的表达式了。关于 PHP assert 断言,可参考 你所不知的 PHP 断言(assert)

方案四:system/exec 函数

systemexecproc_openshell_execpassthru 等系列函数,本质上都是执行外部命令或脚本,以达到执行 PHP 代码的效果,与 include 实现类似,虽能实现但不安全

system('php -r "echo 1 + 2;"');

echo exec('php -r "echo 1 + 2;"');

方案五:create_function 函数

create_function 函数是匿名函数的前生临时替代品,虽然现今还未废弃。作用是什么呢?允许用字符串创建一个 lambda 风格的匿名函数。

函数语法定义:

create_function ( string $args , string $code ) : string

使用示例:

$newfunc = create_function('$a, $b', 'var_dump($a, $b); return $a === $b;');

var_dump($newfunc(1, 2));

示例输出:

int(1)
int(2)
bool(false)

发现完全能实现我们的场景需求~但是又来了,这个函数不安全。为什么呢?看下手册中的 Caution:

This function internally performs an eval() and as such has the same security issues as eval(). Additionally it has bad performance and memory usage characteristics.

If you are using PHP 5.3.0 or newer a native anonymous function should be used instead.

create_function 函数底层走的是 eval 函数,所以面临着与 eval 一样的安全问题。并且,create_function 函数性能低下、占用内存高。而这函数最初就是为了匿名函数而生的,从 PHP 5.3.0 开始就内置实现了匿名函数,所以通过 create_function 去创建 lambda 风格自定义函数就毫无存在的必要了。

方案六:include 文件流

为何又是 include

我们从官方手册中了解到,include 语句用于包含并运行指定文件,并且支持远程文件,比如 include 'http://www.example.com/file.php?foo=1&bar=2';

我们还从手册中能找到这句话:

如果“URL include wrappers”在 PHP 中被激活,可以用 URL(通过 HTTP 或者其它支持的封装协议——见支持的协议和封装协议)而不是本地文件来指定要被包含的文件。

此时,我们是否想起了熟悉的 php://inputscheme://... 风格内置或自定义的URL封装协议。而这些协议都有个特点,即可用于类似 fopen()file_exists()file_get_contents() 的文件系统函数打开。include 读取文件其实与这些函数是一致的。

那我们就可以使用 stream_wrapper_register() 来注册一个用 PHP 类实现的 URL 封装协议。该函数允许用户实现自定义的协议处理器和流,用于所有其它的文件系统函数中(例如 fopen()fread() 等)。关于如何实现并注册一个 Stream Wrapper,可参考官方手册,本文仅提供个最简单的示例,来实现字符串表达式的计算。

class VarStream
{
    private $string;
    private $position;

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        $path = explode('://', $path, 2)[1];

        // 此处可对传入的参数进行自定义解析,并作进一步的操作
        $this->string = $path;
        $this->position = 0;
        return true;
    }

    public function stream_read($count)
    {
        $ret = substr($this->string, $this->position, $count);
        $this->position += strlen($ret);
        return $ret;
    }

    public function stream_eof() {}

    public function stream_stat() {}

}

stream_wrapper_register("var", "VarStream");

try {

    $params = ['count' => 1];
    $expression = '($count += 111) - 8';
    $result = include 'var://<?php extract($params); return ' . $expression . ';';
    var_dump($result);

} catch (Throwable $t) {
    echo $t->getMessage();
}

输出结果:

int(104)

方案七:语法解析

这个方案就比较高大上许多,当然实现方式也难了太多。具体就是自己写个语法解析器,将代码字符串解析成 AST 语法树,然后再把语法树的内容计算成最终的值。

怎么实现呢?不用我们自己再去写了,已经有大佬写好了。当然,如果对 AST 语法解析感兴趣,那学习下如何实现是最好不过的了,会解析语法也就意味着可以自己写门语言了呀 ?

GitHub 中比较有名的 PHP 实现如下 2 个,很多代码静态分析器都是基于这 2 个库开发的。

我们来看个 nikic/php-parser 的例子:

<?php
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;

$code = <<<'CODE'
<?php

function test($foo)
{
    var_dump($foo);
}
CODE;

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}

$dumper = new NodeDumper;
echo $dumper->dump($ast) . "\n";

示例输出:

array(
    0: Stmt_Function(
        byRef: false
        name: Identifier(
            name: test
        )
        params: array(
            0: Param(
                type: null
                byRef: false
                variadic: false
                var: Expr_Variable(
                    name: foo
                )
                default: null
            )
        )
        returnType: null
        stmts: array(
            0: Stmt_Expression(
                expr: Expr_FuncCall(
                    name: Name(
                        parts: array(
                            0: var_dump
                        )
                    )
                    args: array(
                        0: Arg(
                            value: Expr_Variable(
                                name: foo
                            )
                            byRef: false
                            unpack: false
                        )
                    )
                )
            )
        )
    )
)

由此,我们可以任意的实现我们所需的,也不用担心安全性问题。

最后,总结下。我们尝试了很多种方法,都能解决我们或多或少的场景需求,但哪个最适合需要我们自己去考量,但思路值得我们去深入探讨。


感谢您的阅读,觉得内容不错,点个赞吧 ?

原文地址: https://shockerli.net/post/ph...

Jioby
2.9k 声望1.7k 粉丝

无善无恶心之体,有善有恶意之动。知善知恶是良知,为善去恶是格物。