Home » Php » PHP – get all class names inside a particular namespace

PHP – get all class names inside a particular namespace

Posted by: admin November 29, 2017 Leave a comment

Questions:

I want to get all classes inside a namespace. I have something like this:

#File: MyClass1.php
namespace MyNamespace;

class MyClass1() { ... }

#File: MyClass2.php
namespace MyNamespace;

class MyClass2() { ... }

#Any number of files and classes with MyNamespace may be specified.

#File: ClassHandler.php
namespace SomethingElse;
use MyNamespace as Classes;

class ClassHandler {
    public function getAllClasses() {
        // Here I want every classes declared inside MyNamespace.
    }
}

I tried get_declared_classes() inside getAllClasses() but MyClass1 and MyClass2 were not in the list.

How could I do that?

Answers:

The generic approach would be to get all fully qualified classnames (class with full namespace) in your project, and then filter by the wanted namespace.

PHP offers some native functions to get those classes (get_declared_classes, etc), but they won’t be able to find classes that have not been loaded (include / require), therefore it won’t work as expected with autoloaders (like Composer for example).
This is a major issue as the usage of autoloaders is very common.

So your last resort is to find all PHP files by yourself and parse them to extract their namespace and class:

$path = __DIR__;
$fqcns = array();

$allFiles = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
$phpFiles = new RegexIterator($allFiles, '/\.php$/');
foreach ($phpFiles as $phpFile) {
    $content = file_get_contents($phpFile->getRealPath());
    $tokens = token_get_all($content);
    $namespace = '';
    for ($index = 0; isset($tokens[$index]); $index++) {
        if (!isset($tokens[$index][0])) {
            continue;
        }
        if (T_NAMESPACE === $tokens[$index][0]) {
            $index += 2; // Skip namespace keyword and whitespace
            while (isset($tokens[$index]) && is_array($tokens[$index])) {
                $namespace .= $tokens[$index++][1];
            }
        }
        if (T_CLASS === $tokens[$index][0]) {
            $index += 2; // Skip class keyword and whitespace
            $fqcns[] = $namespace.'\'.$tokens[$index][1];
        }
    }
}

If you follow PSR 0 or PSR 4 standards (your directory tree reflects your namespace), you don’t have to filter anything: just give the path that corresponds to the namespace you want.

If you’re not a fan of copying/pasting the above code snippets, you can simply install this library: https://github.com/gnugat/nomo-spaco .
If you use PHP >= 5.5, you can also use the following library: https://github.com/hanneskod/classtools .

Questions:
Answers:

I wasn’t happy with any of the solutions here so I ended up building my class to handle this. This solution requires that you are:

  • Using Composer
  • Using PSR-4

In a nutshell, this class attempts to figure out where the classes actually live on your filesystem based on the namespaces you’ve defined in composer.json. For instance, classes defined in the namespace Backup\Test are found in /home/hpierce/BackupApplicationRoot/src/Test. This can be trusted because mapping a directory structure to namespace is required by PSR-4:

The contiguous sub-namespace names after the “namespace prefix”
correspond to a subdirectory within a “base directory”, in which the
namespace separators represent directory separators. The subdirectory
name MUST match the case of the sub-namespace names.

You may need to adjust appRoot to point to the directory that contains composer.json.

<?php    
namespace Backup\Util;

class ClassFinder
{
    //This value should be the directory that contains composer.json
    const appRoot = __DIR__ . "/../../";

    public static function getClassesInNamespace($namespace)
    {
        $files = scandir(self::getNamespaceDirectory($namespace));

        $classes = array_map(function($file) use ($namespace){
            return $namespace . '\' . str_replace('.php', '', $file);
        }, $files);

        return array_filter($classes, function($possibleClass){
            return class_exists($possibleClass);
        });
    }

    private static function getDefinedNamespaces()
    {
        $composerJsonPath = self::appRoot . 'composer.json';
        $composerConfig = json_decode(file_get_contents($composerJsonPath));

        //Apparently PHP doesn't like hyphens, so we use variable variables instead.
        $psr4 = "psr-4";
        return (array) $composerConfig->autoload->$psr4;
    }

