当前位置: 首页 > 后端技术 > PHP

PHP实现字符串表达式计算

时间:2023-03-30 06:01:46 PHP

什么是字符串表达式?即把我们常用的表达式文本写成一个字符串,比如:“$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代码,权限大、风险高、不安全。如果你的字符串表达式来自于外部输入,你一定要注意,请自己做安全检查和过滤,考虑风险。当然,执行的是外部输入表达式,非常不推荐使用这个函数。方案二:如何实现includetemporaryfiles?将字符串表达式写入临时文件,然后包含临时文件,执行完成后删除临时文件。解决方案仍然很简单。需要考虑的事情:临时文件会很多,一次请求会有很多。必须考虑文件的过期和删除。文件的读写也会涉及到磁盘IO,性能会受到严重影响。那我们还是用这个方案。?解决方案三:assert断言assert其实不能做字符串表达式的计算,但是它被认为是一种猜想,因为它可以验证PHP表达式是否合法。以下示例演示了如何验证字符串表达式是否为有效的PHP表达式:try{assert('a+==1');}catch(Throwable$e){echo$e->getMessage(),"\n";}运行结果:Failureevaluatingcode:a+==1但是还有一个问题,就是安全性,因为它可以像eval一样执行任意代码。因此,从PHP7.2开始,不再可以执行字符串类型的表达式。关于PHP断言,你可以参考你不知道的PHP断言(assert)方案4:system/exec函数system、exec、proc_open、shell_exec、passthru等系列函数,本质上是执行外部命令或脚本来实现执行PHP代码的效果类似于include的实现,虽然可以实现但是不安全。system('php-r"echo1+2;"');echoexec('php-r"echo1+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)发现完全可以实现我们的场景需求~但是同样,这个函数是不安全的.为什么?请参阅手册中的警告:此函数在内部执行eval(),因此具有与eval()相同的安全问题。此外,它具有较差的性能和内存使用特性。如果您使用的是PHP5.3.0或更高版本,则应改用本机匿名函数。create_function在底部使用eval函数,因此它面临与eval相同的安全问题。而且create_function函数性能低,内存占用高。而这个函数最初是为匿名函数创建的。从PHP5.3.0开始,内置了匿名函数,所以不需要通过create_function创建lambda风格的自定义函数。解决方案6:为什么include文件流又被include了?我们从官方手册中得知,include语句用于包含并运行指定文件,支持远程文件,如include'http://www.example.com/file.php?foo=1&bar=2';。我们还可以从手册中找到这句话:如果在PHP中激活了“URLincludewrappers”,则可以使用URL(通过HTTP或其他支持的包装器-请参阅支持的协议和包装器)代替本地文件指定要包含的文件。至此,我们是不是想到了熟悉的php://input等scheme://...风格的内置或者自定义的URL封装协议。而且这些协议有一个特点,就是可以通过fopen()、file_exists()和file_get_contents()等文件系统函数打开。include读取文件其实和这些函数是一致的。然后我们可以使用stream_wrapper_register()来注册一个用PHP类实现的URL包装器。此函数允许用户实现自定义协议处理程序和流以用于所有其他文件系统函数(例如fopen()、fread()等)。StreamWrapper的实现和注册可以参考官方手册。本文只提供最简单的例子来实现字符串表达式的计算。类VarStream{私有$string;私人职位;publicfunctionstream_open($path,$mode,$options,&$opened_pa??th){$path=explode('://',$path,2)[1];//这里可以对传入的参数进行自定义解析,并进行进一步的操作$this->string=$path;$this->position=0;返回真;}publicfunctionstream_read($count){$ret=substr($this->string,$this->position,$count);$this->position+=strlen($ret);返回$ret;}publicfunctionstream_eof(){}publicfunctionstream_stat(){}}stream_wrapper_register("var","VarStream");try{$params=['count'=>1];$expression='($count+=111)-8';$result=include'var://getMessage();}outputresult:int(104)方案七:语法分析这个方案就高大上了,当然实现方法也是困难多了。具体来说,自己写一个语法解析器,将代码字符串解析成AST语法树,然后将语法树的内容计算成最终的值。如何实现?不用我们自己写,已经有大佬写了。当然,如果你对AST语法分析感兴趣,最好学习一下如何实现。知道如何解析语法是否意味着您可以编写自己的语言?GitHub中比较著名的PHP实现如下2,很多代码静态分析器都是基于这两个库开发的。PHP-Parser-最流行的PHPParser-微软件开源我们来看一个nikic/php-parser的例子:create(ParserFactory::PREFER_PHP7);try{$ast=$parser->parse($code);}catch(Error$error){echo"解析错误:{$error->getMessage()}\n";return;}$dumper=newNodeDumper;echo$dumper->dump($ast)。"\n";示例输出:array(0:Stmt_Function(byRef:falsename:Identifier(name:test)params:array(0:Param(type:nullbyRef:falsevariadic:falsevar:Expr_Variable(name:foo)默认值:null))returnType:nullstmts:array(0:Stmt_Expression(expr:Expr_FuncCall(name:Name(parts:array(0:var_dump))args:array(0:Arg(value:Expr_Variable(name:foo)byRef:falseunpack:false))))))由此,我们可以任意地实现我们所需要的,我们就不用担心安全问题了。最后,我们来总结一下。我们尝试了很多方法,都可以或多或少的解决我们的场景需求,但到底哪种方法最合适还需要我们自己去考虑,但是这个想法是值得我们深入探讨的。感谢阅读,我觉得内容不错,喜欢吗?原文地址:https://shockerli.net/post/ph...