用PHP从零开始写一个编译器(四)

前面的链接:

用PHP从零开始写一个编译器(一)

用PHP从零开始写一个编译器(二)

用PHP从零开始写一个编译器(三)

上一节中我们讲到了查询条件中的逻辑表达式节点:LogicNode。逻辑符写链接的是什么呢?

除了not有可能与常量结合,其它的都是谓词表达式。而谓词表达式有一堆运算符。同样,我们将这些定义也放在Symbols类中了。

    /**
     * @var array
     */
    public static $operators = [
        'eq' => '=',
        'ne' => '<>',
        'gt' => '>',
        'ge' => '>=',
        'lt' => '<',
        'le' => '<=',
        'is' => 'is',
        'in' => 'in',
        'out' => 'not in',
        'like' => 'like',
        'between' => 'between',
        'contains' => 'contains'
    ];
复制代码

以上就是所有的谓词操作符。数组中的Key,就是RQL的函数名,可以说,真的相当简单。
比如,eq(a,b) 就是 a = b 。

那么我们来看看谓词节点的类:

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\AstBuilder;

use ByteFerry\RqlParser\Lexer\Symbols;
use ByteFerry\RqlParser\Exceptions\ParseException;

/**
 * Class PredicateNode
 *
 * @package ByteFerry\RqlParser\Ast
 */
class PredicateNode extends AstNode implements NodeInterface
{

    /**
     * @return mixed
     * 这里是对 between谓词的特殊操作
     */
    public function between(){
        [$a, $b, $c] = $this->stage;
        $paramaterRegister =ParamaterRegister::getInstance();
        $paramaterRegister->add($a . '_from',$b);
        $paramaterRegister->add($a . '_to',$c);   // 我们拆成了两个变量, xx_from xx_to
        $this->output[0] = sprintf(' %s BETWEEN %s and %s ', $a, $b, $c);
        return $this->output[0];
    }

    /**
     * @return mixed
     */
    public function build(){
        $this->buildChildren();
        $operator = Symbols::$operators[$this->operator]??null;
        $paramaterRegister =ParamaterRegister::getInstance();

        /**
         * for extend other predicate
         * 目前会调用between,但未来想增加也是可以的
         */
        if(method_exists($this,$operator)){
            return $this->$operator();
        }

        [$a, $b] = $this->stage;  //取出预编译的结果
        if(null === $operator){
            throw new ParseException('The operators ' . $this->symbol . ' is not defined in the parser! ');
        }
        $paramaterRegister->add($a,$b);  // 添加变量到注册表
        $this->output[0] = sprintf(' %s %s %s ', $a,$operator,$b);   // 格式化成 a 谓词 b的形式。
        return $this->output[0];
    }

}
复制代码

那么谓词节点的子节点是什么呢? 常量节点

常量,实际上是基本不用编译的,常量有以下几类,变量名(字段名),常量值,还有特殊常量。我们来看源码:

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\AstBuilder;

use ByteFerry\RqlParser\Lexer\ListLexer;

/**
 * Class ValueNode
 *
 * @package ByteFerry\RqlParser\AstBuilder
 */
class ConstantNode extends AstNode implements NodeInterface
{

    /**
     * @return int
     */
    protected function true(){
        return 1;
    }

    /**
     * @return int
     */
    protected function false(){
        return 0;
    }

    /**
     * @return string
     */
    protected function null(){
        return 'null';
    }

    /**
     * @return string
     */
    protected function empty(){
        return '""';
    }

    /**
     * @param \ByteFerry\RqlParser\Lexer\ListLexer $ListLexer
     *
     * @return int|void
     */
    public function load(ListLexer $ListLexer){
        return $ListLexer->getNextIndex();  //直接跳到下一个token
    }

    /**
     * @return mixed
     */
    public function build(){
        $symbol = $this->symbol;
        if(method_exists($this,$symbol)){
            $this->output[0] = $this->$symbol();
            return  $this->output[0];
        }
        $this->output[0] = $symbol;
        return  $this->output[0];
    }

}
复制代码

可以看出,这里定义了true(), false(), null(), empty() ,其它都是原样返回

谓词操作符中,有in和out,它们的第二个参数都是数组。比如:

in(age,(13,25,37))

这一情况下,我们需要有一个数组节点。代码如下:

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\AstBuilder;


/**
 * Class ArrayNode
 *
 * @package ByteFerry\RqlParser\AstBuilder
 */
class ArrayNode extends AstNode implements NodeInterface
{

    /**
     * @return mixed
     */
    public function build(){
        $this->buildChildren();
        $this->output[0] =  ' (' . implode(', ', $this->stage) . ') ';
        return $this->output[0];
    }

}
复制代码

可以看出,它就是把子节点再变回在括号内的逗号分隔字符串。

前面都是些基本查询,对于聚合查询,那一定有一些聚合函数。对于聚合函数,仍是按原样返回。请看代码:

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\AstBuilder;


/**
 * Class AggregateNode
 *
 * @package ByteFerry\RqlParser\AstBuilder
 */
class AggregateNode extends AstNode implements NodeInterface
{
    /**
     * @return mixed
     */
    public function build(){
        $this->buildChildren();
        $this->output[0] =  $this->operator . '(' . $this->stage[0]. ')';
        return $this->output[0];
    }

}
复制代码

