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.

random
  • 9,774
  • 10
  • 66
  • 83
Allain Lalonde
  • 91,574
  • 70
  • 187
  • 238
  • 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. – Corey Ballou Jan 12 '10 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. – Rudie Aug 25 '11 at 21:41

9 Answers9

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;
}
Venkat D.
  • 2,979
  • 35
  • 42
  • Not all tokens are arrays so this can give some warnings. – hakre Jun 17 '12 at 10:20
  • @hakre: It wouldn't because if `$tokens[$i]` is a string, the syntax `$tokens[i][0]` is still allowed. – netcoder Jul 13 '12 at 15:52
  • Right, but I suggest to use `===` for comparisons in that case. – hakre Jul 14 '12 at 09:03
  • A version with namespaces is available here: https://stackoverflow.com/questions/22761554/php-get-all-class-names-inside-a-particular-namespace/27440555#27440555 – Jakub Zalas Jun 07 '18 at 09:26
  • Just to be thorough, I wanted to verify that the syntax specified here matches the language specification. It does! https://github.com/php/php-langspec/blob/master/spec/14-classes.md#class-declarations – Carter Pape Aug 05 '19 at 02:22
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
Jeremy Harris
  • 24,318
  • 13
  • 79
  • 133
cletus
  • 616,129
  • 168
  • 910
  • 942
  • 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. – okdewit Nov 24 '16 at 22:25
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.

Ondrej Machulda
  • 998
  • 1
  • 12
  • 24
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;
}
frosty22
  • 513
  • 1
  • 5
  • 11
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!

Tivie
  • 18,864
  • 5
  • 58
  • 77
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;
}
2

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

James Skidmore
  • 49,340
  • 32
  • 108
  • 136
  • So, I'd need to compare this with the list of classes from before the include... or can you think of something more efficient? – Allain Lalonde May 30 '09 at 03:41
  • If you're loading the files via an include, that's the most efficient method I can think of. – James Skidmore May 30 '09 at 03:45
  • 1
    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. – cletus May 30 '09 at 03:45
  • Correct. This is assuming that it hasn't already been included. – James Skidmore May 30 '09 at 03:48
  • 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 – Andrei Serdeliuc ॐ May 30 '09 at 04:32
  • Yes but that doesn't help you determine what the classes are if it already has been loaded. – cletus May 30 '09 at 05:13
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;
}
hajamie
  • 2,848
  • 2
  • 22
  • 20
  • 2
    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. – Tivie Jun 20 '12 at 07:10
0

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();
}
Michael D.
  • 96
  • 1
  • 5