如何获取特定名称空间中的所有类名称?

我希望将所有类放在一个名称空间中:

#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不在列表中。

我怎么能这么做?

75771 次浏览

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

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

class_parentsspl_classes()class_uses可以用来检索所有的类名

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

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

<?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混乱使用自动加载。

定位类

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

获取类名

一旦知道了类文件的位置,就可以从文件名中提取类,因为类文件的名称应该由类名后跟 .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);

通用方法是在项目中获取所有完全限定的类名(具有完整名称空间的类) ,然后根据所需的名称空间进行筛选。

PHP 提供了一些本机函数来获取这些类(get _ Declaration _ classes 等) ,但是它们不能找到尚未加载的类(包括/要求) ,因此它不能像自动加载程序预期的那样工作(例如 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]
&& 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;
}
}
}

如果您遵循 PSR 0或 PSR 4标准(您的目录树反映您的名称空间) ,您不必过滤任何东西: 只需给出与您想要的名称空间对应的路径。

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

我将给出一个实际上正在我们的 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 用户可以在 globb ()中使用 app_path() . '/\{\{2}}/*.php'

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

这个包可以在这里找到: 海登皮尔斯/班级发现者

$ composer require haydenpierce/class-finder

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


我对这里的任何解决方案都不满意,所以我最终建立了我的类来处理这个问题:

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

简而言之,这个类试图根据在 composer.json中定义的名称空间来确定类实际位于文件系统中的哪个位置。例如,在名称空间 Backup\Test中定义的类可以在 /home/hpierce/BackupApplicationRoot/src/Test中找到。这是可信的,因为将目录结构映射到名称空间是 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));


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

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

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

因此,首先获得所有已声明的类,然后检查其中哪些类从名称空间开始。

注意 : 您将得到键不以0开始的数组。

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

对于 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;');

我只是做了一些类似的东西,这是相对简单的,但可以建立起来。

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

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

为了增加不同的可能性,这里有一个快速而简单的未经优化的函数,可以使用我能想到的最基本的技术和常用语句来完成您要求的任务:

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”,函数将返回在这两个名称空间中找到的所有类(行为就好像它是 递归的一样)。然而,调整和扩展这个函数将会很简单,远不止于此,大多数情况下需要在 foreach块中做一些调整。

它可能不是 新艺术代码的顶峰,但至少它做了什么提议,应该是简单到不言而喻。

我希望它能帮助你为实现你所追求的目标铺平道路。

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

在尝试了上面的编写器解决方案之后,对于在名称空间中获取递归类所花费的时间不满意,最多可达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
)

注意 : 这个解决方案似乎直接与 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

使用探测器

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