okapi/code-transformer

PHP代码转换器是一个PHP库,允许您修改和转换已加载的PHP类的源代码。

1.3.7 2024-09-07 17:14 UTC

This package is auto-updated.

Last update: 2024-09-07 17:15:52 UTC


README

PHP代码转换器

License: MIT Twitter: @WalterWoshid PHP: >=8.1 Packagist Build

Coverage - PHP 8.1 Coverage - PHP 8.2

PHP代码转换器是一个PHP库,允许您修改和转换已加载的PHP类的源代码。

安装

composer require okapi/code-transformer

使用

📖 内容列表

创建一个内核

<?php

use Okapi\CodeTransformer\CodeTransformerKernel;

// Extend from the "CodeTransformerKernel" class
class Kernel extends CodeTransformerKernel
{
    // Define a list of transformer classes
    protected array $transformers = [
        StringTransformer::class,
        UnPrivateTransformer::class,
    ];
    
    // Define the settings of the kernel from the "protected" properties
    
    // The directory where the transformed source code will be stored
    protected ?string $cacheDir = __DIR__ . '/var/cache';
    
    // The cache file mode
    protected ?int $cacheFileMode = 0777;
}

创建一个转换器

// String Transformer

<?php

use Okapi\CodeTransformer\Transformer;
use Okapi\CodeTransformer\Transformer\Code;

// Extend from the "Transformer" class
class StringTransformer extends Transformer
{
    // Define the target class(es)
    public function getTargetClass(): string|array
    {
        // You can specify a single class or an array of classes
        // You can also use wildcards, see https://github.com/okapi-web/php-wildcards
        return MyTargetClass::class;
    }
    
    // The "transform" method will be called when the target class is loaded
    // Here you can modify the source code of the target class(es)
    public function transform(Code $code): void
    {
        // I recommend using the Microsoft\PhpParser library to parse the source
        // code. It's already included in the dependencies of this package and
        // the "$code->getSourceFileNode()" property contains the parsed source code.
        
        // But you can also use any other library or manually parse the source
        // code with basic PHP string functions and "$code->getOriginalSource()"

        $sourceFileNode = $code->getSourceFileNode();

        // Iterate over all nodes
        foreach ($sourceFileNode->getDescendantNodes() as $node) {
            // Find 'Hello World!' string
            if ($node instanceof StringLiteral
                && $node->getStringContentsText() === 'Hello World!'
            ) {
                // Replace it with 'Hello from Code Transformer!'
                // Edit method accepts a Token or Node class
                $code->edit(
                    $node->children,
                    "'Hello from Code Transformer!'",
                );
                
                // You can also manually edit the source code
                $code->editAt(
                    $node->getStartPosition() + 1,
                    $node->getWidth() - 2,
                    "Hello from Code Transformer!",
                );

                // Append a new line of code
                $code->append('$iAmAppended = true;');
            }
        }
    }
}
// UnPrivate Transformer

<?php

namespace Okapi\CodeTransformer\Tests\Stubs\Transformer;

use Microsoft\PhpParser\TokenKind;
use Okapi\CodeTransformer\Transformer;
use Okapi\CodeTransformer\Transformer\Code;

// Replace all "private" keywords with "public"
class UnPrivateTransformer extends Transformer
{
    public function getTargetClass(): string|array
    {
        return MyTargetClass::class;
    }

    public function transform(Code $code): void
    {
        $sourceFileNode = $code->getSourceFileNode();

        // Iterate over all tokens
        foreach ($sourceFileNode->getDescendantTokens() as $token) {
            // Find "private" keyword
            if ($token->kind === TokenKind::PrivateKeyword) {
                // Replace it with "public"
                $code->edit($token, 'public');
            }
        }
    }
}

目标类

<?php

class MyTargetClass
{
    private string $myPrivateProperty = "You can't get me!";

    private function myPrivateMethod(): void
    {
        echo 'Hello World!';
    }
}

初始化内核

// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered

<?php

use MyKernel;

require_once __DIR__ . '/vendor/autoload.php';

// Initialize the Code Transformer Kernel
$kernel = MyKernel::init();

目标类(已转换)

<?php

class MyTargetClass
{
    public string $myPrivateProperty = "You can't get me!";
    
    public function myPrivateMethod(): void
    {
        echo 'Hello from Code Transformer!';
    }
}
$iAmAppended = true;

结果

<?php

// Just use your classes as usual
$myTargetClass = new MyTargetClass();

$myTargetClass->myPrivateProperty; // You can't get me!
$myTargetClass->myPrivateMethod(); // Hello from Code Transformer!

限制

  • 通常xdebug会指向原始源代码,而不是转换后的代码。问题在于,如果您添加或删除一行代码,xdebug会指向错误的行,因此请尽量保持行数与原始源代码相同。

工作原理

  • CodeTransformerKernel 注册了多个服务

    • TransformerManager 服务存储转换器和它们的配置列表

    • CacheStateManager 服务管理缓存状态

    • StreamFilter 服务注册了一个 PHP 流过滤器,允许在PHP加载之前修改源代码

    • AutoloadInterceptor 服务覆盖了Composer自动加载器,该加载器处理类的加载

类加载时的通用工作流程

  • AutoloadInterceptor 服务拦截类的加载

  • TransformerMatcher 将类名与转换器目标类列表进行匹配

  • 如果类匹配,查询缓存状态以查看转换后的源代码是否已缓存

    • 检查缓存是否有效

      • 缓存过程的修改时间小于源文件或转换器的修改时间
      • 检查缓存文件、源文件和转换器是否存在
      • 检查转换器的数量是否与缓存中的转换器数量相同
    • 如果缓存有效,则从缓存中加载转换后的源代码

    • 如果没有,则向AutoloadInterceptor服务返回流过滤器路径

  • StreamFilter 通过应用匹配的转换器来修改源代码

    • 如果修改后的源代码与原始源代码不同,则将转换后的源代码缓存在缓存中
    • 如果没有,仍然将其缓存在缓存中,但不包含缓存的源文件路径,这样转换过程就不会再次执行

测试

  • 运行 composer run-script test
    或者
  • 运行 composer run-script test-coverage

显示您的支持

如果这个项目对您有帮助,请给一个⭐!

🙏 感谢

📝 许可证

版权所有 © 2023 Valentin Wotschel
本项目采用 MIT 许可证。