PHP - 获取特定命名空间内的所有类名

时间:2014-03-31 12:28:54

标签: php namespaces

我想在命名空间中获取所有类。我有这样的事情:

#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.
    }
}

我在get_declared_classes()内尝试getAllClasses(),但MyClass1MyClass2不在列表中。

我怎么能这样做?

15 个答案:

答案 0 :(得分:25)

通用方法是在项目中获取所有完全限定的类名(具有完整命名空间的类),然后按所需的命名空间进行过滤。

PHP提供了一些本机函数来获取这些类(get_declared_classes等),但是它们无法找到尚未加载的类(include / require),因此它不会像以前一样工作预期与自动加载器(例如Composer)。 这是一个主要问题,因为自动加载器的使用非常普遍。

所以你最后的办法是自己找到所有PHP文件并解析它们以提取它们的命名空间和类:

$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] && T_WHITESPACE === $tokens[$index + 1][0] && T_STRING === $tokens[$index + 2][0]) {
            $index += 2; // Skip class keyword and whitespace
            $fqcns[] = $namespace.'\\'.$tokens[$index][1];

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

如果您遵循PSR 0或PSR 4标准(您的目录树反映了您的命名空间),您就不必过滤任何内容:只需提供与您想要的命名空间相对应的路径。

如果您不是复制/粘贴上述代码段的粉丝,则只需安装此库:https://github.com/gnugat/nomo-spaco即可。 如果您使用PHP> = 5.5,您还可以使用以下库:https://github.com/hanneskod/classtools

答案 1 :(得分:17)

更新:由于这个答案变得有些流行,我创建了一个packagist包以简化操作。它基本上包含了我在这里描述的内容,无需自己添加类或手动配置$appRoot。它最终可能不仅仅支持PSR-4。

可以在此处找到该软件包:haydenpierce/class-finder

$ composer require haydenpierce/class-finder

请参阅自述文件中的更多信息。

我对这里的任何解决方案都不满意所以我最终建立了我的课来处理这个问题。 此解决方案要求您

  • 使用Composer
  • 使用PSR-4

简而言之,这个类试图根据你在composer.json中定义的命名空间来确定类实际存在于文件系统中的位置。例如,在Backup\Test中可以找到命名空间/home/hpierce/BackupApplicationRoot/src/Test中定义的类。这可以信任,因为将目录结构映射到命名空间是required by PSR-4

  

“名称空间前缀”之后的连续子命名空间名称   对应于“基础目录”中的子目录,其中   名称空间分隔符表示目录分隔符。子目录   name必须匹配子命名空间名称的大小写。

您可能需要调整appRoot以指向包含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));
            }

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

        return false;
    }
}

答案 2 :(得分:5)

非常有趣的是,似乎没有任何反射方法可以帮助您。但是我提出了一个能够读取名称空间信息的小类。

为了做到这一点,你必须遍历所有已定义的类。然后我们获取该类的名称空间并将其与类名本身一起存储到数组中。

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

输出将是:

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

当然除了NameSpaceFinder类本身之外的所有内容如果组装得快而脏。因此,请随意使用自动加载来清理include混乱。

答案 3 :(得分:3)

我认为很多人可能会遇到这样的问题,所以我依靠@hpierce和@loïc-faugeron的答案来解决这个问题。

使用下面描述的类,您可以拥有命名空间中的所有类,或者它们尊重某个术语。

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

在这种情况下,您必须使用Composer执行类自动加载。使用可用的ClassMap,简化了解决方案。

答案 4 :(得分:2)

找到类

类可以通过其名称和名称空间在文件系统中进行本地化,就像自动加载器一样。在正常情况下,命名空间应该告诉类文件的相对路径。包含路径是相对路径的起点。函数get_include_path()返回一个字符串中的包含路径列表。可以测试每个包含路径,是否存在与命名空间匹配的相对路径。如果找到匹配的路径,您将知道类文件的位置。

获取班级名称

