用php写编译器,你是不是讲笑话?不是,我是认真的。
虽然,国外网站上已经有多篇关于用PHP写一个编译器的文章,但那都是属于讲解性的文档。今天我们要做的,是对RQL语言进行编译或者说解析。
那么,RQL是什么呢?RQL是Resource Query Language的简称,也就是说,它是一个资源查询语言。RQL和GraphQL是什么关系?其实没有关系,GraphQL本身就是资源查询,但它有很多附加的功能,比如,自省,数据类型。RQL就没有那么复杂,它只是查询。
为什么要写这个编译器或解析器呢?因为,如果使用RQL,就可以增强RESTful API查询,一个项目中,大量的接口,均可以通过通用的代码完成。
那么,RQL语言具体有些什么样的语法呢? 相当简单,即是函数结构。
比如,a = b , RQL 写出来就是 eq(a,b)。更具体的,可以参考:
byteferry.github.io/rql-parser/…
那么,具体的需求是什么呢?
把RQL解析成MVC框架中,SERVICE层可用的参数,从而能够通过ApiBridge完成对应函数的调用。
很简单。但是,我们约定几个规范,
必须使用设计模式。所有函数,不得超过50行,if嵌套不得超过三层。变量,下划线式命名(考虑到数据库中均用下划线,另外,变量可以与其它类型有所区分),其它(函数,类等)均以驼峰命名。最终代码,必须用Travis CI 持续集成通过,代码覆盖率95%以上。这样做的目的,是真正做一个受欢迎的开源。真正方便未来维护。绝对不允许像Tp那样,一个函数近200行,测试代码覆盖率百分之二十多。
接下来就要真的动手了。这里得使用我们在大学的《编译原理》课程中的知识。
一个编译器,有以下几个组成部分,第一是,符号表,也就是,哪些东西是要编译的。
第二,是词法,第三,是列表词法,第四,是Token, 当然,还有AST,即Abstract Syntex Tree(抽像语法树)。估计,你已经晕了。
还是上代码,先上符号表
/*
* This file is part of the ByteFerry/Rql-Parser package.
*
* (c) BardoQi <67158925@qq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace ByteFerry\RqlParser\Lexer;
use ReflectionClass;
use ByteFerry\RqlParser\AstBuilder as Ast;
/**
* Class Symbols
*
* @package ByteFerry\RqlParser
*/
class Symbols
{
/**
* The keys of Symbol List Of Lexer
* 这里定义的是Token的类型,首先是把Token分类
*
* @var array
*/
public const T_WORD = 'T_WORD';
public const T_STRING = 'T_STRING';
public const T_OPEN_PARENTHESIS = 'T_OPEN_PARENTHESIS'; // (
public const T_CLOSE_PARENTHESIS = 'T_CLOSE_PARENTHESIS'; // )
public const T_PLUS = 'T_PLUS'; // +
public const T_COMMA = 'T_COMMA'; // ,
public const T_MINUS = 'T_MINUS'; // -
public const T_COLON = 'T_COLON'; // :
/**
* The Symbol List Of Lexer
* 这里是定义的是不同类型的Token所用的正则表达式。我们当然用正则表达式来获取Token,因为这样,代码最简单。
*
* @var array
*/
public static $symbol_expressions = [
'T_WORD' => '(?<T_WORD>\w+_*?\w*?)', // word
'T_OPEN_PARENTHESIS' => '(?<T_OPEN_PARENTHESIS>\({1})', // (
'T_CLOSE_PARENTHESIS' => '(?<T_CLOSE_PARENTHESIS>\){1})', // )
'T_STRING' => '(?<T_STRING>".*?")|(?<T_DOT>\.{1})', // ".*"
'T_COLON' => '(?<T_COLON>:{1})', // :
'T_COMMA' => '(?<T_COMMA>,{1})', // ,
'T_PLUS' => '(?<T_PLUS>\+{1})', // +
'T_MINUS' => '(?<T_MINUS>\-{1})', // -
];
/**
* we use the rules to ensure the rql language is correct
* we put the rules here only for doing the maintenance conveniently
* 这里定义的是语法检查规则。就是不同Token后面能够跟什么样的Token
*
* @var array
*/
public static $rules = [
'T_WORD' => ['T_OPEN_PARENTHESIS','T_CLOSE_PARENTHESIS','T_COMMA','T_COLON'],
'T_STRING' => ['T_OPEN_PARENTHESIS','T_CLOSE_PARENTHESIS','T_COMMA','T_COLON'],
'T_OPEN_PARENTHESIS' => ['T_WORD','T_STRING','T_PLUS','T_MINUS','T_CLOSE_PARENTHESIS'],
'T_CLOSE_PARENTHESIS' =>['T_CLOSE_PARENTHESIS','T_COMMA'],
'T_COLON'=>['T_WORD','T_STRING'],
'T_COMMA'=>['T_WORD','T_STRING','T_OPEN_PARENTHESIS','T_PLUS','T_MINUS'],
'T_PLUS'=>['T_WORD'],
'T_MINUS'=>['T_WORD']
];
/**
* list of operator aliases
* 这里定义的是Rql关键字的别名的映射关系,
* @var array
*/
public static $type_alias = [
'plus' =>'increment',
'minus'=>'decrement',
'cols'=>'columns',
'only'=>'columns',
'field'=>'columns',
'select'=>'columns',
'aggr'=>'aggregate',
'mean'=>'avg',
'nin' =>'out',
];
/**
* mapping the type to node type
* 这里定义的是RQL关键字与语法树的节点类型的映射关系
* @var array
*/
public static $type_mappings = [
'aggr' =>'N_COLUMN',
'aggregate' =>'N_COLUMN',
'all' =>'N_QUERY',
'and' =>'N_LOGIC',
'any' =>'N_QUERY',
'arr' =>'N_ARRAY',
'avg' =>'N_AGGREGATE',
'between' =>'N_PREDICATE',
'cols' =>'N_COLUMN',
'columns' =>'N_COLUMN',
'count' =>'N_QUERY',
'create' =>'N_QUERY',
'data' =>'N_DATA',
'decrement' =>'N_QUERY',
'delete' =>'N_QUERY',
'distinct' =>'N_COLUMN',
'empty' => 'N_CONSTANT',
'eq' =>'N_PREDICATE',
'except' =>'N_COLUMN',
'exists' =>'N_QUERY',
'false' => 'N_CONSTANT',
'filter' =>'N_FILTER',
'first' =>'N_QUERY',
'ge' =>'N_PREDICATE',
'gt' =>'N_PREDICATE',
'having' =>'N_FILTER',
'in' =>'N_PREDICATE',
'increment' =>'N_QUERY',
'is' => 'N_PREDICATE',
'le' =>'N_PREDICATE',
'like' =>'N_PREDICATE',
'limit' =>'N_LIMIT',
'lt' =>'N_PREDICATE',
'max' =>'N_AGGREGATE',
'mean' =>'N_AGGREGATE',
'min' =>'N_AGGREGATE',
'minus' =>'N_QUERY',
'ne' =>'N_PREDICATE',
'nin' =>'N_PREDICATE',
'not' =>'N_LOGIC',
'null'=>'N_CONSTANT',
'one' =>'N_QUERY',
'only' =>'N_COLUMN',
'or' =>'N_LOGIC',
'out' =>'N_PREDICATE',
'plus' =>'N_QUERY',
'search' =>'N_SEARCH',
'select' =>'N_COLUMN',
'sort' =>'N_SORT',
'sum' =>'N_AGGREGATE',
'true' => 'N_CONSTANT',
'update' =>'N_QUERY',
'values' =>'N_COLUMN',
];
/**
* mapping node type to class
* 这里再把上面的节点类型,映射到对应的节点类
* @var array
*/
public static $class_mapping = [
'N_AGGREGATE' => Ast\AggregateNode::class,
'N_ARRAY'=> Ast\ArrayNode::class,
'N_COLUMN' => Ast\ColumnsNode::class,
'N_CONSTANT' => Ast\ConstantNode::class,
'N_DATA' => Ast\DataNode::class,
'N_FILTER' => Ast\FilterNode::class,
'N_LIMIT' => Ast\LimitNode::class,
'N_LOGIC' => Ast\LogicNode::class,
'N_PREDICATE' => Ast\PredicateNode::class,
'N_QUERY' => Ast\QueryNode::class,
'N_SEARCH' => Ast\SearchNode::class,
'N_SORT' => Ast\SortNode::class,
];
/**
* 这里定义的是RQL的操作符与实际操作符的映射
* @var array
*/
public static $operators = [
'eq' => '=',
'ne' => '<>',
'gt' => '>',
'ge' => '>=',
'lt' => '<',
'le' => '<=',
'is' => 'is',
'in' => 'in',
'out' => 'not in',
'like' => 'like',
'between' => 'between',
'contains' => 'contains'
];
/**
* Query type mapping
* 这里定义的是RQL的查询类型,是读还是写
* @var array
*/
public static $query_type_mapping = [
'all' => 'Q_READ',
'any' => 'Q_READ',
'count' => 'Q_READ',
'create' => 'Q_WRITE',
'decrement' => 'Q_WRITE',
'delete' => 'Q_WRITE',
'exists' => 'Q_READ',
'first' => 'Q_READ',
'increment' => 'Q_WRITE',
'one' => 'Q_READ',
'update' => 'Q_WRITE',
];
/**
* 下面两个静态函数,用到时再讲
* @return array
* @throws \ReflectionException
*/
public static function getSymbolsKey()
{
$reflect = new ReflectionClass(__CLASS__);
return $reflect->getConstants();
}
/**
* @return string
*/
public static function makeExpression(){
$expression = '/';
$expression .= implode('|', self::$symbol_expressions);
return $expression . '/';
}
}
可以发现,这个类不太地道,不只是符号表,而是罗列了一堆映射关系。为什么这么做呢,目的相当简单。就是把所有类似于配置或映射的集中在这里,方便后续升级或修改。 接下来,我们要做一个解析器,来通过词法类,把输入的RQL先变成Token数组。
我们的解析器类代码如下:
declare(strict_types=1);
/*
* This file is part of the ByteFerry/Rql-Parser package.
*
* (c) BardoQi <67158925@qq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ByteFerry\RqlParser;
use ByteFerry\RqlParser\AstBuilder\NodeVisitor; //引用节点访问者类
use ByteFerry\RqlParser\Lexer\Lexer; //用词法Lexer类
use ByteFerry\RqlParser\AstBuilder\NodeInterface; //引用节点Interface
use ByteFerry\RqlParser\Lexer\Token; //引用Token类
use ByteFerry\RqlParser\Lexer\ListLexer; //引用列表词法ListLexer类
use ByteFerry\RqlParser\AstBuilder\ParamaterRegister; //参数注册表类
/**
* Class Parser
*
* @package ByteFerry\RqlParser
*/
class Parser
{
/**
* @var NodeInterface[]
*/
protected $node_list = [];
/**
* @param \ByteFerry\RqlParser\Lexer\ListLexer $tokens
* 这里也不复杂,关键使用了访问者模式。如果不清楚访问者模式,可能要看设计模式的书,脑补一下。
* @return \ByteFerry\RqlParser\AstBuilder\NodeInterface[]
*/
protected function load(ListLexer $ListLexer){
$ListLexer->rewind(); //对拿到的ListLexer重置编移量到第一个
/** @var Token $token */
$token = $ListLexer->current(); // 读取当前的,也就是,读第一个。
// 开始消费每一个token
for(; (false !== $token); $token = $ListLexer->consume()){
$symbol = $token->getSymbol(); //获取token的symbol
/** @var NodeInterface $node */
$node = NodeVisitor::visit($symbol); //再用访问者模式,获得真正的节点对象
$node->load($ListLexer); // 节点再载入ListLexer
$this->node_list[] = $node; // 把解析成的node存入数组
}
return $this->node_list; 返回node 列表
}
/**
* @param bool $is_segmaent
* 这个函数相当简单,根据不同的类型,返回不同的类的实例。
* @return QueryInterface
*/
protected static function getOutputObject( $is_fragmaent = false){
if(false === $is_fragmaent){
return Query::of();
}
return Fragment::of();
}
/**
* @param $string 这里是传入的RQL String
* @param bool $is_fragmaent 这里传入的是,RQL是一个片段,还是一个完整的查询
* 这里是一切的入口
*
* @return array
* @throws \ByteFerry\RqlParser\Exceptions\RegexException
*/
public static function parse($string, $is_fragmaent = false)
{
/** @var ListLexer $tokens */ // 首先,Lexer把RQL字符串,转换成tokens数组(列表词法类)
$tokens = Lexer::of()->tokenise($string);
$instance = new static(); // 创建当前类的实例
ParamaterRegister::newInstance(); // 初始化参数注册表
/** @var NodeInterface[] $node_list */
$node_list = $instance->load($tokens); // 通过load方法,将$tokens转换成节点列表。
$ir_list = [] ;
/** @var NodeInterface $node */
foreach($node_list as $node){
$ir_list[] = $node->build(); // 对每一个节点进行编对,存入到预编译列表中
}
$queries = [];
foreach ($ir_list as $ir) {
$query = self::getOutputObject($is_fragmaent); //再把它转换成Query对象数组,返回。
$queries[] = $query->from($ir);
}
return $queries; // 到此,最复杂的RQL,耗时不到3MS,效率相当高。
}
}
我们可以看出,这个解析器,只是抽象语法树的顶层。它只做了它清楚的事情,即:只是调用对应的类来完成。
接下来,词法类要登场了。
/*
* This file is part of the ByteFerry/Rql-Parser package.
*
* (c) BardoQi <67158925@qq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace ByteFerry\RqlParser\Lexer;
use ByteFerry\RqlParser\Abstracts\BaseObject;
use ByteFerry\RqlParser\Exceptions\ParseException;
/**
* Class Lexer
*
* @package ByteFerry\RqlParser
*/
class Lexer extends BaseObject
{
/**
* @var array
*/
protected $symbol_keys;
/**
* @var int
*/
protected $previous_type = -1;
/**
* @var ListLexer | null
*/
protected $listLexer = null; //ListLexer(token数组容器)。
/**
* Lexer constructor
*
* we need get the array of keys of the symbols first!
*
*/
public function __construct(){
$this->symbol_keys= Symbols::getSymbolsKey(); //我们调用了这个函数,把常量装进了一个数组。
}
/**
* The match data is in the target key and the offset!=-1
*
* @param $match
*
* @return array
*/
protected function getMatch($match){
foreach($this->symbol_keys as $key){ // 查出实际匹配的,
if(isset($match[$key]) && (-1 !== $match[$key][1])){
return [$key=>$match[$key]]; //转成可用的格式
}
}
return [];
}
/**
* @param $match
*
* @return mixed
*/
protected function addToken($match){
$key = key($match); // 上面转的那个格式 [$key=>$match[$key]] 所以,可以拿到key
[$symbol,$offset] = $match[$key]; // 再取出symbol,offset
// 通过addItem方法加到listLexer中,这里,token有一个previous_type 上一节点类型,及早及时写入,免得后续再要处理
$this->listLexer->addItem(Token::from($key,$symbol,$this->previous_type));
/**
* set the next_token_type for last token
*/
$this->listLexer->setNextType($key); // 对上一个节点,告诉它,下一个节点的类型是什么。
$this->previous_type = $key; //重置previous_type 到当前的key
return $offset + strlen($symbol); //返回 编移,告诉FOR循环,结束了没有
}
/**
* @param $rql_str
* 我们在Parser类中调用的是它,那我们看它做了什么
* @return \ByteFerry\RqlParser\Lexer\ListLexer
* @throws \ByteFerry\RqlParser\Exceptions\RegexException
*/
public function tokenise($rql_str){
//首先,创建 ListLexer实例
$this->listLexer = ListLexer::of();
/**
* using all the regular expressions
*/
$math_expression = Symbols::makeExpression(); //这里,调用了这一函数,把所有正则表达式装进了数组,到此,上面说的,等用到时再讲的两个函数,这里都讲到了。
$rql_str = trim($rql_str); // 去空格
$end_pos = strlen($rql_str); // 获取长度
for($offset=0;$offset<$end_pos;){ //循环匹配表达式,
preg_match($math_expression, $rql_str, $result,PREG_OFFSET_CAPTURE,$offset);
if (preg_last_error() !== PREG_NO_ERROR) { // 如果出错,抛出异常,因为,这很重要,要对程序员友好,不要做第二个Tp.
throw new ParseException(array_flip(get_defined_constants(true)['pcre'])[preg_last_error()]);
}
/**
* get the result from matches
*/
$match = $this->getMatch($result); //将匹配结果进行格式转换。
/**
* update the offset
*/
$offset = $this->addToken($match); 添加到本类的token数组中
}
if(0 !== $this->listLexer->getLevel()){ // 同样,如果括号不匹配,那也是语法错误,抛出异常。
throw new ParseException('The bracket are not paired.');
}
return $this->listLexer; // 返回listLexer
}
}
我们可以看出,Lexer只做了一件事,就是把字符串中通过正则匹配到的转换成Token,写到listLexer中,待后续进一步操作。 (待续)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!