Reconstruct / get source code of a PHP function
Solution 1
Expanding on the suggestion to use the ReflectionFunction, you could use something like this:
$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);
Solution 2
There isn't anything that will give you the actual code of the function. The only thing close to that available is the ReflectionFunction class. For classes you have ReflectionClass that gives you the class members (constants, variables and methods) and their visibility, but still no actual code.
Workaround (it does involve reading the source file):
Use ReflectionFunction::export to find out the file name and line interval where the function is declared, then read the content from that file on those lines. Use string processing to get what's between the first {
and the last }
.
Note: The Reflection API is poorly documented. ReflectionFunction::export
is deprecated since PHP 7.4
Solution 3
We program through different operating systems, gnu/linux, windows, mac... Due to this, we have different carriage returns in the code, to solve this, I forked the Brandon Horsley's answer and prepare to check different CR and get code from an class's method instead of a function:
$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;
Solution 4
thank you, final function
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;
}
Solution 5
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.
Marek Sebera
Updated on July 05, 2022Comments
-
Marek Sebera almost 2 years
Can I programmatically get the source code of a function by its name?
Like:
function blah($a, $b) { return $a*$b; } echo getFunctionCode("blah");
is it possible?
Are there any php self-descriptive functions to reconstruct function/class code? (I mean instead of getting source code right from the source file.)
In Java there exists: http://java.sun.com/developer/technicalArticles/ALT/Reflection/
-
Marek Sebera almost 13 yearsnice workaround, i will just give it a little time for other users to take a part before checking your answer as accepted
-
Alin Purcaru almost 13 yearsErm... That's not "expanding". That's "implementing".
-
Brandon Horsley almost 13 yearsRight, I wanted to point out that there were functions to return the filename and source lines specifically (ie: no parsing), but mentioned "expanding" because you had already pointed to the reflection classes. As you pointed out, the API is poorly documented, so code facilitates this.
-
Ascherer over 11 years@MarekSebera its not about whos first, its about which answer you used, for the record
-
jakabadambalazs over 9 yearsThere is something fishy with line numbers (it could be platform difference) - for me the code above cuts the last two lines of the method body. This works for me: $mA["body"] = implode("", array_slice($source, $func->getStartLine()-1, ($func->getEndLine()-$func->getStartLine()+1))); However I am not using
$func = new ReflectionFunction('myfunction');
but$reflectionClass->getMethod("myfunction")
- not sure. -
jlettvin over 9 yearsAbsolutely PERFECT! I've been searching for this capacity. I am trying to implement the equivalent of a python "with" statement, and this code enables me to rewrite a function (under a new name) with wrapper code. Three thumbs up. It worked without correction, and it integrates well once decorated with appropriate syntax.
-
AbiusX almost 6 yearsThis is not such a great idea, specially if the code is not as nice as you'd expect it to be.
-
SparK almost 5 yearsPHP_EOL wouldn't do it?
-
ZiTAL almost 5 yearsYes:
$lines = preg_split("/".PHP_EOL."/", $input);
-
Richard Tyler Miles almost 4 yearsjust use file_get_contents() not file() and you can delete
$source = implode('', array_slice($source, 0, count($source)));
this line. -
Richard Tyler Miles almost 4 yearsjust use file_get_contents() not file() and you can delete
$source = implode('', array_slice($source, 0, count($source)));
this line. -
Richard Tyler Miles almost 4 yearsAdd this to the top of the code to help support cross platform carriage returns.
ini_set("auto_detect_line_endings", true);
-
Tomas Votruba over 3 yearsNote: this method is deprecated since PHP 7.4: php.net/manual/en/reflectionfunction.export.php
-
Tomas Votruba almost 3 yearsVery nice work! Thank you for taking time with this. It's exacly what I need for caching closure results.
-
IMSoP almost 3 yearsNote that PHP_EOL will return the line ending native to whatever system this function runs on; it won't know anything about what's in the file. If you want the same code to be able to detect all the different line ending combinations, the original regex is the correct approach. That said, using
file()
to return an array of lines, combining them into a string, and then splitting them back into lines, seems very inefficient. -
Loading over 2 yearsHow to parse text source code, such as the code saved in database.