sterzik/expression

自定义用户定义的语法表达式解析库

v0.0.1 2024-03-01 13:16 UTC

This package is auto-updated.

Last update: 2024-09-17 21:41:16 UTC


README

入门

使用composer将包安装到项目中

composer require sterzik/expression

或者您可以使用任何PSR-4兼容的自动加载器,在项目的src/子目录中搜索命名空间Sterzik\Expression中的类。

以下示例中,我们假设所有类都是通过use语句导入的。因此,我们将不会引用完全限定的类名,而是仅引用“终结”类名,预期文件开头有use语句。例如,如果我们引用类Parser,实际上我们假设存在一个

use Sterzik\Expression\Parser;

hello world应用程序

require_once(PATH_TO_EXPRESSION_LIBRARY."/autoload.php");
use Sterzik\Expression\Parser;

$parser = new Parser();

#quotes are part of the string, backslash must be double encoded
#because of the php string syntax
$expr = $parser->parse('"Hello world!\\n"'); 
echo $expr->evaluate();

评估过程

评估表达式的过程分为两部分:解析和评估解析后的表达式。解析过程负责正确解释表达式(计算运算符优先级、解析括号等),而评估过程负责将值替换到该表达式中,并按照解析器已给出的顺序计算所有操作。

一旦表达式被解析,它可以被多次评估。

简单解析

$parser = new Parser();
$expression = $parser->parse("1+2*3");

结果$expression是表示解析表达式的Expression类的对象。

简单评估

$value = $expression->evaluate();

评估方法只是将常量替换到该表达式中,并计算结果,对于上面解析的表达式,结果应该是7

使用变量评估

$expr = $parser->parse("a+b*(c+d)");
$vars = ["a" => 1, "b" => 2, "c" => 3, "d" => 4];
$result = $expr->evaluate($vars);

构建自定义解析器

此库最大的优点在于构建自定义解析器和自定义评估器的可能性。让我们首先看看自定义解析器。

ParserSettings类

ParserSettings作为创建自定义解析器的工具。要创建自定义解析器,你需要一个ParserSettings类的实例。解析器设置实例包含有关

  • 定义的运算符
  • 常量
  • 变量别名

获取预定义的ParserSettings实例

目前,有两个预定义的ParserSettings类实例。

$defaultParserSettings = ParserSettings::get("default");
$emptyParserSettings = ParserSettings::get("empty");

虽然默认解析器设置包含预定义的默认运算符列表,但空的解析器设置是完全空的,可以定义自定义运算符。

定义自定义运算符

$ps = ParserSettings::get("empty");
$ps->opPriority(1);
$ps->opArity("L");
$ps->addOp("+");
$ps->addOp("*");

此段代码将定义两个具有相同优先级的运算符:“+”和“*”。使用这样的解析器(如何使用自定义解析器见下文)会导致评估表达式1+2*3的结果为9,因为现在这两个运算符的优先级都是1,并且左结合性(从左到右评估)。

也可以将运算符定义为标识符

$ps->addOp("plus");

在这种情况下,将能够评估表达式1 plus 2

定义运算符别名

默认情况下,运算符的名称将传递给评估器。因此,如果您定义一个运算符“+”,则字符串“+”将传递给评估器。如果您定义一个运算符“plus”,则字符串“plus”将传递给评估器。有时,将一个不同于运算符本身的字符串传递给评估器可能很有用。通常在您想为某个其他运算符定义别名的情况下。例如,您可以这样做

$ps->addOp("+");
$ps->addOp("plus", "+");

现在,“+”和“plus”运算符将传递相同的运算符“+ ”给评估器。在评估器中,定义一个用于“+”的函数就足够了,您不需要为“plus”定义相同的代码。

使用自定义解析器

首先,您需要从解析器设置创建自定义解析器

$parser = new Parser($parserSettings);

现在,您可以像往常一样使用解析器。例如

$expr = $parser->parse("1 plus 2");

您甚至可以将任何解析器设置定义为默认设置。在这种情况下,不传递任何参数创建新的解析器将根据这些默认解析器设置创建解析器。

$oldDefault = $parserSettings->setDefault();
$parser = new Parser(); #this parser uses $parserSettings as parser settings
$oldDefault->setDefault();
$parser2 = new Parser(); #this parser uses the original parser settings 

定义自定义常量

如果您想定义自定义常量,可以使用ParserSettings的此方法。

$parserSettings->addConstant("true", true);

