重构/获取PHP函数的源代码

72

我能否通过函数名以编程方式获取函数的源代码?

例如:

function blah($a, $b) { return $a*$b; }
echo getFunctionCode("blah");

有没有 PHP 自描述函数来重构函数/类的代码?(我是指不是从源文件中直接获取源代码。)

在 Java 中存在:http://java.sun.com/developer/technicalArticles/ALT/Reflection/


2
不太确定为什么这个问题被踩了。实际上我觉得它很有趣。 - Martin Bean
2
@Marek,Martin可能是因为Marek对Jules天真的回答留下了非常粗鲁的评论。 - Alin Purcaru
@Alin也许吧,但他的回答真的很糟糕。 - Marek Sebera
6
@Marek,请不要与此争论。我只想说你的评论是没必要的。如果答案不好,可以使用“踩”按钮,而不是侮辱他人。请理解,谢谢。 - Alin Purcaru
我看不到所说的答案或粗鲁的评论? - Martin Bean
@Martin 那个用户在几次获得反对票之后删除了它。 - Marek Sebera
6个回答

103

在使用 ReflectionFunction 的建议上进行扩展,您可以使用类似于以下代码:

$func = new ReflectionFunction('myfunction');
$filename = $func->getFileName();
$start_line = $func->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block
$end_line = $func->getEndLine();
$length = $end_line - $start_line;

$source = file($filename);
$body = implode("", array_slice($source, $start_line, $length));
print_r($body);

9
那不是“扩展”,而是“实施”。 - Alin Purcaru
2
我想指出有一些函数可以返回文件名和源代码行数(即:无需解析),但是因为你已经提到了反射类,所以我提到了“扩展”。正如你所指出的,该API文档不太好,因此代码可以帮助实现这一点。 - Brandon Horsley
@MarekSebera 这不是关于谁先回答的问题,而是关于你使用了哪个答案,记录一下。 - Ascherer
1
行号有些可疑(可能是平台差异)- 对我来说,上面的代码会剪切方法体的最后两行。这对我有效: $mA["body"] = implode("", array_slice($source, $func->getStartLine()-1, ($func->getEndLine()-$func->getStartLine()+1)));但是我没有使用 $func = new ReflectionFunction('myfunction'); 而是使用了 $reflectionClass->getMethod("myfunction") - 不确定。 - jakabadambalazs
1
绝对完美!我一直在寻找这个功能。我正在尝试实现类似于Python中的“with”语句,而这段代码使我能够使用包装器代码重写函数(在新名称下)。三个大拇指!它可以无需更正地工作,并且一旦用适当的语法修饰,它可以很好地集成。 - jlettvin
显示剩余2条评论

17

没有什么东西可以给你实际的函数代码。唯一接近的是ReflectionFunction类。对于类,您有ReflectionClass,它提供了类成员(常量、变量和方法)及其可见性,但仍然没有实际的代码。


解决方法(它需要读取源文件):
使用ReflectionFunction::export查找函数声明所在的文件名和行间隔,然后从那个文件中读取这些行的内容。使用字符串处理获取第一个 { 和最后一个 } 之间的内容。

注意:Reflection API文档不完善。自PHP 7.4以来,ReflectionFunction::export已弃用。


不错的解决方法,我会再给其他用户一些时间参与讨论,然后再将您的答案标记为已接受。 - Marek Sebera
4
注意:自PHP 7.4起,此方法已被弃用:https://www.php.net/manual/zh/reflectionfunction.export.php。 - Tomas Votruba

6

我们使用不同的操作系统进行编程,如gnu/linux、Windows和Mac...由于这个原因,在代码中会有不同的回车符。为了解决这个问题,我分叉了Brandon Horsley的答案,并准备检查不同的回车符,并从一个类的方法中获取代码,而不是从一个函数中获取:

$cn = 'class_example';
$method = 'method_example';

$func = new ReflectionMethod($cn, $method);

$f = $func->getFileName();
$start_line = $func->getStartLine() - 1;
$end_line = $func->getEndLine();
$length = $end_line - $start_line;

$source = file($f);
$source = implode('', array_slice($source, 0, count($source)));
// $source = preg_split("/(\n|\r\n|\r)/", $source);
$source = preg_split("/".PHP_EOL."/", $source);

$body = '';
for($i=$start_line; $i<$end_line; $i++)
    $body.="{$source[$i]}\n";

echo $body;

2
PHP_EOL不行吗? - SparK
1
是的:$lines = preg_split("/".PHP_EOL."/", $input); - ZiTAL
2
请使用file_get_contents()而不是file(),您可以删除$source = implode('', array_slice($source, 0, count($source)));这行代码。 - Richard Tyler Miles
请注意,PHP_EOL将返回运行此函数的系统本地行尾;它不会知道文件中的内容。如果您希望相同的代码能够检测所有不同的行尾组合,则原始正则表达式是正确的方法。话虽如此,使用file()返回一系列行,将它们组合成一个字符串,然后将它们拆分回行似乎非常低效。 - IMSoP

3

谢谢,最终函数。