    private static function getNamespaceDirectory($namespace)
    {
        $composerNamespaces = self::getDefinedNamespaces();

        $namespaceFragments = explode('\', $namespace);
        $undefinedNamespaceFragments = [];

        while($namespaceFragments) {
            $possibleNamespace = implode('\', $namespaceFragments) . '\';

            if(array_key_exists($possibleNamespace, $composerNamespaces)){
                return realpath(self::appRoot . $composerNamespaces[$possibleNamespace] . implode('/', $undefinedNamespaceFragments));
            }

            $undefinedNamespaceFragments[] = array_pop($namespaceFragments);
        }

        return false;
    }
}

Questions:
Answers:

Pretty interesting that there does not seem to be any reflection method that does that for you. However I came up with a little class that is capable of reading namespace information.

In order to do so, you have to traverse trough all defined classes. Then we get the namespace of that class and store it into an array along with the classname itself.

<?php

// ClassOne namespaces -> ClassOne
include 'ClassOne/ClassOne.php';

// ClassOne namespaces -> ClassTwo
include 'ClassTwo/ClassTwo.php';
include 'ClassTwo/ClassTwoNew.php';

// So now we have two namespaces defined 
// by ourselves (ClassOne -> contains 1 class, ClassTwo -> contains 2 classes)

class NameSpaceFinder {

    private $namespaceMap = [];
    private $defaultNamespace = 'global';

    public function __construct()
    {
        $this->traverseClasses();
    }

    private function getNameSpaceFromClass($class)
    {
        // Get the namespace of the given class via reflection.
        // The global namespace (for example PHP's predefined ones)
        // will be returned as a string defined as a property ($defaultNamespace)
        // own namespaces will be returned as the namespace itself

        $reflection = new \ReflectionClass($class);
        return $reflection->getNameSpaceName() === '' 
                ? $this->defaultNamespace
                : $reflection->getNameSpaceName();
    }

    public function traverseClasses()
    {
        // Get all declared classes
        $classes = get_declared_classes();

        foreach($classes AS $class)
        {
            // Store the namespace of each class in the namespace map
            $namespace = $this->getNameSpaceFromClass($class);
            $this->namespaceMap[$namespace][] = $class;
        }
    }

    public function getNameSpaces()
    {
        return array_keys($this->namespaceMap);
    }

    public function getClassesOfNameSpace($namespace)
    {
        if(!isset($this->namespaceMap[$namespace]))
            throw new \InvalidArgumentException('The Namespace '. $namespace . ' does not exist');

        return $this->namespaceMap[$namespace];
    }

}

$finder = new NameSpaceFinder();
var_dump($finder->getClassesOfNameSpace('ClassTwo'));

The output will be:

array(2) { [0]=> string(17) "ClassTwo\ClassTwo" [1]=> string(20) "ClassTwo\ClassTwoNew" }

Of course everything besides the NameSpaceFinder class itself if assembled quick and dirty. So feel free to clean up the include mess by using autoloading.

Questions:
Answers:

Locate Classes

A class can be localized in the file system by its name and its namespace, like the autoloader does. In the normal case the namespace should tell the relative path to the class files. The include paths are the starting points of the relative paths. The function get_include_path() returns a list of include paths in one string. Each include path can be tested, whether there exists a relative path which matches the namespace. If the matching path is found, you will know the location of the class files.

Get Class Names

As soon as the location of the class files is known, the classes can be extracted from the file names, because the name of a class file should consist of the class name followed by .php.

Sample Code

Here is a sample code to get all class names of the namespace foo\bar as a string array:

$namespace = 'foo\bar';

// Relative namespace path
$namespaceRelativePath = str_replace('\', DIRECTORY_SEPARATOR, $namespace);

// Include paths
$includePathStr = get_include_path();
$includePathArr = explode(PATH_SEPARATOR, $includePathStr);

// Iterate include paths
$classArr = array();
foreach ($includePathArr as $includePath) {
    $path = $includePath . DIRECTORY_SEPARATOR . $namespaceRelativePath;
    if (is_dir($path)) { // Does path exist?
        $dir = dir($path); // Dir handle     
        while (false !== ($item = $dir->read())) {  // Read next item in dir
            $matches = array();
            if (preg_match('/^(?<class>[^.].+)\.php$/', $item, $matches)) {
                $classArr[] = $matches['class'];
            }
        }
        $dir->close();
    }
}

// Debug output
var_dump($includePathArr);
var_dump($classArr);

Questions:
Answers:

I am going to give an example which is actually being used in our Laravel 5 app but can be used almost everywhere. The example returns class names with the namespace which can be easily taken out, if not required.

Legend

  • {{1}} – Path to remove from current file’s path to get to app folder
  • {{2}} – The folder path from app folder where the target classes exist
  • {{3}} – Namespace path

Code

$classPaths = glob(str_replace('{{1}}', '',__DIR__) .'{{2}}/*.php');
$classes = array();
$namespace = '{{3}}';
foreach ($classPaths as $classPath) {
    $segments = explode('/', $classPath);
    $segments = explode('\', $segments[count($segments) - 1]);
    $classes[] = $namespace . $segments[count($segments) - 1];
}

Laravel people can use app_path() . '/{{2}}/*.php' in glob().

Questions:
Answers:

I think a lot of people might have a problem like this, so I relied on the answers from @hpierce and @loïc-faugeron to solve this problem.

With the class described below, you can have all classes within a namespace or they respect a certain term.

<?php

namespace Backup\Util;

final class ClassFinder
{
    private static $composer = null;
    private static $classes  = [];

    public function __construct()
    {
        self::$composer = null;
        self::$classes  = [];

        self::$composer = require APP_PATH . '/vendor/autoload.php';

        if (false === empty(self::$composer)) {
            self::$classes  = array_keys(self::$composer->getClassMap());
        }
    }

    public function getClasses()
    {
        $allClasses = [];

        if (false === empty(self::$classes)) {
            foreach (self::$classes as $class) {
                $allClasses[] = '\' . $class;
            }
        }

        return $allClasses;
    }

    public function getClassesByNamespace($namespace)
    {
        if (0 !== strpos($namespace, '\')) {
            $namespace = '\' . $namespace;
        }

        $termUpper = strtoupper($namespace);
        return array_filter($this->getClasses(), function($class) use ($termUpper) {
            $className = strtoupper($class);
            if (
                0 === strpos($className, $termUpper) and
                false === strpos($className, strtoupper('Abstract')) and
                false === strpos($className, strtoupper('Interface'))
            ){
                return $class;
            }
            return false;
        });
    }

    public function getClassesWithTerm($term)
    {
        $termUpper = strtoupper($term);
        return array_filter($this->getClasses(), function($class) use ($termUpper) {
            $className = strtoupper($class);
            if (
                false !== strpos($className, $termUpper) and
                false === strpos($className, strtoupper('Abstract')) and
                false === strpos($className, strtoupper('Interface'))
            ){
                return $class;
            }
            return false;
        });
    }
}

In this case, you must use Composer to perform class autoloading. Using the ClassMap available on it, the solution is simplified.

Questions:
Answers:

The easiest way should be to use your own autoloader __autoload function and inside of it save the loaded classes names. Does that suits You ?

Otherwise I think You will have to deal with some reflection methods.

Questions:
Answers:

class_parents, spl_classes() and class_uses can be used to retrieve all the class names

Questions:
Answers:

You can use get_declared_classes but with a little additional work.

$needleNamespace = 'MyNamespace';
$classes = get_declared_classes();
$neededClasses = array_filter($classes, function($i) use ($needleNamespace) {
    return strpos($i, $needleNamespace) === 0;
});

So first you get all declared classes and then check which of them starts with your namespace.

Note: you will get array where keys do not start with 0. To achive this, you can try: array_values($neededClasses);.

Leave a Reply

Your email address will not be published. Required fields are marked *