定制 ryangjchandler/lexical 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

ryangjchandler/lexical

Composer 安装命令:

composer require ryangjchandler/lexical

包简介

Attribute-driven tokenisation for PHP.

README 文档

README

Latest Version on Packagist Tests Total Downloads

Installation

You can install the package via Composer:

composer require ryangjchandler/lexical

Usage

Let's write a simple lexer for mathematical expressions. The expressions can contain numbers (only integers) and a handful of operators (+, -, *, /).

Begin by creating a new enumeration that describes the token types.

enum TokenType
{
    case Number;
    case Add;
    case Subtract;
    case Multiply;
    case Divide;
}

Lexical provides a set of attributes that can be added to each case in an enumeration:

  • Regex - accepts a single regular expression.
  • Literal - accepts a string of continuous characters.
  • Error - designates a specific enumeration case as the "error" type.

Using those attributes with TokenType looks like this.

enum TokenType
{
    #[Regex("[0-9]+")]
    case Number;
    
    #[Literal("+")]
    case Add;
    
    #[Literal("-")]
    case Subtract;
    
    #[Literal("*")]
    case Multiply;

    #[Literal("/")]
    case Divide;
}

With the attributes in place, we can start to build a lexer using the LexicalBuilder.

$lexer = (new LexicalBuilder)
    ->readTokenTypesFrom(TokenType::class)
    ->build();

The readTokenTypesFrom() method is used to tell the builder where we should look for the various tokenising attributes. The build() method will take those attributes and return an object that implements LexerInterface, configured to look for the specified token types.

Then it's just a case of calling the tokenise() method on the lexer object to retrieve an array of tokens.

$tokens = $lexer->tokenise('1+2'); // -> [[TokenType::Number, '1', Span(0, 1)], [TokenType::Add, '+', Span(1, 2)], [TokenType::Number, '2', Span(2, 3)]]

The tokenise() method returns a list of tuples, where the first item is the "type" (TokenType in this example), the second item is the "literal" (a string containing the matched characters) and the third item is the "span" of the token (the start and end positions in the original string).

Skipping whitespace and other patterns

Continuing with the example of a mathematical expression, the lexer currently understands 1+2 but it would fail to tokenise 1 + 2 (added whitespace). This is because by default it expects each and every possible character to fall into a pattern.

The whitespace is insignificant in this case, so can be skipped safely. To do this, we need to add a new Lexer attribute to the TokenType enumeration and pass through a regular expression that matches the characters we want to skip.

#[Lexer(skip: "[ \t\n\f]+")]
enum TokenType
{
    // ...
}

Now the lexer will skip over any whitespace characters and successfully tokenise 1 + 2.

Error handling

When a lexer encounters an unexpected character, it will throw an UnexpectedCharacterException.

try {
    $tokens = $lexer->tokenise();
} catch (UnexpectedCharacterException $e) {
    dd($e->character, $e->position);
}

As mentioned above, there is an Error attribute that can be used to mark an enum case as the "error" type.

enum TokenType
{
    // ...

    #[Error]
    case Error;
}

Now when the input is tokenised, the unrecognised character will be consumed like other tokens and will have a type of TokenType::Error.

$tokens = $lexer->tokenise('1 % 2'); // -> [[TokenType::Number, '1'], [TokenType::Error, '%'], [TokenType::Number, '2']]

Custom Token objects

If you prefer to work with dedicated objects instead of Lexical's default tuple values for each token, you can provide a custom callback to map the matched token type and literal into a custom object.

class Token
{
    public function __construct(
        public readonly TokenType $type,
        public readonly string $literal,
        public readonly Span $span,
    ) {}
}

$lexer = (new LexicalBuilder)
    ->readTokenTypesFrom(TokenType::class)
    ->produceTokenUsing(fn (TokenType $type, string $literal, Span $span) => new Token($type, $literal, $span))
    ->build();

$lexer->tokenise('1 + 2'); // -> [Token { type: TokenType::Number, literal: "1" }, ...]

Token Producers

Regular expressions and literal tokens can get you quite far when it comes to tokenisation, but there are scenarios where it would be easier to write "real" code to tokenise your input.

Lexical makes this possible by providing a Token Producer API. Token producers are regular PHP objects that implement the RyanChandler\Lexical\Contracts\TokenProviderInterface or RyanChandler\Lexical\Contracts\TolerantTokenProviderInterface interfaces.

They are attached to your token types using the RyanChandler\Lexical\Attributes\Custom attribute, passing through the fully-qualified name of the token producer class.

use RyanChandler\Lexical\Attributes\Custom;
use RyanChandler\Lexical\InputSource;

enum Literals
{
    #[Custom(StringTokenProducer::class)]
    case String;
}

class StringTokenProducer implements TokenProducerInterface
{
    public function produce(InputSource $source): ?string
    {
        // 
    }
}

The InputSource object provided to the produce() method can be used to determine whether or not a token can be produced at the current offset. It comes with a range of utility methods such as current(), peek() and match().

If your token type has an Error case defined, your token producer will need to implement the TolerantTokenProducerInterface instead. This interface has an additional method, canProduce(), which is used to determine whether or not the token can be seen anywhere in the remaining input.

Here's an example token producer that tokenises double-quoted strings.

class StringTokenProducer implements TolerantTokenProducerInterface
{
    public function canProduce(InputSource $source): int|false
    {
        $matches = $source->match('/"/', PREG_OFFSET_CAPTURE);

        if (! $matches) {
            return false;
        }

        return $matches[0][1];
    }

    public function produce(InputSource $source): ?string
    {
        // If we're not looking at a double quote, return since we can't produce a token here.
        if ($source->current() !== '"') {
            return null;
        }

        // Place an offset marker in case we need to rewind at any point.
        $source->mark();

        // Consume the " character.
        $token = $source->consume();

        while ($source->current() !== '"') {
            // If we reach the end of the file before we find a closing double-quote,
            // we can rewind to the marker and return early.
            if ($source->isEof()) {
                $source->rewind();

                return null;
            }

            // Consume the current character.
            $token .= $source->consume();
        }

        // If we reach this point, we must be at a double-quote character since the
        // loop above has finished and we haven't returned yet.
        $token .= $source->consume();

        // Return the consumed text and let the Lexer handle the rest.
        return $token;
    }
}

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

ryangjchandler/lexical 适用场景与选型建议

ryangjchandler/lexical 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 372 次下载、GitHub Stars 达 54, 最近一次更新时间为 2023 年 06 月 07 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「lexical」 「ryangjchandler」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 ryangjchandler/lexical 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 ryangjchandler/lexical 我们能提供哪些服务?
定制开发 / 二次开发

基于 ryangjchandler/lexical 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 372
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 54
  • 点击次数: 25
  • 依赖项目数: 3
  • 推荐数: 0

GitHub 信息

  • Stars: 54
  • Watchers: 2
  • Forks: 2
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2023-06-07