至于有哪些聚合函数,我们在文档中有说明,请看文档:

byteferry.github.io/rql-parser/…

到此,我们还剩下4个节点,

  • 用作排序的,SortNode,
  • 用于分页的, LimitNode,
  • 用作搜索的:SearchNode,
  • 用于写操作的:DataNode,

排序节点:RQL示例:sort(+age, -score)

这是指,age顺序,score倒序。可见,RQL相当简洁。

以下是代码


/*
 * 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\AstBuilder;

use ByteFerry\RqlParser\Lexer\Symbols;
use ByteFerry\RqlParser\Lexer\ListLexer;

/**
 * Class SortNode
 *
 * @package ByteFerry\RqlParser\Ast
 */
class SortNode extends AstNode implements NodeInterface
{

    /**
     * @return string
     */
    public function getNodeType()
    {
        return 'sort';
    }

    /**
     * @param \ByteFerry\RqlParser\Lexer\ListLexer $ListLexer
     * 此函数是提供给load函数调用,返回要排序的哪个属性(列),以及方向。
     * @return \Generator|void
     */
    protected function getArguments(ListLexer $ListLexer){

        $token = $ListLexer->consume();  // 先消费一个token
        // the sort node will end with ')'
        // we only need consume the word token
        while(!$token->isClose()){
            $property = $token->getSymbol();
            $direction = 'ASC';  //默认给定 ASC

            // with prev type we could know the direction.
            if($token->getPrevType() === Symbols::T_MINUS){   前一个类型如果是T_MINUS,则就是DESC
                $direction = 'DESC';
            }
            yield [$property, $direction]; ,返回一个控代器给LOAD
            $token = $ListLexer->consume(); 

        }

    }

    /**
     * @param \ByteFerry\RqlParser\Lexer\ListLexer $ListLexer
     *
     * @return int|void
     */
    public function load(ListLexer $ListLexer){
        /**
         * set resource_name value
         * 这里只负责写入  stage
         */
        foreach($this->getArguments($ListLexer) as $argument){
            $this->stage[] = $argument;
        }
        // return the $index, perhaps there are other queries still.
        return $ListLexer->getNextIndex();
    }

    /**
     * @return mixed
     */
    public function build(){
        $this->output = [$this->getNodeType() => $this->stage];
        return $this->output;
    }

}
复制代码

可见,排序重载了load函数,这里又一次看到,Token中保存上一个类型,下一个类型的方便性了。

分页节点 LimitNode,

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\AstBuilder;


/**
 * Class LimitNode
 *
 * @package ByteFerry\RqlParser\Ast
 */
class LimitNode extends AstNode implements NodeInterface
{

    /**
     * @return mixed
     */
    public function build(){

        $this->buildChildren();
        $this->output = ['limit'=>[$this->stage[0],$this->stage[1]??0]] ;
        return $this->output;

    }

}
复制代码

分页本身也就上一到两个数字子节点,是常时子节点。至于这两个数字是offser和perPage还是page和perPage,我们不管,我们只把limit交出去就行。这样的好处是,自由度高一些。

搜索节点:SearchNode, 为什么要这个节点呢?因为,有时搜索是在多个字段中同时找的。所以,用SearchNode,单独传查询参数是一个好想法。同样,像Laravel框架中一些插件,可以让你定义Searchable,即哪些字段要查询。这就大大减少了代码量。

   /*
 * 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\AstBuilder;


/**
 * Class SearchNode
 *
 * @package ByteFerry\RqlParser\Ast
 */
class SearchNode extends AstNode implements NodeInterface
{

    /**
     * @return string
     */
    public function getNodeType()
    {
        return 'search';
    }

    /**
     * @return mixed
     */
    public function build(){

        $this->buildChildren();
        $query =  trim($this->stage[0],'\'"');  //常量节点会返回带引号的值,所以,变量名去掉引号
        if(trim($query,'%')===$query){
            $query .='%';  //没有通配符的,也加上
        }
        $this->output = [$this->getNodeType() =>$query];
        return $this->output;
    }

}
复制代码

写操作用的数据节点,DataNode,目前流行的RQL并没有写操作,当然,我现在也不清楚oracle的RQL是否支持写操作。数据节点的表示方式示例:data(age:12,score:89)

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\AstBuilder;


/**
 * Class DataNode
 *
 * @package ByteFerry\RqlParser\Ast
 */
class DataNode extends AstNode implements nodeInterface
{

    /**
     * @return mixed
     */
    public function build(){
        $this->buildChildren();
        foreach($this->pair() as $item){  //使用父类的pair迭代器,一次取两个
            [$property, $value] = $item;
            $this->output[$property] = trim($value,'""');  //写成数组
        }
        return ['data' => $this->output];
    }

}
复制代码

到目前为此,抽象语法树中的节点类我们都讲解完了。再复习一下程序的流程
1、拿到字符串以后,先用Lexer将其变成Token数组,放到ListLexer中,然后,Parser通过NodeVisitor创建节点,调用Load方法,实现数据加载为抽象语法树,同时也完成预编译。
接下来调用顶层build方法,完成编译,获得结果。build是深度优先的。所以,一定是最先编译叶节点,逐步向上返回。
下一节,我们要讲一些辅助的类,以及关于代码的单元测试了。(待续)

继续阅读:

用PHP从零开始写一个编译器(五)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享