只要知道了类文件的位置,就可以从文件名中提取类,因为类文件的名称应该由类名后跟.php组成。

示例代码

下面是一个示例代码,用于将命名空间foo\bar的所有类名称作为字符串数组:

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

答案 5 :(得分:2)

我将举一个实际在我们的Laravel 5应用程序中使用的示例,但几乎可以在任何地方使用。该示例返回带有命名空间的类名,如果不需要,可以轻松取出。

图例

  • $classPaths = glob(str_replace('app_path() . '/{{2}}/*.php'', '',__DIR__) .'{{2}}/*.php'); $classes = array(); $namespace = 'enter image description here'; foreach ($classPaths as $classPath) { $segments = explode('/', $classPath); $segments = explode('\\', $segments[count($segments) - 1]); $classes[] = $namespace . $segments[count($segments) - 1]; } - 从当前文件的路径中删除以转到app文件夹的路径
  • $plot = new PHPlot();
    
    //Define some data
    $example_data = array(
         array('Learning',3),
         array('Reading',3),
         array('Writing',4),
         array('Oral Communication',2),
         array('Numeracy',2),
    );
    $plot->SetDataValues($example_data);
    $plot->TuneYAutoRange(4, 'decimal');
    $plot->SetYTickIncrement(1);
    
    - 目标类所在的app文件夹的文件夹路径
  • NSIS dialog layout - 命名空间路径

代码

$plot->TuneYAutoRange(0, 'R', 0);

Laravel人可以在glob()中使用find ${base:-.} -type d | sed 's%/[^/]*$%%' | sort -u

答案 6 :(得分:1)

class_parentsspl_classes()class_uses可用于检索所有类名

答案 7 :(得分:0)

最简单的方法应该是使用您自己的自动加载器__autoload函数,并在其中保存加载的类名称。这适合你吗?

否则我认为你将不得不处理一些反思方法。

答案 8 :(得分:0)

您可以使用get_declared_classes,但需要额外的工作。

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

首先,您获取所有已声明的类,然后检查它们中的哪一个以您的命名空间开头。

注意:您将获得数据,其中键不以0开头。要实现此目的,您可以尝试:array_values($neededClasses);

答案 9 :(得分:0)

对于symfony,您可以使用Finder组件:

http://symfony.com/doc/current/components/finder.html

答案 10 :(得分:0)

我只是做了类似的事情,这相对简单,但是可以建立。

  public function find(array $excludes, ?string $needle = null)
  {
    $path = "../".__DIR__;
    $files = scandir($path);
    $c = count($files);
    $models = [];
    for($i=0; $i<$c; $i++) {
      if ($files[$i] == "." || $files[$i] == ".." || in_array($dir[$i], $excludes)) {
        continue;
      }
      $model = str_replace(".php","",$dir[$i]);
      if (ucfirst($string) == $model) {
        return $model;
      }
      $models[] = $model;
    }
    return $models;
  }

答案 11 :(得分:0)

上面有一些有趣的答案,对于建议的任务,实际上有些特别复杂。

为了给可能性增加不同的风味,这里有一个快速简便的未优化功能,可以使用我可能想到的最基本的技术和常见的语句来完成您要求的操作:

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

仅用作:

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

var_dump($MyClasses);

我编写此函数是为了假设您不是在名称空间上添加最后“后跟斜杠”(\),因此您不会不必将其加倍即可逃脱。 ;)

请注意,此功能仅是示例,有很多缺陷。根据上面的示例,如果您使用'namespace\sub'并且'namespace\sub\deep'存在,该函数将返回在两个命名空间中找到的所有类(表现为 recursive )。但是,调整和扩展此功能的范围远不止于此,这主要是需要在foreach块中进行几处调整。

它可能不是 code-art-nouveau 的巅峰之作,但至少它可以完成提议的内容,并且应该足够简单,以不言自明。

我希望它能为您实现所需的目标铺平道路。

注意:PHP 5和7友好。

答案 12 :(得分:0)

在尝试了以上的作曲家解决方案之后,对于在名称空间中获取递归类所花费的时间不满意,最长为3秒,但是在某些机器上花费了6-7秒,这是不可接受的。下面的类在正常的3-4级深度目录结构中以〜0.05呈现类。

   
namespace Helpers;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class ClassHelper
{
    public static function findRecursive(string $namespace): array
    {
        $namespacePath = self::translateNamespacePath($namespace);

        if ($namespacePath === '') {
            return [];
        }

        return self::searchClasses($namespace, $namespacePath);
    }

    protected static function translateNamespacePath(string $namespace): string
    {
        $rootPath = __DIR__ . DIRECTORY_SEPARATOR;

        $nsParts = explode('\\', $namespace);
        array_shift($nsParts);

        if (empty($nsParts)) {
            return '';
        }

        return realpath($rootPath. implode(DIRECTORY_SEPARATOR, $nsParts)) ?: '';
    }

    private static function searchClasses(string $namespace, string $namespacePath): array
    {
        $classes = [];

        /**
         * @var \RecursiveDirectoryIterator $iterator
         * @var \SplFileInfo $item
         */
        foreach ($iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($namespacePath, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        ) as $item) {
            if ($item->isDir()) {
                $nextPath = $iterator->current()->getPathname();
                $nextNamespace = $namespace . '\\' . $item->getFilename();
                $classes = array_merge($classes, self::searchClasses($nextNamespace, $nextPath));
                continue;
            }
            if ($item->isFile() && $item->getExtension() === 'php') {
                $class = $namespace . '\\' . $item->getBasename('.php');
                if (!class_exists($class)) {
                    continue;
                }
                $classes[] = $class;
            }
        }

        return $classes;
    }
}