这样的解析器将评估标识符true为PHP中的true值。

定义一元运算符

$parserSettings->addPrefixOp("++");
$parserSettings->addPostfixOp("--");

如果您想将相同的运算符同时定义为前缀运算符甚至后缀运算符,您需要通过传递给评估器的名称来区分它们。

$parserSettings->addPrefixOp("++x", "++");
$parserSettings->addPostfixOp("x++", "++");

传递给评估器的字符串可以是任何字符串,没有其他限制。因此,虽然字符串"++x"不能用作运算符,但它可以用作传递给评估器的别名。

定义多元运算符

多元运算符是接受两个以上参数的运算符。经典的C运算符?:是这种运算符的一个完美例子。

$parserSettings->addMultinaryOp("?:", "?", ":");

调用多元运算符时,第一个参数是传递给评估器的别名,这是必须的。

定义可变参数运算符

可变参数运算符可以接受可变数量的操作数,而所有操作数都传递给一个函数。对于可变参数运算符,参数数量不重要,因为它们作为整体进行评估。例如,使用具有以下内容的解析器:

$parserSettings->addVariadicOp(",");

将评估表达式a,b,c作为接受三个参数的运算符,(前缀表示法中的","(a,b,c))。而如果,被定义为标准的二进制左参数运算符,表达式将被评估为","(","(a,b),c)(前缀表示法)。

定义括号

$parserSettings->addParenthesis("()", "(", ")", false);

addParenthesis的参数

  1. 传递给评估器的别名
  2. 导致括号打开的运算符
  3. 导致括号关闭的运算符
  4. 如果允许0-参数括号,则为true。

定义前缀/后缀括号/索引

您甚至可以将括号定义为前缀/后缀一元运算符。例如,在许多语言中,标准函数调用可以理解为后缀括号/索引。例如,以下内容将定义标准函数调用运算符

$parserSettings->addPostfixIndex("fn()", "(", ")", true);

要定义前缀索引,例如,您可以运行

$parserSettings->addPrefixIndex("<<>>", "<<", ">>", false);

这将定义一个特殊的类似于类型转换的运算符。使用该运算符的表达式可能看起来像:<<A>>B

注意:在前缀和后缀索引中,第一个参数是应用索引的表达式,第二个参数是括号/括号内的表达式。

处理解析错误

解析器可以根据它如何处理解析错误以两种模式运行

  1. (默认)返回null,错误信息可以通过调用方法getLastError()获取。
  2. 如果发生错误,抛出ParserException类的异常。

解析错误模式可以针对每个请求动态设置,或直接通过构造函数传递。

通过返回null处理错误

$parser = new Parser($parserSettings, false); #creates a parser NOT throwing exceptions
$expr = $parser->parse($expressionString);
if ($expr === null) {
    echo "parser error: " . $parser->getLastError() . "\n";
    return;
} 

通过抛出异常处理错误

$parser = new Parser($parserSettings, true); #creates a parser in exception throwing mode
try {
    $expr = $parser->parse($expressionString);
} catch(ParserException $e) {
    echo "parser error: " . $e->getMessage() . "\n";
    return;
}

动态更改错误处理模式

$parser = new Parser($parserSettings);
$parser->throwExceptions(true); #parser in exception throwing mode
$parser->throwExceptions(false); #parser in "return null" mode

操作解析表达式

一旦表达式成功解析,就会返回一个Expression类的对象。此对象表示表达式的抽象评估树。有三个Expression子类。每个代表不同类型的表达式

  1. 常量
  2. 变量
  3. 操作

每个表达式都有一个重要方法:type(),它将返回表示该表达式类型的字符串

  • 常量返回类型const
  • 变量返回类型var
  • 操作返回类型op

常量表达式

每个常量表达式代表一个常量。例如,如果解析表达式12,它将得到一个值为12的常量表达式。可以通过这种方式访问该类型的属性

if ($expr->type() == "const") {
    $value = $expr->value();
    echo "The expression represents a constant with a value of: " . $value . "\n";
}

对于内置常量(即字符串和整数,也许在未来版本的库中还有浮点数),该表达式的值正是它在PHP中表示的标量。这意味着,整数由整数表示,字符串由字符串表示,如果实现,浮点数将由浮点数表示。

用户定义的常量将按照ParserSettings::addConstant()方法定义的方式传递。

常量的一个重要点是,每个常量值都将被评估。因此,在评估时,您可以以任何方式处理常量。

变量表达式