function get_function($method,$class=null){

    if (!empty($class)) $func = new ReflectionMethod($class, $method);
    else $func = new ReflectionFunction($method);

    $f = $func->getFileName();
    $start_line = $func->getStartLine() - 1;
    $end_line = $func->getEndLine();
    $length = $end_line - $start_line;

    $source = file($f);
    $source = implode('', array_slice($source, 0, count($source)));
    $source = preg_split("/".PHP_EOL."/", $source);

    $body = '';
    for($i=$start_line; $i<$end_line; $i++)
        $body.="{$source[$i]}\n";

    return $body;   
}

1
请使用file_get_contents()而不是file(),您可以删除$source = implode('', array_slice($source, 0, count($source)));这行代码。 - Richard Tyler Miles

2
我有类似的需求,在了解到\ReflectionFunction只有关于开始和结束行的信息后,感觉有必要编写一些代码来提取闭包的代码,或者更可能是短闭包,当多个闭包存在于同一行甚至嵌套时(安全第一)。唯一的限制是你必须知道它是第一个、第二个等闭包,如果它们已经作为参数列表或数组传递,你可能会有所了解。
在我的情况下,我有非常具体的要求,但也许获取闭包代码的一般解决方案对其他人也有用,所以我将把它放在这里...
<?php
namespace Phluid\Transpiler;

use ReflectionFunction;

final class Source
{
    private const OPEN_NEST_CHARS = ['(', '[', '{'];
    private const CLOSE_NEST_CHARS = [')', ']', '}'];
    private const END_EXPRESSION_CHARS = [';', ','];

    public static function doesCharBeginNest($char)
    {
        return \in_array($char, self::OPEN_NEST_CHARS);
    }

    public static function doesCharEndExpression($char)
    {
        return \in_array($char, self::END_EXPRESSION_CHARS);
    }

    public static function doesCharEndNest($char)
    {
        return \in_array($char, self::CLOSE_NEST_CHARS);
    }

    public static function readFunctionTokens(ReflectionFunction $fn, int $index = 0): array
    {
        $file = \file($fn->getFileName());
        $tokens = \token_get_all(\implode('', $file));
        $functionTokens = [];
        $line = 0;

        $readFunctionExpression = function ($i, &$functionTokens) use ($tokens, &$readFunctionExpression) {
            $start = $i;
            $nest = 0;

            for (; $i < \count($tokens); ++$i) {
                $token = $tokens[$i];

                if (\is_string($token)) {
                    if (self::doesCharBeginNest($token)) {
                        ++$nest;
                    } elseif (self::doesCharEndNest($token)) {
                        if ($nest === 0) {
                            return $i + 1;
                        }

                        --$nest;
                    } elseif (self::doesCharEndExpression($token)) {
                        if ($nest === 0) {
                            return $i + 1;
                        }
                    }
                } elseif ($i !== $start && ($token[0] === \T_FN || $token[0] === \T_FUNCTION)) {
                    return $readFunctionExpression($i, $functionTokens);
                }

                $functionTokens[] = $token;
            }

            return $i;
        };

        for ($i = 0; $i < \count($tokens); ++$i) {
            $token = $tokens[$i];
            $line = $token[2] ?? $line;

            if ($line < $fn->getStartLine()) {
                continue;
            } elseif ($line > $fn->getEndLine()) {
                break;
            }

            if (\is_array($token)) {
                if ($token[0] === \T_FN || $token[0] === \T_FUNCTION) {
                    $functionTokens = [];
                    $i = $readFunctionExpression($i, $functionTokens);

                    if ($index === 0) {
                        break;
                    }

                    --$index;
                }
            }
        }

        return $functionTokens;
    }
}
Source::readFunctionTokens()方法返回类似于PHP自带的\token_get_all()函数的输出,只是过滤了从闭包开头到结尾的代码。因此,它是字符串和数组的混合,具体取决于PHP语法的需要,详见这里

用法:

$fn = [fn() => fn() => $i = 0, function () { return 1; }];
$tokens = Source::readFunctionTokens(new \ReflectionFunction($fn[1]), 1);

第二个参数为0将返回最外层作用域中第一个闭包的代码,而1将返回最外层作用域中第二个闭包的代码。如果您想使用它,请随意整理它,因为代码非常粗糙和原始。但是,它应该相当稳定和有能力,因为我们已经知道所有语法都是有效的,并且可以遵循基本语法规则。


非常好的工作!感谢您花时间完成这个。这正是我需要用于缓存闭包结果的东西。 - Tomas Votruba
如何解析文本源代码,例如保存在数据库中的代码。 - Loading

0

我会在这个帖子中添加另一种优秀代码的风味。这对我很有效。

请注意,您可以使用yourClassName::class替换self::class以帮助您的编辑器解析文件。

$methods = get_class_methods(self::class);

foreach ($methods as $method) {
    
    
    $func = new ReflectionMethod(self::class, $method);

    $f = $func->getFileName();

    $start_line = $func->getStartLine() - 1;

    $end_line = $func->getEndLine();

    $length = $end_line - $start_line;

    $source = file_get_contents($f);

    $source = preg_split('/' . PHP_EOL . '/', $source);

    $body = implode(PHP_EOL, array_slice($source, $start_line, $length));

    echo $body . PHP_EOL . PHP_EOL;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接