74

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/

6
  • 2
    Not quite sure why this question was down-voted. I found it quite interesting actually. Commented Aug 11, 2011 at 13:35
  • 2
    @Marek, Martin Probably because of the very rude comment Marek left to Jules naive answer. Commented Aug 11, 2011 at 13:36
  • @Alin maybe, but his answer was really bad. Commented Aug 11, 2011 at 13:37
  • 6
    @Marek Let's not get into polemics with this. I'll just say that your comment was uncalled for. If an answer is bad you can use the downvote button, not insults. Commented Aug 11, 2011 at 13:38
  • I can't see said answer or rude comment? Commented Aug 11, 2011 at 13:55

6 Answers 6

105

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);
Sign up to request clarification or add additional context in comments.

7 Comments

Erm... That's not "expanding". That's "implementing".
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.
@MarekSebera its not about whos first, its about which answer you used, for the record
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.
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.
|
18

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

2 Comments

nice workaround, i will just give it a little time for other users to take a part before checking your answer as accepted
Note: this method is deprecated since PHP 7.4: php.net/manual/en/reflectionfunction.export.php
6

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;

4 Comments

PHP_EOL wouldn't do it?
Yes: $lines = preg_split("/".PHP_EOL."/", $input);
just use file_get_contents() not file() and you can delete $source = implode('', array_slice($source, 0, count($source))); this line.
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.
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;   
}

1 Comment

just use file_get_contents() not file() and you can delete $source = implode('', array_slice($source, 0, count($source))); this line.
3

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.

3 Comments

Very nice work! Thank you for taking time with this. It's exacly what I need for caching closure results.
How to parse text source code, such as the code saved in database.
oh, it should be very-very slooooow
0

I'll add another flavor of the great code in this feed. This worked for me.

Note you can replace self::class with yourClassName::class to help your editor resolve the file.

$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;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.