web-dev-qa-db-ja.com

PHP-特定の名前空間内のすべてのクラス名を取得する

名前空間内のすべてのクラスを取得したい。私はこのようなものを持っています:

_#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()を試しましたが、_MyClass1_と_MyClass2_はリストにありませんでした。

どうすればそれができますか?

47
Pedram Behroozi

一般的なアプローチは、プロジェクト内のすべての完全修飾クラス名(完全な名前空間を持つクラス)を取得し、必要な名前空間でフィルター処理することです。

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 .

27
Loïc Faugeron

Update:この回答はやや人気が出てきたので、物事を単純化するためにpackagistパッケージを作成しました。クラスを自分で追加したり、手動で$appRootを設定したりする必要なしに、基本的にここで説明した内容が含まれています。最終的にはPSR-4以上のものをサポートするかもしれません。

そのパッケージはここにあります: haydenpierce/class-Finder

$ composer require haydenpierce/class-Finder

詳細については、READMEファイルを参照してください。


私はここの解決策のどれにも満足していなかったので、これを処理するためにクラスを構築することになりました。 この解決策は、あなたがいる必要があります

  • Composerを使用する
  • PSR-4を使用する

一言で言えば、このクラスは、composer.jsonで定義した名前空間に基づいて、クラスが実際にファイルシステムのどこに存在するかを把握しようとします。たとえば、名前空間Backup\Testで定義されたクラスは/home/hpierce/BackupApplicationRoot/src/Testにあります。ディレクトリ構造を名前空間にマッピングするのは PSR-4で必要 であるため、これは信頼できます

「ネームスペースプレフィックス」の後の連続するサブネームスペース名は、「ベースディレクトリ」内のサブディレクトリに対応します。ここで、ネームスペースセパレータはディレクトリセパレータを表します。サブディレクトリ名は、サブ名前空間名の大文字小文字と一致しなければなりません。

composer.jsonを含むディレクトリを指すようにappRootを調整する必要がある場合があります。

<?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;
    }
}
21
HPierce

あなたのためにそれを行う反射法がないように見えることはかなり興味深い。しかし、名前空間情報を読み取ることができる小さなクラスを思いつきました。

そのためには、定義されたすべてのクラスをトラバースする必要があります。次に、そのクラスの名前空間を取得し、クラス名自体とともに配列に格納します。

_<?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混乱をクリーンアップしてください。

6
thpl

多くの人がこのような問題を抱えているかもしれないと思うので、この問題を解決するために@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を使用すると、ソリューションが簡素化されます。

クラスを見つける

クラスは、オートローダーのように、名前と名前空間によってファイルシステムにローカライズできます。通常、名前空間はクラスファイルへの相対パスを指定する必要があります。インクルードパスは、相対パスの開始点です。関数 get_include_path() は、1つの文字列に含まれるパスのリストを返します。名前空間に一致する相対パスが存在するかどうかにかかわらず、各インクルードパスをテストできます。一致するパスが見つかると、クラスファイルの場所がわかります。

クラス名を取得する

クラスファイルの名前はクラス名の後に.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);
2
Henrik

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'を使用できます。

2
Umair Ahmed

_class_parents_、spl_classes()、および_class_uses_を使用して、すべてのクラス名を取得できます

1
Harsh Chunara

_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);を試してください。

1
FreeLightman

symfonyでは、Finderコンポーネントを使用できます:

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

0

上記の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
)
0
Cylosh

上記の非常にいくつかの興味深い答え、提案されたタスクには実際には複雑なものがあります。

可能性に別のフレーバーを追加するために、ここで考えられる最も基本的なテクニックと一般的なステートメントを使用して、求めていることを実行するための迅速かつ簡単な最適化されていない関数:

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

notlast "末尾のスラッシュ"(\)ネームスペースで、エスケープするために2倍にする必要はありません。 ;)

この関数は単なる例であり、多くの欠陥があることに注意してください。上記の例に基づいて、「namespace\sub 'および' namespace\sub\deep 'が存在する場合、関数は両方の名前空間で見つかったすべてのクラスを返します(あたかもrecursiveのように動作します)。ただし、この関数をそれ以上に調整および拡張するのは簡単で、ほとんどの場合、foreachブロックでいくつかの調整が必要です。

code-art-nouveauの頂点ではないかもしれませんが、少なくとも提案されたことを実行し、自明であるように十分にシンプルであるべきです。

あなたが探しているものを達成するための道を開く助けになることを願っています。

注:PHP 5および7フレンドリー。

0
Julio Marchi

最も簡単な方法は、独自のオートローダーを使用することです__autoload関数とその内部は、ロードされたクラス名を保存します。それはあなたに合っていますか?

それ以外の場合、いくつかのリフレクションメソッドを処理する必要があると思います。

0
Uriziel

私はちょうど似たようなことをしました、これは比較的簡単ですが、構築することができます。

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