变量表达式代表一个变量。每个变量都有一个名称。名称可以是任何字符串,但当前解析器的实现将仅生成变量名称为C语言标识符的变量表达式。

可以通过这种方式访问变量表达式

if ($expr->type() == "var") {
    $name = $expr->name();
    echo "The expression represents a variable with a name: ".$name."\n";
}

运算表达式

运算表达式代表任何类型的运算。每个运算都有一个

  1. 运算标识符(任何字符串)
  2. 运算参数(表达式数组)

可以通过这种方式访问运算表达式

if ($expr->type() == "op") {
    $identifier = $expr->op();
    $arguments = $expr->arguments();
    #easy accessing arguments:
    foreach ($expr as $argument) {
        #do something with each argument
    }

}

创建自己的表达式

创建自己的表达式通常在执行表达式后处理(稍后介绍)时很有用

$constant = Expression::create("const", 15);
$variable = Expression::create("var", "abc");
$operation = Expression::create("op", "+", $constant, $variable); #alternative #1
$operation = Expression::create("op", "+", [$constant, $variable]); #alternative #2

转储和恢复表达式

一旦表达式被解析,将其存储并稍后重建表达式可能很有用。

转储表达式

有几种方法可以转储表达式

$dataStruct = $expr->dump();       #dumps as a php structure without objects (serializable to json) 
$dataJson   = $expr->dumpJson();   #dumps into a json encoded  string
$dataB64    = $expr->dumpBase64(); #dumps into a base64 encoded string

注意:如果您想使用json和base 64转储方法,您的表达式必须不包含任何无法在json中表示的常量。例如,如果您创建一个值为PHP对象引用的常量,它将无法在json中表示。但是,即使您使用对象引用作为常量,您仍然可以使用dump()方法。

恢复表达式

从之前转储的数据中,可以使用以下方法重建表达式

$expr = Expression::restore($dataStruct);
$expr = Expression::restoreJson($dataJson);
$expr = Expression::restoreBase64($dataB64);

恢复函数会检查用于恢复的数据的正确性。如果数据格式不正确,将返回一个null,而不是表达式。

评估表达式详细说明

如果您想评估任何表达式,您需要一个评估器。评估器是一个对象,负责计算每个表达式及其子表达式的值。评估过程始终从表示表达式的抽象树的叶子到根进行。

要构建自定义评估器,您需要定义如何评估所有三种类型的表达式(变量、常量和运算)。要评估变量,有一个“变量对象”可用,每次评估表达式时都可以将其传递给评估器。完全由评估器负责如何解释这个“变量对象”。虽然将变量对象理解为关联数组是一种好的做法,但这不是必需的。

有必要了解,任何评估器都依赖于所使用的解析器。必须确保与特定解析器一起使用的评估器实现解析器能够生成所有操作。

构建自定义评估器

 # obtain the default evaluator
 # (able to evaluate all operations from the default parser)
 $evaluator = Evaluator::get("default"); 
 
 # obtain the empty evaluator
 # (no operations are defined there)
 $evaluator = Evaluator::get("empty");

如果您将构建自定义评估器,一个好的主意是从空评估器开始。

使用自定义评估器

在单个位置使用评估器

$result = $expr->evaluate($varObject, $evaluator);

您也可以将评估器设置为默认值

$oldEvaluator = $evaluator->setDefault();
$expr->evaluate($varObject); # $evaluator need not be passed, because it is default now

###定义评估操作

定义常量评估

$evaluator->defConstant(function ($const) {
    return $const;
});

这是评估常量的典型示例。实际上,即使在评估器中,您也不需要定义此类常量评估。它默认以这种方式进行评估。

定义变量评估

$evaluator->defVar(function ($var, $varObject) {
    return $varObject[$var];
});

在这种情况下,上面的函数是默认的变量评估方式。如果您对这个结果满意,则无需定义它。

定义操作

$evaluator->defOp("+", function ($a, $b) {
    return $a + $b;
});

甚至可能使用多个函数执行同一操作。如果是这种情况,对于特定操作,将选择与操作参数数量最匹配的函数。例如,您可以定义

$evaluator->defOp("-", function ($a, $b) {
    return $a-$b;
});
$evaluator->defOp("-", function ($a) {
    return - $a;
});

