【问题标题】:How to get all class names inside a particular namespace?如何获取特定命名空间内的所有类名?
【发布时间】:2014-05-10 18:58:53
【问题描述】:

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

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

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

我该怎么做?

【问题讨论】:

    标签: php namespaces


    【解决方案1】:

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

    PHP 提供了一些本机函数来获取这些类(get_declared_classes 等),但它们无法找到尚未加载的类(包括/要求),因此它无法按预期使用自动加载器(例如作曲家)。 这是一个主要问题,因为自动加载器的使用非常普遍。

    所以你最后的办法是自己查找所有 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 标准(您的目录树反映了您的命名空间),则无需过滤任何内容:只需提供与您想要的命名空间对应的路径即可。

    如果你不喜欢复制/粘贴上面的代码 sn-ps,你可以简单地安装这个库:https://github.com/gnugat/nomo-spaco。 如果您使用 PHP >= 5.5,还可以使用以下库:https://github.com/hanneskod/classtools

    【讨论】:

    • +1 for github.com/hanneskod/classtools - 在这个确切的问题中如何使用它来解决问题并不是很明显,但是通过一些实验它就可以完成工作。它比这里列出的其他包/技术要快很多(可能是因为我可以针对特定目录),但它仍然相对较慢。我在构建/部署时使用它,所以它不是一个问题。我不会在浏览器请求生命周期中运行它。
    • 捕获命名空间后的while循环是什么?我一直在测试,我只能看到需要$namespace .= $tokens[$index++][1]; 这一行,所以希望有人能启发我。
    【解决方案2】:

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

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

    $ composer require haydenpierce/class-finder
    

    在 README 文件中查看更多信息。


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

    • 使用作曲家
    • 使用 PSR-4

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

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

    您可能需要调整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));
    
            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;
        }
    }
    

    【讨论】:

    • @PaulBasenko,你能打开一个问题吗?我宁愿你在 Gitlab 上报告问题,而不是在这里。
    • 哦,你还支持这个包吗?
    • 好的,谢谢回复。我没有看到包是新创建的,抱歉)))如果我以后会重现错误,我会在 Gitlab 包工作流中创建一个问题。
    • 这可能是一种通用方法,但速度很慢。我尝试在 ZF3 中使用您的 package 并具有大量依赖项。结果 - 生产力受到影响。
    • @dearsina,我不确定我是否完全理解“执行”课程的意思。我 90% 确定库在返回它们之前确实调用了 class_exists() - 我认为如果它试图在引用不存在的类的类上调用它会导致错误(例如,如果你有不存在的类部署在生产中)。这可能是也可能不是你在说什么。如果您有任何建议,我绝对愿意接受改进,但我更愿意在 Gitlab 上进行讨论,而不是在这里的 cmets :)
    【解决方案3】:

    上面有很多有趣的答案,其中一些实际上对于提议的任务特别复杂。

    为了给可能性添加不同的风味,这里有一个快速简单的非优化函数,可以使用我能想到的最基本的技术和常用语句来完成您所要求的事情:

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

    我编写此函数是为了假设您在命名空间上添加 last“斜杠”(\),因此您不会有加倍以逃避它。 ;)

    请注意这个函数只是一个例子,有很多缺陷。基于上面的例子,如果你使用'namespace\sub'并且'namespace\sub\deep'存在,该函数将返回在两个命名空间中找到的所有类(表现得好像它是递归)。然而,调整和扩展这个功能会很简单,主要需要在 foreach 块中进行一些调整。

    它可能不是 code-art-nouveau 的巅峰之作,但至少它做到了所提议的并且应该足够简单以至于不言自明。

    我希望它可以帮助您为实现您的目标铺平道路。

    注意:PHP 5、7 和 8 友好。

    【讨论】:

    • 因为这是基于get_declared_class() 它只会找到已经加载的类。所以 IMO 这个解决方案是不完整的,因为它错过了问题的要点。
    • 我很抱歉,但我不确定你的意思......请启发我:问题的重点是什么?关于您的评论,get_declared_class() 当然只会列出已加载的类。它适用于任何内置的 PHP 函数,例如 class_exists()get_declared_interfaces()get_defined_functions()(仅举几例)。如果未加载文件,则其中的任何内容都不存在。但是,任何初级程序员都明白这种显而易见性,并且只有在他们知道他们正在考虑的“细节”已加载时才调用解释“细节”的函数。
    • 没错,但重点是:如何在命名空间中获取所有类,即使它们没有加载。他已经尝试过get_declared_classes(),但它错过了 2 个卸载的课程。所以这还不够。这就是为什么其他答案更复杂的原因,因为它们包括扫描目录以加载所有相关类文件的部分。
    • 你说的是问题中的NOT。 Pedram Behroozi 无处要求“类,即使它们加载”。您自己添加它。如果您有这样的问题,请提出,但不要仅仅因为您想象问题中实际上不存在的东西而对答案提出异议。底线是:如果你想在 PHP 中存在某些东西,它必须是 requiredincludedPSR-loaded。仅仅因为文件夹中有.php 文件并不意味着PHP 会使用它们。如果要在未加载的文件中查找类,则需要手动解析和解释它们。
    • 请再次阅读问题中的这句话:*我在 getAllClasses() 中尝试了 get_declared_classes() 但 MyClass1 和 MyClass2 不在列表中。”所以提问者想要MyClass1MyClass2即使它们没有加载。他已经尝试过get_declared_classes(),但这不是他想要的。所以这个答案没有添加任何问题中没有的内容。
    【解决方案4】:

    很有趣,似乎没有任何反射方法可以为您做到这一点。但是我想出了一个能够读取命名空间信息的小类。

    为此,您必须遍历所有已定义的类。然后我们获取该类的命名空间并将其与类名本身一起存储到一个数组中。

    <?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]=&gt; string(17) "ClassTwo\ClassTwo" [1]=&gt; string(20) "ClassTwo\ClassTwoNew" }

    当然,除了 NameSpaceFinder 类本身之外的所有东西,如果组装得又快又脏的话。所以请随意使用自动加载来清理include 的混乱。

    【讨论】:

    • 实际上自动加载会破坏您的解决方案,因为get_declared_classes() 将只包含已经自动加载的类。这需要使用 MyClass1 和 MyClass2(比如“自动加载”),然后才能找到它们。
    【解决方案5】:

    我想很多人可能会有这样的问题,所以我依靠@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,解决方案被简化了。

    【讨论】:

    • 请注意,您必须使用-o 选项生成一个优化的自动加载器,才能与所有类一起使用。 composer dump-autoload -o
    • 是的!自动生成,你们都很好。相当快!比其他的更喜欢这个
    【解决方案6】:

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

    传说

    • {{1}} - 从当前文件路径中删除以获取应用文件夹的路径
    • {{2}} - 目标类所在应用文件夹的文件夹路径
    • {{3}} - 命名空间路径

    代码

    $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 人可以在 glob() 中使用app_path() . '/{{2}}/*.php'

    【讨论】:

      【解决方案7】:

      在尝试了上面的 composer 解决方案后,对获取命名空间内的递归类所花费的时间不满意,最多 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
      )
      

      【讨论】:

      • 我试过haydenpierce/class-finder,效果很好,但速度超级慢。这种方法很快。
      【解决方案8】:

      使用查找器

      composer 需要 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;
      }
      

      【讨论】:

        【解决方案9】:

        定位类

        一个类可以通过其名称和名称空间在文件系统中本地化,就像自动加载器一样。在正常情况下,命名空间应该告诉类文件的相对路径。包含路径是相对路径的起点。函数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);
        

        【讨论】:

          【解决方案10】:

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

          【讨论】:

            【解决方案11】:

            您可以使用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);

            【讨论】:

            • 又好又短,给你最大的灵活性。但是请注意,应在名称空间之后包含 \\ 以排除任何包含针头名称空间的名称空间。就像在命名空间 App 中一样,AppendIterator 类也将被包含在内。
            【解决方案12】:

            注意:此解决方案似乎可以直接与 Laravel 一起使用。对于 Laravel 外部,您可能需要从给定源中复制和修改 ComposerClassMap 类。我没试过。

            如果您已经在使用 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");
                });
            }
            

            ComposerClassMap 类的来源:https://github.com/facade/ignition/blob/master/src/Support/ComposerClassMap.php

            【讨论】:

            【解决方案13】:

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

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

            【讨论】:

            • 我认为你必须知道类名才能使用__autoload。例如:$a = new MyClass1()。我只知道命名空间,不知道类名。
            【解决方案14】:

            我只是做了类似的事情,这个比较简单,但可以构建出来。

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

            【讨论】:

              【解决方案15】:

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

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

              $result1 = $finder->in(__DIR__)->files()->contains('namespace foo;');
              $result2 = $finder->in(__DIR__)->files()->contains('namespace bar;');
              

              【讨论】:

                猜你喜欢
                • 2010-09-25
                • 2011-06-21
                • 2011-03-22
                • 2010-10-31
                • 2018-07-19
                • 1970-01-01
                相关资源
                最近更新 更多