Reconstruct / get source code of a PHP function

24,339

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.

Share:
24,339
Marek Sebera
Author by

Marek Sebera

Updated on July 05, 2022

Comments

  • Marek Sebera
    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
    Marek Sebera almost 13 years
    nice workaround, i will just give it a little time for other users to take a part before checking your answer as accepted
  • Alin Purcaru
    Alin Purcaru almost 13 years
    Erm... That's not "expanding". That's "implementing".
  • Brandon Horsley
    Brandon Horsley almost 13 years
    Right, 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
    Ascherer over 11 years
    @MarekSebera its not about whos first, its about which answer you used, for the record
  • jakabadambalazs
    jakabadambalazs over 9 years
    There 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
    jlettvin over 9 years
    Absolutely 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
    AbiusX almost 6 years
    This is not such a great idea, specially if the code is not as nice as you'd expect it to be.
  • SparK
    SparK almost 5 years
    PHP_EOL wouldn't do it?
  • ZiTAL
    ZiTAL almost 5 years
    Yes: $lines = preg_split("/".PHP_EOL."/", $input);
  • Richard Tyler Miles
    Richard Tyler Miles almost 4 years
    just use file_get_contents() not file() and you can delete $source = implode('', array_slice($source, 0, count($source))); this line.
  • Richard Tyler Miles
    Richard Tyler Miles almost 4 years
    just use file_get_contents() not file() and you can delete $source = implode('', array_slice($source, 0, count($source))); this line.
  • Richard Tyler Miles
    Richard Tyler Miles almost 4 years
    Add this to the top of the code to help support cross platform carriage returns. ini_set("auto_detect_line_endings", true);
  • Tomas Votruba
    Tomas Votruba over 3 years
    Note: this method is deprecated since PHP 7.4: php.net/manual/en/reflectionfunction.export.php
  • Tomas Votruba
    Tomas Votruba almost 3 years
    Very nice work! Thank you for taking time with this. It's exacly what I need for caching closure results.
  • IMSoP
    IMSoP almost 3 years
    Note 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
    Loading over 2 years
    How to parse text source code, such as the code saved in database.