I have a similar need and after learning that \ReflectionFunction
only has information on the start and end lines, felt it necessary to write some code to extract the code of a closure or more likely a short closure when multiple may exist on the same line and even be nested (better safe than sorry). The one caveat is you have to know whether it's the 1st, 2nd, etc. closure, which you probably do somewhat if they have been passed as an argument list or array.
I have very specific desires in my case, but maybe the general solution of getting the code of a closure will be useful to others, so I'll drop it here...
<?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;
}
}
The Source::readFunctionTokens()
method will return similar output to PHP's own \token_get_all()
function, just filtered down to only the code from the start of the closure to the end. As such, it's a mix of strings and arrays depending on PHP's syntactical needs, see here.
Usage:
$fn = [fn() => fn() => $i = 0, function () { return 1; }];
$tokens = Source::readFunctionTokens(new \ReflectionFunction($fn[1]), 1);
0 as the second arg will return the code for the first closure in the outermost scope, and 1 will return the second closure in the outermost scope. The code is very rough and raw so feel obliged to neaten it up if you wish to use it. It should be pretty stable and capable though, since we already know all syntax is valid and can go off basic syntax rules.