Reconstruct / get source code of a PHP function

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/


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.