用法:    

    $classes = ClassHelper::findRecursive(__NAMESPACE__);
    print_r($classes);

结果:

Array
(
    [0] => Helpers\Dir\Getters\Bar
    [1] => Helpers\Dir\Getters\Foo\Bar
    [2] => Helpers\DirSame\Getters\Foo\Cru
    [3] => Helpers\DirSame\Modifiers\Foo\Biz
    [4] => Helpers\DirSame\Modifiers\Too\Taz
    [5] => Helpers\DirOther\Modifiers\Boo
)

答案 13 :(得分:0)

如果您已经使用Composer进行PSR-4兼容的自动加载,则可以使用此方法获取所有自动加载的类并对其进行过滤(这是我的模块系统中的示例,直接从那里复制并粘贴):

function get_available_widgets()
{
    $namespaces = array_keys((new ComposerClassMap)->listClasses());
    return array_filter($namespaces, function($item){
        return Str::startsWith($item, "App\\Modules\\Widgets\\") && Str::endsWith($item, "Controller");
    });
}

答案 14 :(得分:0)

使用查找器

作曲家需要 symfony/finder

public function getAllNameSpaces($path)
{
    $filenames = $this->getFilenames($path);
    $namespaces = [];
    foreach ($filenames as $filename) {
        $namespaces[] = $this->getFullNamespace($filename) . '\\' . $this->getClassName($filename);
    }
    return $namespaces;
}

private function getClassName($filename)
{
    $directoriesAndFilename = explode('/', $filename);
    $filename = array_pop($directoriesAndFilename);
    $nameAndExtension = explode('.', $filename);
    $className = array_shift($nameAndExtension);
    return $className;
}

private function getFullNamespace($filename)
{
    $lines = file($filename);
    $array = preg_grep('/^namespace /', $lines);
    $namespaceLine = array_shift($array);
    $match = [];
    preg_match('/^namespace (.*);$/', $namespaceLine, $match);
    $fullNamespace = array_pop($match);

    return $fullNamespace;
}

private function getFilenames($path)
{
    $finderFiles = Finder::create()->files()->in($path)->name('*.php');
    $filenames = [];
    foreach ($finderFiles as $finderFile) {
        $filenames[] = $finderFile->getRealpath();
    }
    return $filenames;
}