38

Given that each PHP file in our project contains a single class definition, how can I determine what class or classes are defined within the file?

I know I could just regex the file for class statements, but I'd prefer to do something that's more efficient.

2
  • What's the purpose of getting the class name of each file? The best solution should be tailored to fit your problem space. As it stands I feel as though there's probably a better solution depending on what you're looking to do. Commented Jan 12, 2010 at 17:44
  • 7
    It's been a while, but still: you could call get_declared_classes, save it, include the class file, and call get_declared_classes again. The difference is in that file. Simple. Commented Aug 25, 2011 at 21:41

9 Answers 9

64

I needed something like this for a project I am working on, and here are the functions I wrote:

function file_get_php_classes($filepath) {
  $php_code = file_get_contents($filepath);
  $classes = get_php_classes($php_code);
  return $classes;
}

function get_php_classes($php_code) {
  $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;
}
Sign up to request clarification or add additional context in comments.

5 Comments

Not all tokens are arrays so this can give some warnings.
@hakre: It wouldn't because if $tokens[$i] is a string, the syntax $tokens[i][0] is still allowed.
Right, but I suggest to use === for comparisons in that case.
A version with namespaces is available here: stackoverflow.com/questions/22761554/…
Just to be thorough, I wanted to verify that the syntax specified here matches the language specification. It does! github.com/php/php-langspec/blob/master/spec/…
16

If you just want to check a file without loading it use token_get_all():

<?php
header('Content-Type: text/plain');
$php_file = file_get_contents('c2.php');
$tokens = token_get_all($php_file);
$class_token = false;
foreach ($tokens as $token) {
  if (is_array($token)) {
    if ($token[0] == T_CLASS) {
       $class_token = true;
    } else if ($class_token && $token[0] == T_STRING) {
       echo "Found class: $token[1]\n";
       $class_token = false;
    }
  }       
}
?>

Basically, this is a simple finite state machine. In PHP the sequence of tokens will be:

  • T_CLASS: 'class' keyword;
  • T_WHITESPACE: space(s) after 'class';
  • T_STRING: name of class.

So this code will handle any weird spacing or newlines you get just fine because it's using the same parser PHP uses to execute the file. If token_get_all() can't parse it, neither can PHP.

By the way, you use token_name() to turn a token number into it's constant name.

Here is my c2.php:

<?php
class MyClass {
  public __construct() {
  }
}

class MyOtherClass {
  public __construct() {
  }
}
?>

Output:

Found class: MyClass
Found class: MyOtherClass

1 Comment

Please note that something like User::class is valid syntax for retrieving the FQCN string (like "App\Model\User") since PHP 5.5. So you either need to explicitly check for the T_WHITESPACE (and nothing else) in between, or you could check for an absence of T_COLON before T_CLASS.
6

Or you could easily use AnnotationsParser from Nette\Reflection (installable using composer):

use Nette\Reflection\AnnotationsParser;
$classes = AnnotationsParser::parsePhp(file_get_contents($fileName));
var_dump($classes);

Output will be then something like this:

array(1) {
  ["Your\Class\Name"] =>
  array(...) {
      // property => comment
  },
  ["Your\Class\Second"] =>
  array(...) {
      // property => comment
  },
}

The parsePhp() method basically does something similar as examples in other answers, but you don't have to declare nor test the parsing yourselves.

Comments

5

I needed parse classes from file with namespaces, so I modified code. If somebody need too, here is it:

public function getPhpClasses($phpcode) {
    $classes = array();

    $namespace = 0;  
    $tokens = token_get_all($phpcode); 
    $count = count($tokens); 
    $dlm = false;
    for ($i = 2; $i < $count; $i++) { 
        if ((isset($tokens[$i - 2][1]) && ($tokens[$i - 2][1] == "phpnamespace" || $tokens[$i - 2][1] == "namespace")) || 
            ($dlm && $tokens[$i - 1][0] == T_NS_SEPARATOR && $tokens[$i][0] == T_STRING)) { 
            if (!$dlm) $namespace = 0; 
            if (isset($tokens[$i][1])) {
                $namespace = $namespace ? $namespace . "\\" . $tokens[$i][1] : $tokens[$i][1];
                $dlm = true; 
            }   
        }       
        elseif ($dlm && ($tokens[$i][0] != T_NS_SEPARATOR) && ($tokens[$i][0] != T_STRING)) {
            $dlm = false; 
        } 
        if (($tokens[$i - 2][0] == T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] == "phpclass")) 
                && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
            $class_name = $tokens[$i][1]; 
            if (!isset($classes[$namespace])) $classes[$namespace] = array();
            $classes[$namespace][] = $class_name;
        }
    } 
    return $classes;
}

1 Comment

Couldn't make your snippet work with files with multiple namespaces
4

My snippet too. Can parse files with multiple classes, interfaces, arrays and namespaces. Returns an array with classes+types (class, interface, abstract) divided by namespaces.

