PHP 實現字符串表達式計算

什麼是字符串表達式?即,將咱們常見的表達式文本寫到了字符串中,如:"$age >= 20"$age 的值是動態的整型變量。php

什麼是字符串表達式計算?即,咱們須要一段程序來執行動態的表達式,如給定一個含表達式的字符串變量並計算其結果,而表達式字符串是動態的,好比爲客戶A執行的表達式是 $orderCount >= 10,而爲客戶B執行的表達式是 $orderTotal >= 1000html

場景在哪兒?同一份程序具備徹底通用性,但差別就其中一個表達式而已,那麼咱們須要將其抽象出來,讓表達式變成動態的、可配置的、或可生成的。git

方案一:eval 函數

eval 函數多是咱們第一個想到的方案,也是最簡單直接的方案。咱們來試驗下:github

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

// 輸出:
// bool(true)

嗯~徹底能知足咱們的需求,由於 eval 函數執行的 PHP 表達式,只要字符串內表達式符合 PHP 語法就行。shell

但需注意的是,eval 函數可執行任意 PHP 代碼,也就意味着權限大、風險高、不安全。若是你的字符串表達式來自於外部輸入,那務必注意了請自行作好安全檢查和過濾,並考慮風險。固然,執行的是外部輸入表達式,很是不建議使用此函數。express

方案二:include 臨時文件

如何實現?將字符串表達式寫入一個臨時文件,而後 include 這個臨時文件,執行完成後再刪除這個臨時文件。安全

方案依然很簡單。須要考慮的有:app

  • 臨時文件會不少,一個請求就有不少個,文件的過時和刪除務必考慮在內
  • 文件的讀寫,也就牽扯到了磁盤 IO,那性能一定受到嚴重影響

那這個方案咱們還採用嗎?函數

方案三:assert 斷言

其實 assert 作不到字符串表達式的計算,但提出來也算個猜測,由於能實現 PHP 表達式是否合法的校驗。post

下例演示瞭如何驗證某個字符串表達式是否爲合法的 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/php-expression-string?source=cnblogs

原文出處:https://www.cnblogs.com/shockerli/p/php-expression-string.html

相關文章
相關標籤/搜索