这将导致对于二进制减法使用第一个函数,但对于一元减法使用第二个函数。这个特性不仅可以通过名称(在上面的两个例子中都是"-")来区分操作,甚至可以通过其参数数量来区分。虽然这个特性在简单的例子(如这个例子)中应该可以正确工作,但在更复杂的例子中依赖它并不是一个好主意。您仍然可以在解析器中直接区分操作,这在更复杂的情况下肯定更安全。

有时,您可能想定义一个默认的操作处理程序

$evaluator->defOpDefault(function ($operation, ...$arguments) {
    #do something with the operation and theirs arguments
    return $result
});

如果没有找到特定操作的特定处理程序,将调用默认处理程序。

在某些情况下,除了在默认情况下传递操作外,甚至对于特定操作也有可能传递操作。这可以通过将true作为defOp的第三个参数来实现。

$evaluator->defOp("-", function ($op, $a, $b) {
    #do something depending on $op
    return $result;
}, true);

某些操作可能更复杂。例如,标准的?:运算符。这个运算符保证,只有当第一个参数评估为true时,第二个参数才会评估,只有当第一个参数评估为false时,第三个参数才会评估。如果使用defOp()方法,这是不可能处理的,因为始终评估所有参数。因此,存在一个方法defOpEx

$evaluator->defOpEx("?:", function ($cond, $ifTrue, $ifFalse) {
    return $cond->value() ? $ifTrue->value() : $ifFalse->value();
});

在这种情况下,正确的评估是通过正确的PHP评估?:运算符来实现的。

L值

评估过程甚至能够处理赋值以及其他类型的操作。为此,我们需要L值。

从系统的角度来看,一个L值是LValue类的对象。这样的对象至少支持两种方法

  1. function lvalue() - 返回自身 ($this) - 对于L值本身来说,这并不是特别必要,但它对于L值包装器(参见后面)变得更加重要
  2. function value() - 返回L值应评估到的值

但通常,L值可能支持其他方法。例如

  1. function assign($value) - 将值分配给由L值表示的变量或内存空间
  2. function fncall($arguments) - 调用由L值表示的函数,并将参数$arguments传递给该函数

但通常,您可以定义任何您想要的方法。唯一必须实现的方法是value()方法。如果您想使用赋值,将适当的函数命名为assign($value)是一个好习惯,但它的具体含义完全取决于您。

创建L值

有两种方法可以构建自定义L值

  1. 从抽象类LValue派生,并实现抽象的value()方法以及您想要实现的任何其他方法。
  2. 使用L值构建器。

虽然第一种方法很清楚,但我们将展示如何使用第二种方法

 function createVariableLValue($varName, $varObject)
 {
     $builder = new LValueBuilder();
     $builder->value(function () use ($varName, $varObject) {
         return $varObject[$varName];
     });
     $builder->assign(function ($value) use ($varName, $varObject) {
         $varObject[$varName] = $value;
     });
     
     return $builder->getLValue();
     
 }

使用L值

您可以在评估器中的任何操作中使用L值作为结果。因此,使用上面定义的函数createVariableLValue(),您可以分配L值,例如变量

$evaluator->defVar(function ($var, $varObject) {
    return createVariableLValue($varName, $varObject);
});

注意:上面的示例只有在 $varObject 是一个对象引用时才能正常工作。因为要能够处理赋值,你需要通过引用传递变量对象。因此,存在一个名为 Variables 的类,它几乎像数组一样工作,但它是一个对象,所以它是通过引用传递的。下面是如何使用 Variables 类的示例

$varObject = new Variables(["a" => 1, "b" => 2, "c" => 3]);
$varObject["a"] = 2;
unset($varObject["c"]);
foreach($varObject as $variable => $value) {
    #do something for all variables and its values
}
$varArray = $varObject->asArray(); #convert back to an ordinary array

如果你想要定义一个赋值的操作符,你可以按以下方式进行

$evaluator->defOpEx("=", function ($a, $b) {
    $a->assign($b->value());
});

注意:变量 $a$b 不包含 L 值,而是 L 值包装器。实际上,在你能够访问 L 值之前,你需要评估适当的子表达式。这正是 L 值和 L 值包装器之间的区别:L 值包装器具有与 L 值相同的函数,但调用它们将产生评估子表达式的副作用。这意味着,这两段代码执行不同的操作

#piece #1
$evaluator->defOpEx("some_unary_operator", function ($arg) {
    return $arg->value() + $arg->value();
});
#piece #2
$evaluator->defOpEx("someoperator", function ($arg) {
    $arg = $arg->lvalue();
    return $arg->value() + $arg->value();
});