<?php    
    /**
     * 
     * Looks what classes and namespaces are defined in that file and returns the first found
     * @param String $file Path to file
     * @return Returns NULL if none is found or an array with namespaces and classes found in file
     */
    function classes_in_file($file)
    {

        $classes = $nsPos = $final = array();
        $foundNS = FALSE;
        $ii = 0;

        if (!file_exists($file)) return NULL;

        $er = error_reporting();
        error_reporting(E_ALL ^ E_NOTICE);

        $php_code = file_get_contents($file);
        $tokens = token_get_all($php_code);
        $count = count($tokens);

        for ($i = 0; $i < $count; $i++) 
        {
            if(!$foundNS && $tokens[$i][0] == T_NAMESPACE)
            {
                $nsPos[$ii]['start'] = $i;
                $foundNS = TRUE;
            }
            elseif( $foundNS && ($tokens[$i] == ';' || $tokens[$i] == '{') )
            {
                $nsPos[$ii]['end']= $i;
                $ii++;
                $foundNS = FALSE;
            }
            elseif ($i-2 >= 0 && $tokens[$i - 2][0] == T_CLASS && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) 
            {
                if($i-4 >=0 && $tokens[$i - 4][0] == T_ABSTRACT)
                {
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'ABSTRACT CLASS');
                }
                else
                {
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'CLASS');
                }
            }
            elseif ($i-2 >= 0 && $tokens[$i - 2][0] == T_INTERFACE && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING)
            {
                $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'INTERFACE');
            }
        }
        error_reporting($er);
        if (empty($classes)) return NULL;

        if(!empty($nsPos))
        {
            foreach($nsPos as $k => $p)
            {
                $ns = '';
                for($i = $p['start'] + 1; $i < $p['end']; $i++)
                    $ns .= $tokens[$i][1];

                $ns = trim($ns);
                $final[$k] = array('namespace' => $ns, 'classes' => $classes[$k+1]);
            }
            $classes = $final;
        }
        return $classes;
    }

Outputs something like this...

array
  'namespace' => string 'test\foo' (length=8)
  'classes' => 
    array
      0 => 
        array
          'name' => string 'bar' (length=3)
          'type' => string 'CLASS' (length=5)
      1 => 
        array
          'name' => string 'baz' (length=3)
          'type' => string 'INTERFACE' (length=9)
array
  'namespace' => string 'this\is\a\really\big\namespace\for\testing\dont\you\think' (length=57)
  'classes' => 
    array
      0 => 
        array
          'name' => string 'yes_it_is' (length=9)
          'type' => string 'CLASS' (length=5)
      1 => 
        array
          'name' => string 'damn_too_big' (length=12)
          'type' => string 'ABSTRACT CLASS' (length=14)
      2 => 
        array
          'name' => string 'fogo' (length=6)
          'type' => string 'INTERFACE' (length=9)

Might help someone!

Comments

3

Use PHP's function get_declared_classes(). This returns an array of classes defined in the current script.

6 Comments

So, I'd need to compare this with the list of classes from before the include... or can you think of something more efficient?
If you're loading the files via an include, that's the most efficient method I can think of.
If you want to find out what classes are in a file, this won't do it. You can't compare get_declared_classes() before and after an include because the file may already be included/autoloaded/required.
Correct. This is assuming that it hasn't already been included.
include_once will return true if the file has already been included, so you could check if the file has been included already before using get_declared_classes
|
2

I've extended Venkat D's answer a bit to include returning the methods, and to search through a directory. (This specific example is built for CodeIgniter, which will return all the methods in the ./system/application/controller files - in other words, every public url that you can call through the system.)

function file_get_php_classes($filepath,$onlypublic=true) {
    $php_code = file_get_contents($filepath);
    $classes = get_php_classes($php_code,$onlypublic);
    return $classes;
}

function get_php_classes($php_code,$onlypublic) {
    $classes = array();
    $methods=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];
            $methods[$class_name] = array();
        }
        if ($tokens[$i - 2][0] == T_FUNCTION
        && $tokens[$i - 1][0] == T_WHITESPACE
        && $tokens[$i][0] == T_STRING) {
            if ($onlypublic) {
                if ( !in_array($tokens[$i-4][0],array(T_PROTECTED, T_PRIVATE))) {
                    $method_name = $tokens[$i][1];
                    $methods[$class_name][] = $method_name;
                }
            } else {
                $method_name = $tokens[$i][1];
                $methods[$class_name][] = $method_name;
            }
        }
    }
    return $methods;
}

function mapSystemClasses($controllerdir="./system/application/controllers/",$onlypublic=true) {
    $result=array();
    $dh=opendir($controllerdir);
    while (($file = readdir($dh)) !== false) {
        if (substr($file,0,1)!=".") {
            if (filetype($controllerdir.$file)=="file") {
                $classes=file_get_php_classes($controllerdir.$file,$onlypublic);
                foreach($classes as $class=>$method) {
                    $result[]=array("file"=>$controllerdir.$file,"class"=>$class,"method"=>$method);

                }
            } else {
                $result=array_merge($result,mapSystemClasses($controllerdir.$file."/",$onlypublic));
            }
        }
    }
    closedir($dh);
    return $result;
}

Comments

1

Nowadays (2022) you can use the Composer package roave/better-reflection.

To get all classes defined in a file you can use this code with version 5 of the package:

use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\DefaultReflector;
use Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;

$astLocator = (new BetterReflection())->astLocator();
$reflector = new DefaultReflector(new SingleFileSourceLocator('path/to/file.php', $astLocator));
$classes = $reflector->reflectAllClasses();

$classNames = [];
foreach ($classes as $class) {
    $classNames[] = $class->getName();
}

Comments

0

You can ignore abstract classes like this (note the T_ABSTRACT token):

function get_php_classes($php_code)
{
    $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 && !($tokens[$i - 3] && $i - 4 >= 0 && $tokens[$i - 4][0] == T_ABSTRACT))
        {
            $class_name = $tokens[$i][1];
            $classes[] = $class_name;
        }
    }
    return $classes;
}

1 Comment

This will raise Illegal Offset since for count starts at 2 and negative numbers are not legal array keys. (2-4 = -2)... You can correct this if you add the condition $i-4>=0 before testing for T_ABSTRACT.

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.