30

I have a php file which contains only one class. how can I know what class is there by knowing the filename? I know I can do something with regexp matching but is there a standard php way? (the file is already included in the page that is trying to figure out the class name).

1

13 Answers 13

66

There are multiple possible solutions to this problem, each with their advantages and disadvantages. Here they are, it's up you to decide which one you want.

Tokenizer

This method uses the tokenizer and reads parts of the file until it finds a class definition.

Advantages

  • Do not have to parse the file entirely
  • Fast (reads the beginning of the file only)
  • Little to no chance of false positives

Disadvantages

  • Longest solution

Code

$fp = fopen($file, 'r');
$class = $buffer = '';
$i = 0;
while (!$class) {
    if (feof($fp)) break;

    $buffer .= fread($fp, 512);
    $tokens = token_get_all($buffer);

    if (strpos($buffer, '{') === false) continue;

    for (;$i<count($tokens);$i++) {
        if ($tokens[$i][0] === T_CLASS) {
            for ($j=$i+1;$j<count($tokens);$j++) {
                if ($tokens[$j] === '{') {
                    $class = $tokens[$i+2][1];
                }
            }
        }
    }
}

Regular expressions

Use regular expressions to parse the beginning of the file, until a class definition is found.

Advantages

  • Do not have to parse the file entirely
  • Fast (reads the beginning of the file only)

Disadvantages

  • High chances of false positives (e.g.: echo "class Foo {";)

Code

$fp = fopen($file, 'r');
$class = $buffer = '';
$i = 0;
while (!$class) {
    if (feof($fp)) break;

    $buffer .= fread($fp, 512);
    if (preg_match('/class\s+(\w+)(.*)?\{/', $buffer, $matches)) {
        $class = $matches[1];
        break;
    }
}

Note: The regex can probably be improved, but no regex alone can do this perfectly.

Get list of declared classes

This method uses get_declared_classes() and look for the first class defined after an include.

Advantages

  • Shortest solution
  • No chance of false positive

Disadvantages

  • Have to load the entire file
  • Have to load the entire list of classes in memory twice
  • Have to load the class definition in memory

Code

$classes = get_declared_classes();
include 'test2.php';
$diff = array_diff(get_declared_classes(), $classes);
$class = reset($diff);

Note: You cannot simply do end() as others suggested. If the class includes another class, you will get a wrong result.


This is the Tokenizer solution, modified to include a $namespace variable containing the class namespace, if applicable:

$fp = fopen($file, 'r');
$class = $namespace = $buffer = '';
$i = 0;
while (!$class) {
    if (feof($fp)) break;

    $buffer .= fread($fp, 512);
    $tokens = token_get_all($buffer);

    if (strpos($buffer, '{') === false) continue;

    for (;$i<count($tokens);$i++) {
        if ($tokens[$i][0] === T_NAMESPACE) {
            for ($j=$i+1;$j<count($tokens); $j++) {
                if ($tokens[$j][0] === T_STRING) {
                     $namespace .= '\\'.$tokens[$j][1];
                } else if ($tokens[$j] === '{' || $tokens[$j] === ';') {
                     break;
                }
            }
        }

        if ($tokens[$i][0] === T_CLASS) {
            for ($j=$i+1;$j<count($tokens);$j++) {
                if ($tokens[$j] === '{') {
                    $class = $tokens[$i+2][1];
                }
            }
        }
    }
}

Say you have this class:

namespace foo\bar {
    class hello { }
}

...or the alternative syntax:

namespace foo\bar;
class hello { }

You should have the following result:

var_dump($namespace); // \foo\bar
var_dump($class);     // hello

You could also use the above to detect the namespace a file declares, regardless of it containing a class or not.

Sign up to request clarification or add additional context in comments.

9 Comments