在第一段中,一元操作符的子表达式将被评估两次,而在第二段中,子表达式只被评估一次。

值得注意的是,调用 $argument->lvalue() 即使子表达式的评估产生普通值,也会产生一个 L 值。它将被简单地转换为一个只有 value() 方法定义的 L 值。但也可以控制如果对 lvalue 调用不支持的方法时会发生什么

$evaluator->defNotLvalue(function () {
    throw new Exception("LValue required for assignments");
});

如果有人尝试调用未定义的 L 值的方法,还可以构建一个默认方法在每次尝试调用时被调用。为此,可以使用 LValueBuildersetDefaultCallback() 方法

function createVariableLValue($varName, $varObject)
{
    $builder = new LValueBuilder();
    $builder->value(function () use ($varName, $varObject) {
        return $varObject[$varName];
    });
    $builder->assign(function ($value) use ($varName, $varObject) {
        $varObject[$varName] = $value;
    });

    $builder->setDefaultCallback(function ($method) {
        throw new Exception("Method ".$method." is not supported for that L-value");
    });

    return $builder->getLValue();
}

表达式后处理

有时,对解析后的表达式进行一些后处理是必要的。否则,评估表达式可能会变得过于复杂。虽然你可以在以任何方式解析表达式之后进行任何表达式后处理,但解析器本身也内置了一个简单机制,可以立即后处理任何表达式。这个机制并不打算执行复杂的后处理,而更像是简单任务的后处理器。

以下是一些表达式后处理的示例

  • 从表达式中删除括号 - 解析器本身将创建一个操作,即使对于括号也是如此。但实际上,括号仅用于改变操作符的评估顺序,并充当一个恒等操作符。因此,在解析后的表达式中维护括号绝对不是必要的。
  • 将两个操作组合在一起。例如:如果你想要实现一个函数调用操作符(如 function(a,b,c)),你实际上需要将后缀索引操作 () 与可变操作 , 结合成一个单独的操作。因为 , 操作符应该具有最低的优先级,你可以预期,可变操作 , 是表示表达式的抽象树中后缀索引操作 () 的直接祖先。

后处理器可以设置在 ParserSettings 的实例中。要设置后处理,只需使用 setPostprocessOp() 方法

$parserSettings->addParenthesis('()', '(', ')');
$parserSettings->setPostprocessOp('()', function ($expr, $op) {
    return $expr[0];
});

传递给 setPostprocessOp() 方法的函数负责后处理。它接受两个参数:要后处理的表达式和要后处理的操作。它应该返回应替换原始操作的表达式。整个替换过程是从抽象树的叶子到根。

一个更复杂的示例

$parserSettings->addPostfixIndex('fn()', '(', ')', true);
#ensure the op ',' will have the lowest priority
$parserSettings->opPriority('0');
$parserSettings->addVariadicOp(',');
$parserSettings->setPostprocessOp('fn()', function ($expr, $op) {
    # count($expr) is the number of arguments of the 'fn()' operation
    # possibilities for the number of arguments:
    # 0 - impossible
    # 1 - fncall without any argument
    # 2 - fncall with one or more arguments
    # 3 or more - impossible
    if (count($expr) != 2) {
        return $expr;
    }
    $arg = $expr[1];
    #if the second argument is not the ',' operator, we don't need to postprocess anything
    if ($arg->type() != "op" || $arg->op() != ",") {
        return $expr;
    }
    $newArguments = array_merge([$expr[0]], $arg->arguments());
    return Expression::create("op", "fn()", $newArguments);
});

后处理函数也可以返回两个特殊值

  • null - 与如果返回第一个参数相同。null 表示:“不要更改表达式”。
  • false - 后处理函数失败。解析过程将以错误结束。

还可以对变量和常量进行后处理

#convert all variables "parent" to some variable depending on the current context
$parserSettings->setPostprocessVar(function ($expr) {
    if ($expr->name() == "parent") {
        $parentVar = getCurrentParentName(); #obtain somehow the current parent name
        return Expression::create("var", $parentVar);
    }
    return null;
});

#convert all non-string constants to strings
$parserSettings->setPostprocessConst(function ($expr) {
    if (!is_string($expr->value())) {
        return Expression::create("const", (string)$expr->value());
    }
    return null;
});

此外,还可以为所有操作创建一个通用的后处理处理器

$parserSettings->setDefaultPostprocessOp(function ($expr, $op) {
    return Expression::create("op", "postprocessed:$op", $expr->arguments());
});