摘要:规则引擎用法及实现原理解读废话不多说,的官方地址是注意,本例中只拿普通数组做例子进行分析简介是一个用实现的依赖包,目的是实现一个数据过滤规则引擎。
规则引擎RulerZ用法及实现原理解读
废话不多说,rulerz的官方地址是:https://github.com/K-Phoen/ru...
注意,本例中只拿普通数组做例子进行分析1. 简介
RulerZ是一个用php实现的composer依赖包,目的是实现一个数据过滤规则引擎。RulerZ不仅支持数组过滤,也支持一些市面上常见的ORM,如Eloquent、Doctrine等,也支持Solr搜索引擎。
这是一个缺少中文官方文档的开源包,当然由于star数比较少,可能作者也觉得没必要。
在你的项目composer.json所在目录下运行:
composer require "kphoen/rulerz"3.使用 - 过滤
现有数组如下:
$players = [ ["pseudo" => "Joe", "fullname" => "Joe la frite", "gender" => "M", "points" => 2500], ["pseudo" => "Moe", "fullname" => "Moe, from the bar!", "gender" => "M", "points" => 1230], ["pseudo" => "Alice", "fullname" => "Alice, from... you know.", "gender" => "F", "points" => 9001], ];
初始化引擎:
use RulerZCompilerCompiler; use RulerZTarget; use RulerZRulerZ; // compiler $compiler = Compiler::create(); // RulerZ engine $rulerz = new RulerZ( $compiler, [ new TargetNativeNative([ // 请注意,这里是添加目标编译器,处理数组类型的数据源时对应的是Native "length" => "strlen" ]), ] );
创建一条规则:
$rule = "gender = :gender and points > :min_points"
将参数和规则交给引擎分析。
$parameters = [ "min_points" => 30, "gender" => "F", ]; $result = iterator_to_array( $rulerz->filter($players, $rule, $parameters) // the parameters can be omitted if empty ); // result 是一个过滤后的数组 array:1 [▼ 0 => array:4 [▼ "pseudo" => "Alice" "fullname" => "Alice, from... you know." "gender" => "F" "points" => 9001 ] ]4.使用 - 判断是否满足规则
$rulerz->satisfies($player, $rule, $parameters); // 返回布尔值,true表示满足5.底层代码解读
下面,让我们看看从创建编译器开始,到最后出结果的过程中发生了什么。
1.Compiler::create();
这一步是实例化一个FileEvaluator类,这个类默认会将本地的系统临时目录当做下一步临时类文件读写所在目录,文件类里包含一个has()方法和一个write()方法。文件类如下:
2.初始化RulerZ引擎,new RulerZ()
先看一下RulerZ的构建方法:public function __construct(Compiler $compiler, array $compilationTargets = []) { $this->compiler = $compiler; foreach ($compilationTargets as $targetCompiler) { $this->registerCompilationTarget($targetCompiler); } }这里的第一个参数,就是刚刚的编译器类,第二个是目标编译器类(实际处理数据源的),因为我们选择的是数组,所以这里的目标编译器是Native,引擎会将这个目标编译类放到自己的属性$compilationTargets。
public function registerCompilationTarget(CompilationTarget $compilationTarget): void { $this->compilationTargets[] = $compilationTarget; }3.运用filter或satisfies方法
这一点便是核心了。
以filter为例:public function filter($target, string $rule, array $parameters = [], array $executionContext = []) { $targetCompiler = $this->findTargetCompiler($target, CompilationTarget::MODE_FILTER); $compilationContext = $targetCompiler->createCompilationContext($target); $executor = $this->compiler->compile($rule, $targetCompiler, $compilationContext); return $executor->filter($target, $parameters, $targetCompiler->getOperators()->getOperators(), new ExecutionContext($executionContext)); }第一步会检查目标编译器是否支持筛选模式。
第二步创建编译上下文,这个一般统一是Context类实例public function createCompilationContext($target): Context { return new Context(); }第三步,执行compiler的compile()方法
public function compile(string $rule, CompilationTarget $target, Context $context): Executor { $context["rule_identifier"] = $this->getRuleIdentifier($target, $context, $rule); $context["executor_classname"] = "Executor_".$context["rule_identifier"]; $context["executor_fqcn"] = "RulerZCompiledExecutorExecutor_".$context["rule_identifier"]; if (!class_exists($context["executor_fqcn"], false)) { $compiler = function () use ($rule, $target, $context) { return $this->compileToSource($rule, $target, $context); }; $this->evaluator->evaluate($context["rule_identifier"], $compiler); } return new $context["executor_fqcn"](); } protected function getRuleIdentifier(CompilationTarget $compilationTarget, Context $context, string $rule): string { return hash("crc32b", get_class($compilationTarget).$rule.$compilationTarget->getRuleIdentifierHint($rule, $context)); } protected function compileToSource(string $rule, CompilationTarget $compilationTarget, Context $context): string { $ast = $this->parser->parse($rule); $executorModel = $compilationTarget->compile($ast, $context); $flattenedTraits = implode(PHP_EOL, array_map(function ($trait) { return " "."use ".ltrim($trait, "").";"; }, $executorModel->getTraits())); $extraCode = ""; foreach ($executorModel->getCompiledData() as $key => $value) { $extraCode .= sprintf("private $%s = %s;".PHP_EOL, $key, var_export($value, true)); } $commentedRule = str_replace(PHP_EOL, PHP_EOL." // ", $rule); return <<getCompiledRule()}; } } EXECUTOR; } 这段代码会依照crc13算法生成一个哈希串和Executor拼接作为执行器临时类的名称,并将执行器相关代码写进上文提到的临时目录中去。生成的代码如下:
// /private/var/folders/w_/sh4r42wn4_b650l3pc__fh7h0000gp/T/rulerz_executor_ff2800e8 :min_points and points > :min_points protected function execute($target, array $operators, array $parameters) { return ($this->unwrapArgument($target["gender"]) == $parameters["gender"] && ($this->unwrapArgument($target["points"]) > $parameters["min_points"] && $this->unwrapArgument($target["points"]) > $parameters["min_points"])); } }这个临时类文件就是最后要执行过滤动作的类。
FilterTrait中的filter方法是首先被执行的,里面会根据execute返回的布尔值来判断,是否通过迭代器返回符合条件的行。
execute方法就是根据具体的参数和操作符挨个判断每行中对应的cell是否符合判断来返回true/false。public function filter($target, array $parameters, array $operators, ExecutionContext $context) { return IteratorTools::fromGenerator(function () use ($target, $parameters, $operators) { foreach ($target as $row) { $targetRow = is_array($row) ? $row : new ObjectContext($row); if ($this->execute($targetRow, $operators, $parameters)) { yield $row; } } }); }satisfies和filter基本逻辑类似,只是最后satisfies是执行单条判断。
有一个问题,我们的编译器是如何知道我们设立的操作规则$rule的具体含义的,如何parse的?
Go further - 抽象语法树
这就涉及另一个问题了,抽象语法树(AST)。我们都知道php zend引擎在解读代码的过程中有一个过程是语法和词法分析,这个过程叫做parser,中间会将代码转化为抽象语法树,这是引擎能够读懂代码的关键步骤。
同样,我们在写一条规则字符串的时候,代码如何能够明白我们写的是什么呢?那就是抽象语法树。
以上面的规则为例:
gender = :gender and points > :min_points
这里, =、and、>都是操作符,但是机器并不知道他们是操作符,也不知道其他字段是什么含义。
于是rulerz使用自己的语法模板。
首先是默认定义了几个操作符。
function ($a, $b) { return sprintf("(%s && %s)", $a, $b); }, "or" => function ($a, $b) { return sprintf("(%s || %s)", $a, $b); }, "not" => function ($a) { return sprintf("!(%s)", $a); }, "=" => function ($a, $b) { return sprintf("%s == %s", $a, $b); }, "is" => function ($a, $b) { return sprintf("%s === %s", $a, $b); }, "!=" => function ($a, $b) { return sprintf("%s != %s", $a, $b); }, ">" => function ($a, $b) { return sprintf("%s > %s", $a, $b); }, ">=" => function ($a, $b) { return sprintf("%s >= %s", $a, $b); }, "<" => function ($a, $b) { return sprintf("%s < %s", $a, $b); }, "<=" => function ($a, $b) { return sprintf("%s <= %s", $a, $b); }, "in" => function ($a, $b) { return sprintf("in_array(%s, %s)", $a, $b); }, ]; $defaultOperators = [ "sum" => function () { return array_sum(func_get_args()); }, ]; $definitions = new Definitions($defaultOperators, $defaultInlineOperators); return $definitions->mergeWith($customOperators); } }在RulerZParserParser中,有如下方法:
public function parse($rule) { if ($this->parser === null) { $this->parser = CompilerLlk::load( new FileRead(__DIR__."/../Grammar.pp") ); } $this->nextParameterIndex = 0; return $this->visit($this->parser->parse($rule)); }这里要解读一个核心语法文件Grammar.pp,Pascal语法脚本:
// // Hoa // // // @license // // New BSD License // // Copyright © 2007-2015, Ivan Enderlin. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the Hoa nor the names of its contributors may be // used to endorse or promote products derived from this software without // specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // Inspired from HoaRulerGrammar. // // @author Stéphane Py// @author Ivan Enderlin // @author Kévin Gomez // @copyright Copyright © 2007-2015 Stéphane Py, Ivan Enderlin, Kévin Gomez. // @license New BSD License %skip space s // Scalars. %token true (?i)true %token false (?i)false %token null (?i)null // Logical operators %token not (?i)not %token and (?i)and %token or (?i)or %token xor (?i)xor // Value %token string ("|")(.*?)(? logical_operation() #operation )? operand: ::parenthesis_:: logical_operation() ::_parenthesis:: | value() parameter: | value: ::not:: logical_operation() #not | | | | | | | parameter() | variable() | array_declaration() | function_call() variable: ( object_access() #variable_access )* object_access: ::dot:: #attribute_access #array_declaration: ::bracket_:: value() ( ::comma:: value() )* ::_bracket:: #function_call: ::parenthesis_:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::_parenthesis:: 上面Llk::load方法会加载这个基础语法内容并解析出片段tokens,tokens解析的逻辑就是正则匹配出我们需要的一些操作符和基础标识符,并将对应的正则表达式提取出来:
array:1 [▼ "default" => array:20 [▼ "skip" => "s" "true" => "(?i)true" "false" => "(?i)false" "null" => "(?i)null" "not" => "(?i)not" "and" => "(?i)and" "or" => "(?i)or" "xor" => "(?i)xor" "string" => "("|")(.*?)(? "-?d+.d+" "integer" => "-?d+" "parenthesis_" => "(" "_parenthesis" => ")" "bracket_" => "[" "_bracket" => "]" "comma" => "," "dot" => "." "positional_parameter" => "?" "named_parameter" => ":[a-z-A-Z0-9_]+" "identifier" => "[^s()[],.]+" ] ]这一步也会生成一个rawRules
array:10 [▼ "#expression" => " logical_operation()" "logical_operation" => " operation() ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?" "operation" => " operand() (logical_operation() #operation )?" "operand" => " ::parenthesis_:: logical_operation() ::_parenthesis:: | value()" "parameter" => " | " "value" => " ::not:: logical_operation() #not | | | | | | | parameter() | variable() | array_declaration() | function_call( ▶" "variable" => " ( object_access() #variable_access )*" "object_access" => " ::dot:: #attribute_access" "#array_declaration" => " ::bracket_:: value() ( ::comma:: value() )* ::_bracket::" "#function_call" => " ::parenthesis_:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::_parenthesis::" ] 这个rawRules会通过analyzer类的analyzeRules方法解析替换里面的::表示的空位,根据$_ppLexemes属性的值,CompilerLlkLexer()词法解析器会将rawRules数组每一个元素解析放入双向链表栈(SplStack)中,然后再通过对该栈插入和删除操作,形成一个包含所有操作符和token实例的数组$rules。
array:54 [▼ 0 => Concatenation {#64 ▶} "expression" => Concatenation {#65 ▼ #_name: "expression" #_children: array:1 [▼ 0 => 0 ] #_nodeId: "#expression" #_nodeOptions: [] #_defaultId: "#expression" #_defaultOptions: [] #_pp: " logical_operation()" #_transitional: false } 2 => Token {#62 ▶} 3 => Concatenation {#63 ▼ #_name: 3 #_children: array:1 [▼ 0 => 2 ] #_nodeId: "#and" #_nodeOptions: [] #_defaultId: null #_defaultOptions: [] #_pp: null #_transitional: true } 4 => Token {#68 ▶} 5 => Concatenation {#69 ▶} 6 => Token {#70 ▶} 7 => Concatenation {#71 ▶} 8 => Choice {#72 ▶} 9 => Concatenation {#73 ▶} 10 => Repetition {#74 ▶} "logical_operation" => Concatenation {#75 ▶} 12 => Token {#66 ▶} 13 => Concatenation {#67 ▶} 14 => Repetition {#78 ▶} "operation" => Concatenation {#79 ▶} 16 => Token {#76 ▶} 17 => Token {#77 ▶} 18 => Concatenation {#82 ▶} "operand" => Choice {#83 ▶} 20 => Token {#80 ▶} 21 => Token {#81 ▼ #_tokenName: "named_parameter" #_namespace: null #_regex: null #_ast: null #_value: null #_kept: true #_unification: -1 #_name: 21 #_children: null #_nodeId: null #_nodeOptions: [] #_defaultId: null #_defaultOptions: [] #_pp: null #_transitional: true } "parameter" => Choice {#86 ▶} 23 => Token {#84 ▶} 24 => Concatenation {#85 ▶} 25 => Token {#89 ▶} 26 => Token {#90 ▶} 27 => Token {#91 ▶} 28 => Token {#92 ▶} 29 => Token {#93 ▶} 30 => Token {#94 ▶} "value" => Choice {#95 ▶} 32 => Token {#87 ▶} 33 => Concatenation {#88 ▶} 34 => Repetition {#98 ▶} "variable" => Concatenation {#99 ▶} 36 => Token {#96 ▶} 37 => Token {#97 ▶} "object_access" => Concatenation {#102 ▶} 39 => Token {#100 ▶} 40 => Token {#101 ▶} 41 => Concatenation {#105 ▶} 42 => Repetition {#106 ▶} 43 => Token {#107 ▶} "array_declaration" => Concatenation {#108 ▶} 45 => Token {#103 ▶} 46 => Token {#104 ▶} 47 => Token {#111 ▶} 48 => Concatenation {#112 ▶} 49 => Repetition {#113 ▶} 50 => Concatenation {#114 ▶} 51 => Repetition {#115 ▶} 52 => Token {#116 ▶} "function_call" => Concatenation {#117 ▶} ]然后返回HoaCompilerLlkParser实例,这个实例有一个parse方法,正是此方法构成了一个语法树。
public function parse($text, $rule = null, $tree = true) { $k = 1024; if (isset($this->_pragmas["parser.lookahead"])) { $k = max(0, intval($this->_pragmas["parser.lookahead"])); } $lexer = new Lexer($this->_pragmas); $this->_tokenSequence = new IteratorBuffer( $lexer->lexMe($text, $this->_tokens), $k ); $this->_tokenSequence->rewind(); $this->_errorToken = null; $this->_trace = []; $this->_todo = []; if (false === array_key_exists($rule, $this->_rules)) { $rule = $this->getRootRule(); } $closeRule = new RuleEkzit($rule, 0); $openRule = new RuleEntry($rule, 0, [$closeRule]); $this->_todo = [$closeRule, $openRule]; do { $out = $this->unfold(); if (null !== $out && "EOF" === $this->_tokenSequence->current()["token"]) { break; } if (false === $this->backtrack()) { $token = $this->_errorToken; if (null === $this->_errorToken) { $token = $this->_tokenSequence->current(); } $offset = $token["offset"]; $line = 1; $column = 1; if (!empty($text)) { if (0 === $offset) { $leftnl = 0; } else { $leftnl = strrpos($text, " ", -(strlen($text) - $offset) - 1) ?: 0; } $rightnl = strpos($text, " ", $offset); $line = substr_count($text, " ", 0, $leftnl + 1) + 1; $column = $offset - $leftnl + (0 === $leftnl); if (false !== $rightnl) { $text = trim(substr($text, $leftnl, $rightnl - $leftnl), " "); } } throw new CompilerExceptionUnexpectedToken( "Unexpected token "%s" (%s) at line %d and column %d:" . " " . "%s" . " " . str_repeat(" ", $column - 1) . "↑", 0, [ $token["value"], $token["token"], $line, $column, $text ], $line, $column ); } } while (true); if (false === $tree) { return true; } $tree = $this->_buildTree(); if (!($tree instanceof TreeNode)) { throw new CompilerException( "Parsing error: cannot build AST, the trace is corrupted.", 1 ); } return $this->_tree = $tree; }我们得到的一个完整的语法树是这样的:
Rule {#120 ▼ #_root: Operator {#414 ▼ #_name: "and" #_arguments: array:2 [▼ 0 => Operator {#398 ▼ #_name: "=" #_arguments: array:2 [▼ 0 => Context {#396 ▼ #_id: "gender" #_dimensions: [] } 1 => Parameter {#397 ▼ -name: "gender" } ] #_function: false #_laziness: false #_id: null #_dimensions: [] } 1 => Operator {#413 ▼ #_name: "and" #_arguments: array:2 [▼ 0 => Operator {#401 ▼ #_name: ">" #_arguments: array:2 [▼ 0 => Context {#399 ▶} 1 => Parameter {#400 ▶} ] #_function: false #_laziness: false #_id: null #_dimensions: [] } 1 => Operator {#412 ▶} ] #_function: false #_laziness: true #_id: null #_dimensions: [] } ] #_function: false #_laziness: true #_id: null #_dimensions: [] } }这里有根节点、子节点、操作符参数以及HoaRulerModelOperator实例。
这时$executorModel = $compilationTarget->compile($ast, $context);就可以通过NativeVisitor的visit方法对这个语法树进行访问和分析了。
这一步走的是visitOperator()
/** * {@inheritdoc} */ public function visitOperator(ASTOperator $element, &$handle = null, $eldnah = null) { $operatorName = $element->getName(); // the operator does not exist at all, throw an error before doing anything else. if (!$this->operators->hasInlineOperator($operatorName) && !$this->operators->hasOperator($operatorName)) { throw new OperatorNotFoundException($operatorName, sprintf("Operator "%s" does not exist.", $operatorName)); } // expand the arguments $arguments = array_map(function ($argument) use (&$handle, $eldnah) { return $argument->accept($this, $handle, $eldnah); }, $element->getArguments()); // and either inline the operator call if ($this->operators->hasInlineOperator($operatorName)) { $callable = $this->operators->getInlineOperator($operatorName); return call_user_func_array($callable, $arguments); } $inlinedArguments = empty($arguments) ? "" : ", ".implode(", ", $arguments); // or defer it. return sprintf("call_user_func($operators["%s"]%s)", $operatorName, $inlinedArguments); }那么编译好的规则可以通过以下方式得到:
$executorModel->getCompiledRule() // 规则就是 $this->unwrapArgument($target["gender"]) == $parameters["gender"] && ($this->unwrapArgument($target["points"]) > $parameters["min_points"] && $this->unwrapArgument($target["points"]) > $parameters["min_points"])自定义一个操作器由于官方文档太老且无更,所以如果你按照他的文档去自定义的话会哭晕,这里给出一个对应的示例。
$compiler = Compiler::create(); $rulerz = new RulerZ($compiler, [ new Native([ "length" => "strlen" ],[ "contains" => function ($a, $b) { return sprintf("strstr(%s, %s)", $a, $b); } ]) ]);上文中contains表示的是用系统函数strstr()来判断$a中是否包含$b字符,由于编译后的代码是通过字符串生成的,所以你在这个匿名函数中必须要用字符串表达判断逻辑,这也是其缺点之一。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/30936.html
摘要:示例输出第一步先不考虑插件,在已有的中是没有这个公共方法的,如果要简单实现的话可以通过钩子函数来,即在里面验证逻辑。按照插件的开发流程,应该有一个公开方法,在里面使用全局的方法添加一些组件选项,方法包含一个钩子函数,在钩子函数中验证。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)showImg(https://segmen...
摘要:导读阅读本文需要有足够的时间,笔者会由浅到深带你一步一步了解一个资深架构师所要掌握的各类知识点,你也可以按照文章中所列的知识体系对比自身,对自己进行查漏补缺,觉得本文对你有帮助的话,可以点赞关注一下。目录一基础篇二进阶篇三高级篇四架构篇五扩 导读:阅读本文需要有足够的时间,笔者会由浅到深带你一步一步了解一个资深架构师所要掌握的各类知识点,你也可以按照文章中所列的知识体系对比自身,对自己...
摘要:与此相对,强类型语言的类型之间不一定有隐式转换。三为什么是弱类型弱类型相对于强类型来说类型检查更不严格,比如说允许变量类型的隐式转换,允许强制类型转换等等。在中,加性运算符有大量的特殊行为。 从++[[]][+[]]+[+[]]==10?深入浅出弱类型JS的隐式转换 本文纯属原创? 如有雷同? 纯属抄袭? 不甚荣幸! 欢迎转载! 原文收录在【我的GitHub博客】,觉得本文写的不算烂的...
摘要:神策数据关海南营销策略引擎解读,以平台化构建营销新生态计算引擎图片神策数据关海南营销策略引擎解读,以平台化构建营销新生态微信在神策数据驱动大会现场,神策营销云架构师关海南发表了题为营销策略引擎的技术演进的演讲。 在神策 2021 数据驱动大会现场,神策营销云架构师关海南发表了题为《营销策略引擎(Express)...
阅读 2266·2023-04-25 14:17
阅读 1479·2021-11-23 10:02
阅读 2148·2021-11-23 09:51
阅读 781·2021-10-14 09:49
阅读 3334·2021-10-11 10:57
阅读 2910·2021-09-24 09:47
阅读 3024·2021-08-24 10:00
阅读 2271·2019-08-29 18:46