How would this work to include namespaces of the class as well? As in, returning the FULL class name prefixed with namespaces?
There is a problem when using Tokenizer solution and fread($fp, 512) returns code with "unfinished" comment, passing this string to token_get_all will result in PHP Warning "Unterminated comment starting line x..." this renders Tokenizer solution useless if you're using comments in code.
@netcoder, if (strpos($buffer, '{') === false) continue; will lead to a false positive if a class has a docblock with inline phpdoc comments (/** bla bla {@see SomeClass} {@link http://example.com}). So you need to filter them (T_DOC_COMMENT) out before check.
@netcoder For PHP 5.5+ code tokenizer solution gives false positive on use ::class statement because it have T_CLASS token code. Example: print_r(token_get_all('<?php $a=\stdClass::class;'));
I made my own function largely based on this answer with the new tokens provided in PHP 8 (we now have T_NAME_QUALIFIED for the namespace). It relies less on string matching and I've found it quite robust from some quick testing. Here is a gist: gist.github.com/cwhite92/f0aaf008e1679b27768fbb8c884df6f7
|
8

You can make PHP do the work by just including the file and get the last declared class:

$file = 'class.php'; # contains class Foo

include($file);
$classes = get_declared_classes();
$class = end($classes);
echo $class; # Foo

If you need to isolate that, wrap it into a commandline script and execute it via shell_exec:

$file = 'class.php'; # contains class Foo

$class = shell_exec("php -r \"include('$file'); echo end(get_declared_classes());\"");    
echo $class; # Foo

If you dislike commandline scripts, you can do it like in this question, however that code does not reflect namespaces.

3 Comments

I can't, the file is included automatically before my file runs and its not the last that is included.
@Dani: I added an example how to do that via commandline, however, you might run into problems when a class depends on another one etc. - however it should show you the idea.
Note that this approach require working autoloader for all parent classes and implemented interfaces. It also executes PHP code outside the class declaration (if such code exists), which may be undesired.
6

Thanks to some people from Stackoverflow and Github, I was able to write this amazing fully working solution:

/**
 * get the full name (name \ namespace) of a class from its file path
 * result example: (string) "I\Am\The\Namespace\Of\This\Class"
 *
 * @param $filePathName
 *
 * @return  string
 */
public function getClassFullNameFromFile($filePathName)
{
    return $this->getClassNamespaceFromFile($filePathName) . '\\' . $this->getClassNameFromFile($filePathName);
}


/**
 * build and return an object of a class from its file path
 *
 * @param $filePathName
 *
 * @return  mixed
 */
public function getClassObjectFromFile($filePathName)
{
    $classString = $this->getClassFullNameFromFile($filePathName);

    $object = new $classString;

    return $object;
}





/**
 * get the class namespace form file path using token
 *
 * @param $filePathName
 *
 * @return  null|string
 */
protected function getClassNamespaceFromFile($filePathName)
{
    $src = file_get_contents($filePathName);

    $tokens = token_get_all($src);
    $count = count($tokens);
    $i = 0;
    $namespace = '';
    $namespace_ok = false;
    while ($i < $count) {
        $token = $tokens[$i];
        if (is_array($token) && $token[0] === T_NAMESPACE) {
            // Found namespace declaration
            while (++$i < $count) {
                if ($tokens[$i] === ';') {
                    $namespace_ok = true;
                    $namespace = trim($namespace);
                    break;
                }
                $namespace .= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
            }
            break;
        }
        $i++;
    }
    if (!$namespace_ok) {
        return null;
    } else {
        return $namespace;
    }
}

/**
 * get the class name form file path using token
 *
 * @param $filePathName
 *
 * @return  mixed
 */
protected function getClassNameFromFile($filePathName)
{
    $php_code = file_get_contents($filePathName);

    $classes = array();
    $tokens = token_get_all($php_code);
    $count = count($tokens);
    for ($i = 2; $i < $count; $i++) {
        if ($tokens[$i - 2][0] == T_CLASS
            && $tokens[$i - 1][0] == T_WHITESPACE
            && $tokens[$i][0] == T_STRING
        ) {

            $class_name = $tokens[$i][1];
            $classes[] = $class_name;
        }
    }

    return $classes[0];
}

3 Comments

Do you have this solution published on github? I also found this article jarretbyrne.com/2015/06/197 , is it your?
Nop it's not published on Github but it's part of one of my open source projects on Github "Hello API" github.com/Mahmoudz/Hello-API/blob/master/app/Port/Butler/…, and no that article is not mine.
This solution will not work if you have defined multiple namespaces on a single file
6

I modified Nette\Reflection\AnnotationsParser that so it returns an array of namespace+classname that are defined in the file

$parser = new PhpParser();
$parser->extractPhpClasses('src/Path/To/File.php');


class PhpParser
{
    public function extractPhpClasses(string $path)
    {
        $code = file_get_contents($path);
        $tokens = @token_get_all($code);
        $namespace = $class = $classLevel = $level = NULL;
        $classes = [];
        while (list(, $token) = each($tokens)) {
            switch (is_array($token) ? $token[0] : $token) {
                case T_NAMESPACE:
                    $namespace = ltrim($this->fetch($tokens, [T_STRING, T_NS_SEPARATOR]) . '\\', '\\');
                    break;
                case T_CLASS:
                case T_INTERFACE:
                    if ($name = $this->fetch($tokens, T_STRING)) {
                        $classes[] = $namespace . $name;
                    }
                    break;
            }
        }
        return $classes;
    }

    private function fetch(&$tokens, $take)
    {
        $res = NULL;
        while ($token = current($tokens)) {
            list($token, $s) = is_array($token) ? $token : [$token, $token];
            if (in_array($token, (array) $take, TRUE)) {
                $res .= $s;
            } elseif (!in_array($token, [T_DOC_COMMENT, T_WHITESPACE, T_COMMENT], TRUE)) {
                break;
            }
            next($tokens);
        }
        return $res;
    }
}

Comments

5
$st = get_declared_classes();
include "classes.php"; //one or more classes in file, contains class class1, class2, etc...

$res = array_values(array_diff_key(get_declared_classes(),$st));
print_r($res); # Array ([0] => class1 [1] => class2 [2] ...)

1 Comment

This is a very clever answer!
3

You can do this in two ways:

  • complex solution: open the file and through regex extract the class-name (like /class ([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/)
  • simply solution: name all your php files with the class-name contained (eg: the class TestFoo in the file TestFoo.php or TestFoo.class.php)

1 Comment

the regex is provided directly from php.net documentation: php.net/manual/en/language.oop5.basic.php
2

You could get all declared classes before you include the file using get_declared_classes. Do the same thing after you have included it and compare the two with something like array_diff and you have your newly added class.

2 Comments

I can't... my file gets executed only after the file was included and I can't change it.
I believe the dot after first sentence need to be comma. If it is a dot, your first sentence becomes invalid since it is considered answer to question.
2

This sample returns all classes. If you're looking for a class which is derived of a specific one, use is_subclass_of

$php_code = file_get_contents ( $file );
    $classes = array ();
    $namespace="";
    $tokens = token_get_all ( $php_code );
    $count = count ( $tokens );

    for($i = 0; $i < $count; $i ++)
    {
        if ($tokens[$i][0]===T_NAMESPACE)
        {
            for ($j=$i+1;$j<$count;++$j)
            {
                if ($tokens[$j][0]===T_STRING)
                    $namespace.="\\".$tokens[$j][1];
                elseif ($tokens[$j]==='{' or $tokens[$j]===';')
                    break;
            }
        }
        if ($tokens[$i][0]===T_CLASS)
        {
            for ($j=$i+1;$j<$count;++$j)
                if ($tokens[$j]==='{')
                {
                    $classes[]=$namespace."\\".$tokens[$i+2][1];
                }
        }
    }
    return $classes;

1 Comment

good hint on is_subclass_of which will reduce the noise on what you are looking for
2

I spent lots of productive time looking for a way around this. From @netcoder's solution its obvious there are lots of cons in all the solutions so far.

So I decided to do this instead. Since most PHP classes has class name same as filename, we could get the class name from the filename. Depending on your project you could also have a naming convention. NB: This assume class does not have namespace

<?php
    $path = '/path/to/a/class/file.php';

    include $path;

    /*get filename without extension which is the classname*/
    $classname = pathinfo(basename($path), PATHINFO_FILENAME);

    /* you can do all this*/
    $classObj = new $classname();

    /*dough the file name is classname you can still*/
    get_class($classObj); //still return classname

Comments

1

Let me add a PHP 8 compatible solution as well. This will scan a file accordingly and return all FQCN's:

$file      = 'whatever.php';
$classes   = [];
$namespace = '';
$tokens    = PhpToken::tokenize(file_get_contents($file));

for ($i = 0; $i < count($tokens); $i++) {
    if ($tokens[$i]->getTokenName() === 'T_NAMESPACE') {
        for ($j = $i + 1; $j < count($tokens); $j++) {
            if ($tokens[$j]->getTokenName() === 'T_NAME_QUALIFIED') {
                $namespace = $tokens[$j]->text;
                break;
            }
        }
    }

    if ($tokens[$i]->getTokenName() === 'T_CLASS') {
        for ($j = $i + 1; $j < count($tokens); $j++) {
            if ($tokens[$j]->getTokenName() === 'T_WHITESPACE') {
                continue;
            }

            if ($tokens[$j]->getTokenName() === 'T_STRING') {
                $classes[] = $namespace . '\\' . $tokens[$j]->text;
            } else {
                break;
            }
        }
    }
}

// Contains all FQCNs found in a file.
$classes;

1 Comment

This should be the accepted answer at 2024! Example gist: gist.github.com/rboonzaijer/46af1e83ca8a018393ec8aab01a9c32e - It gives the complete namespace + classname (so you can instantiate the class directly with $model = new $className();) - It uses the new PhpToken class (which is more memory efficient then the older token_get_all() according to php.net/manual/en/class.phptoken.php ) - It returns multiple classnames in a single file
0

You may be able to use the autoload function.

function __autoload($class_name) {
    include "special_directory/" .$class_name . '.php';
}

And you can echo $class_name. But that requires a directory with a single file.

But it is standard practice to have one class in each file in PHP. So Main.class.php will contain Main class. You may able to use that standard if you are the one coding.

3 Comments

this is the inverse process: from the class name you generate a filepath, Dani instead need know the class name from the file.
It is not inverse, since you do not call it directly with a class name, but this will only work when the filename and class name are the same (now that I read on PHP's website) for example, MyClass.php contains MyClass class. So I would think, the naming convention strategy is the most efficient and fastest strategy in this case.
I agree with you on the naming convention, but personally I think the __autoload is useless here, for use that should be done.
0

This one does the trick for me:

/**
 * @param string $filepath
 * @return string|null
 */
public function getClassFromFile(string $filepath): ?string
{
    static $classes = [];

    if (empty($filepath = $filepath ? realpath($filepath) : null)) {
        return null;
    } elseif (array_key_exists($filepath, $classes)) {
        return $classes[$filepath];
    } 

    $basename = substr(basename($filepath), 0, -4);
    require_once($filepath);
    foreach (array_reverse(get_declared_classes()) as $classname) {
        if (! strcasecmp($basename, basename('/'.str_replace('\\', '/', $classname)))) {
            $reflector = new \ReflectionClass($classname);
            if ($reflector->getFileName() == $filepath) {
                return ($classes[$filepath] = $classname);
            }
        }
    }

    return ($classes[$filepath] = null);
}

Comments

0

You can regiester all classess automatically (in case of not using tools like composer) and have all corresponding files/classes mapping:

spl_autoload_register(function ($class) {
    $file = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
    echo "file is $file and class is $class\n";
    if (file_exists($file)) {
        require_once $file;
        return true;
    }
    return false;
});

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.