Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get all class names inside a particular namespace?

Tags:

namespaces

php

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?

like image 792
Pedram Behroozi Avatar asked Mar 31 '14 12:03

Pedram Behroozi


3 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]
            && T_WHITESPACE === $tokens[$index + 1][0]
            && T_STRING === $tokens[$index + 2][0]
        ) {
            $namespace = $tokens[$index + 2][1];
            // Skip "namespace" keyword, whitespaces, and actual namespace
            $index += 2;
        }
        if (
            T_CLASS === $tokens[$index][0]
            && T_WHITESPACE === $tokens[$index + 1][0]
            && T_STRING === $tokens[$index + 2][0]
        ) {
            $fqcns[] = $namespace.'\\'.$tokens[$index + 2][1];
            // Skip "class" keyword, whitespaces, and actual classname
            $index += 2;

            # break if you have one class per file (psr-4 compliant)
            # otherwise you'll need to handle class constants (Foo::class)
            break;
        }
    }
}

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 .

like image 157
Loïc Faugeron Avatar answered Sep 24 '22 19:09

Loïc Faugeron


Update: Since this answer became somewhat popular, I've created a packagist package to simplify things. It contains basically what I've described here, without the need to add the class yourself or configure the $appRoot manually. It may eventually support more than just PSR-4.

That package can be found here: haydenpierce/class-finder.

$ composer require haydenpierce/class-finder

See more info in the README file.


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));

        return (array) $composerConfig->autoload->{'psr-4'};
    }

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

            array_unshift($undefinedNamespaceFragments, array_pop($namespaceFragments));            
        }

        return false;
    }
}
like image 33
HPierce Avatar answered Sep 23 '22 19:09

HPierce


Quite a few interesting answers above, some actually peculiarly complex for the proposed task.

To add a different flavor to the possibilities, here a quick and easy non-optimized function to do what you ask using the most basic techniques and common statements I could think of:

function classes_in_namespace($namespace) {
      $namespace .= '\\';
      $myClasses  = array_filter(get_declared_classes(), function($item) use ($namespace) { return substr($item, 0, strlen($namespace)) === $namespace; });
      $theClasses = [];
      foreach ($myClasses AS $class):
            $theParts = explode('\\', $class);
            $theClasses[] = end($theParts);
      endforeach;
      return $theClasses;
}

Use simply as:

$MyClasses = classes_in_namespace('namespace\sub\deep');

var_dump($MyClasses);

I've written this function to assume you are not adding the last "trailing slash" (\) on the namespace, so you won't have to double it to escape it. ;)

Please notice this function is only an example and has many flaws. Based on the example above, if you use 'namespace\sub' and 'namespace\sub\deep' exists, the function will return all classes found in both namespaces (behaving as if it was recursive). However, it would be simple to adjust and expand this function for much more than that, mostly requiring a couple of tweaks in the foreach block.

It may not be the pinnacle of the code-art-nouveau, but at least it does what was proposed and should be simple enough to be self-explanatory.

I hope it helps pave the way for you to achieve what you are looking for.

Note: PHP 5, 7, AND 8 friendly.

like image 39
Julio Marchi Avatar answered Sep 21 '22 19:09

Julio Marchi